@livestore/common 0.3.1 → 0.3.2-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts +35 -0
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
- package/dist/ClientSessionLeaderThreadProxy.js +6 -0
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
- package/dist/adapter-types.d.ts +10 -161
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +5 -49
- package/dist/adapter-types.js.map +1 -1
- package/dist/defs.d.ts +20 -0
- package/dist/defs.d.ts.map +1 -0
- package/dist/defs.js +12 -0
- package/dist/defs.js.map +1 -0
- package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +26 -24
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -0
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +112 -122
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +17 -6
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +32 -17
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +1 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +37 -7
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +7 -1
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -0
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +13 -6
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +5 -7
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/make-client-session.d.ts +1 -1
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +1 -1
- package/dist/make-client-session.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +1 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +10 -2
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +2 -2
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js +2 -2
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/EventSequenceNumber.d.ts +20 -2
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +71 -19
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +88 -3
- package/dist/schema/EventSequenceNumber.test.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts +25 -11
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +12 -4
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
- package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +35 -8
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -81
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +67 -62
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +8 -17
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema-management/migrations.d.ts +3 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +2 -0
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sqlite-types.d.ts +72 -0
- package/dist/sqlite-types.d.ts.map +1 -0
- package/dist/sqlite-types.js +5 -0
- package/dist/sqlite-types.js.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -2
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +16 -13
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/graphology.d.ts.map +1 -1
- package/dist/sync/next/graphology.js +0 -6
- package/dist/sync/next/graphology.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +1 -0
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +1 -1
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +12 -3
- package/dist/sync/next/test/event-fixtures.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -0
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +3 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +13 -4
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +23 -10
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +17 -17
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -6
- package/src/ClientSessionLeaderThreadProxy.ts +40 -0
- package/src/adapter-types.ts +19 -166
- package/src/defs.ts +17 -0
- package/src/errors.ts +49 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +141 -180
- package/src/leader-thread/eventlog.ts +78 -56
- package/src/leader-thread/leader-worker-devtools.ts +1 -2
- package/src/leader-thread/make-leader-thread-layer.ts +52 -8
- package/src/leader-thread/materialize-event.ts +8 -1
- package/src/leader-thread/mod.ts +1 -0
- package/src/leader-thread/recreate-db.ts +99 -91
- package/src/leader-thread/types.ts +6 -11
- package/src/make-client-session.ts +2 -2
- package/src/rematerialize-from-eventlog.ts +10 -2
- package/src/schema/EventDef.ts +5 -3
- package/src/schema/EventSequenceNumber.test.ts +120 -3
- package/src/schema/EventSequenceNumber.ts +95 -23
- package/src/schema/LiveStoreEvent.ts +20 -4
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
- package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
- package/src/schema/state/sqlite/mod.ts +1 -1
- package/src/schema/state/sqlite/query-builder/api.ts +38 -8
- package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
- package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
- package/src/schema/state/sqlite/system-tables.ts +9 -22
- package/src/schema/state/sqlite/table-def.ts +1 -1
- package/src/schema-management/migrations.ts +3 -1
- package/src/sql-queries/sql-queries.ts +2 -0
- package/src/sqlite-types.ts +76 -0
- package/src/sync/ClientSessionSyncProcessor.ts +17 -20
- package/src/sync/next/graphology.ts +0 -6
- package/src/sync/next/rebase-events.ts +1 -0
- package/src/sync/next/test/compact-events.test.ts +1 -1
- package/src/sync/next/test/event-fixtures.ts +12 -3
- package/src/sync/sync.ts +3 -0
- package/src/sync/syncstate.test.ts +17 -17
- package/src/sync/syncstate.ts +31 -10
- package/src/version.ts +1 -1
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
FiberHandle,
|
9
9
|
Option,
|
10
10
|
OtelTracer,
|
11
|
+
pipe,
|
11
12
|
Queue,
|
12
13
|
ReadonlyArray,
|
13
14
|
Stream,
|
@@ -17,7 +18,7 @@ import {
|
|
17
18
|
import type * as otel from '@opentelemetry/api'
|
18
19
|
|
19
20
|
import type { SqliteDb } from '../adapter-types.js'
|
20
|
-
import { UnexpectedError } from '../adapter-types.js'
|
21
|
+
import { SyncError, UnexpectedError } from '../adapter-types.js'
|
21
22
|
import { makeMaterializerHash } from '../materializer-helper.js'
|
22
23
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
23
24
|
import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.js'
|
@@ -32,8 +33,6 @@ import { LeaderThreadCtx } from './types.js'
|
|
32
33
|
type LocalPushQueueItem = [
|
33
34
|
event: LiveStoreEvent.EncodedWithMeta,
|
34
35
|
deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
|
35
|
-
/** Used to determine whether the batch has become invalid due to a rejected local push batch */
|
36
|
-
generation: number,
|
37
36
|
]
|
38
37
|
|
39
38
|
/**
|
@@ -58,32 +57,28 @@ type LocalPushQueueItem = [
|
|
58
57
|
* - The latch closes on pull receipt and re-opens post-pull completion.
|
59
58
|
* - Processes up to `maxBatchSize` events per cycle.
|
60
59
|
*
|
61
|
-
* Currently we're advancing the db
|
60
|
+
* Currently we're advancing the state db and eventlog in lockstep, but we could also decouple this in the future
|
62
61
|
*
|
63
62
|
* Tricky concurrency scenarios:
|
64
63
|
* - Queued local push batches becoming invalid due to a prior local push item being rejected.
|
65
64
|
* Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
|
66
65
|
*
|
66
|
+
* See ClientSessionSyncProcessor for how the leader and session sync processors are similar/different.
|
67
67
|
*/
|
68
68
|
export const makeLeaderSyncProcessor = ({
|
69
69
|
schema,
|
70
|
-
dbEventlogMissing,
|
71
|
-
dbEventlog,
|
72
70
|
dbState,
|
73
|
-
dbStateMissing,
|
74
71
|
initialBlockingSyncContext,
|
72
|
+
initialSyncState,
|
75
73
|
onError,
|
76
74
|
params,
|
77
75
|
testing,
|
78
76
|
}: {
|
79
77
|
schema: LiveStoreSchema
|
80
|
-
/** Only used to know whether we can safely query dbEventlog during setup execution */
|
81
|
-
dbEventlogMissing: boolean
|
82
|
-
dbEventlog: SqliteDb
|
83
78
|
dbState: SqliteDb
|
84
|
-
/** Only used to know whether we can safely query dbState during setup execution */
|
85
|
-
dbStateMissing: boolean
|
86
79
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
80
|
+
/** Initial sync state rehydrated from the persisted eventlog or initial sync state */
|
81
|
+
initialSyncState: SyncState.SyncState
|
87
82
|
onError: 'shutdown' | 'ignore'
|
88
83
|
params: {
|
89
84
|
/**
|
@@ -115,18 +110,6 @@ export const makeLeaderSyncProcessor = ({
|
|
115
110
|
|
116
111
|
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
117
112
|
|
118
|
-
/**
|
119
|
-
* Tracks generations of queued local push events.
|
120
|
-
* If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
|
121
|
-
* even if they would be valid on their own.
|
122
|
-
*/
|
123
|
-
// TODO get rid of this in favour of the `mergeGeneration` event sequence number field
|
124
|
-
const currentLocalPushGenerationRef = { current: 0 }
|
125
|
-
|
126
|
-
type MergeCounter = number
|
127
|
-
const mergeCounterRef = { current: dbStateMissing ? 0 : yield* getMergeCounterFromDb(dbState) }
|
128
|
-
const mergePayloads = new Map<MergeCounter, typeof SyncState.PayloadUpstream.Type>()
|
129
|
-
|
130
113
|
// This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
|
131
114
|
const ctxRef = {
|
132
115
|
current: undefined as
|
@@ -150,7 +133,7 @@ export const makeLeaderSyncProcessor = ({
|
|
150
133
|
* - leader sync processor takes a bit and hasn't yet taken e1 from the localPushesQueue
|
151
134
|
* - client session B also pushes e1 (which should be rejected)
|
152
135
|
*
|
153
|
-
* Thus the
|
136
|
+
* Thus the purpose of the pushHeadRef is the guard the integrity of the local push queue
|
154
137
|
*/
|
155
138
|
const pushHeadRef = { current: EventSequenceNumber.ROOT }
|
156
139
|
const advancePushHead = (eventNum: EventSequenceNumber.EventSequenceNumber) => {
|
@@ -162,25 +145,24 @@ export const makeLeaderSyncProcessor = ({
|
|
162
145
|
Effect.gen(function* () {
|
163
146
|
if (newEvents.length === 0) return
|
164
147
|
|
148
|
+
// console.debug('push', newEvents)
|
149
|
+
|
165
150
|
yield* validatePushBatch(newEvents, pushHeadRef.current)
|
166
151
|
|
167
152
|
advancePushHead(newEvents.at(-1)!.seqNum)
|
168
153
|
|
169
154
|
const waitForProcessing = options?.waitForProcessing ?? false
|
170
|
-
const generation = currentLocalPushGenerationRef.current
|
171
155
|
|
172
156
|
if (waitForProcessing) {
|
173
157
|
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
|
174
158
|
|
175
|
-
const items = newEvents.map(
|
176
|
-
(eventEncoded, i) => [eventEncoded, deferreds[i], generation] as LocalPushQueueItem,
|
177
|
-
)
|
159
|
+
const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i]] as LocalPushQueueItem)
|
178
160
|
|
179
161
|
yield* BucketQueue.offerAll(localPushesQueue, items)
|
180
162
|
|
181
163
|
yield* Effect.all(deferreds)
|
182
164
|
} else {
|
183
|
-
const items = newEvents.map((eventEncoded) => [eventEncoded, undefined
|
165
|
+
const items = newEvents.map((eventEncoded) => [eventEncoded, undefined] as LocalPushQueueItem)
|
184
166
|
yield* BucketQueue.offerAll(localPushesQueue, items)
|
185
167
|
}
|
186
168
|
}).pipe(
|
@@ -205,7 +187,7 @@ export const makeLeaderSyncProcessor = ({
|
|
205
187
|
args,
|
206
188
|
clientId,
|
207
189
|
sessionId,
|
208
|
-
...EventSequenceNumber.nextPair(syncState.localHead, eventDef.options.clientOnly),
|
190
|
+
...EventSequenceNumber.nextPair({ seqNum: syncState.localHead, isClient: eventDef.options.clientOnly }),
|
209
191
|
})
|
210
192
|
|
211
193
|
yield* push([eventEncoded])
|
@@ -225,34 +207,12 @@ export const makeLeaderSyncProcessor = ({
|
|
225
207
|
runtime,
|
226
208
|
}
|
227
209
|
|
228
|
-
const initialLocalHead = dbEventlogMissing ? EventSequenceNumber.ROOT : Eventlog.getClientHeadFromDb(dbEventlog)
|
229
|
-
|
230
|
-
const initialBackendHead = dbEventlogMissing
|
231
|
-
? EventSequenceNumber.ROOT.global
|
232
|
-
: Eventlog.getBackendHeadFromDb(dbEventlog)
|
233
|
-
|
234
|
-
if (initialBackendHead > initialLocalHead.global) {
|
235
|
-
return shouldNeverHappen(
|
236
|
-
`During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
|
237
|
-
)
|
238
|
-
}
|
239
|
-
|
240
|
-
const pendingEvents = dbEventlogMissing
|
241
|
-
? []
|
242
|
-
: yield* Eventlog.getEventsSince({ global: initialBackendHead, client: EventSequenceNumber.clientDefault })
|
243
|
-
|
244
|
-
const initialSyncState = new SyncState.SyncState({
|
245
|
-
pending: pendingEvents,
|
246
|
-
upstreamHead: { global: initialBackendHead, client: EventSequenceNumber.clientDefault },
|
247
|
-
localHead: initialLocalHead,
|
248
|
-
})
|
249
|
-
|
250
210
|
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
251
211
|
yield* SubscriptionRef.set(syncStateSref, initialSyncState)
|
252
212
|
|
253
213
|
// Rehydrate sync queue
|
254
|
-
if (
|
255
|
-
const globalPendingEvents =
|
214
|
+
if (initialSyncState.pending.length > 0) {
|
215
|
+
const globalPendingEvents = initialSyncState.pending
|
256
216
|
// Don't sync clientOnly events
|
257
217
|
.filter((eventEncoded) => {
|
258
218
|
const { eventDef } = getEventDef(schema, eventEncoded.name)
|
@@ -281,10 +241,7 @@ export const makeLeaderSyncProcessor = ({
|
|
281
241
|
schema,
|
282
242
|
isClientEvent,
|
283
243
|
otelSpan,
|
284
|
-
currentLocalPushGenerationRef,
|
285
244
|
connectedClientSessionPullQueues,
|
286
|
-
mergeCounterRef,
|
287
|
-
mergePayloads,
|
288
245
|
localPushBatchSize,
|
289
246
|
testing: {
|
290
247
|
delay: testing?.delays?.localPushProcessing,
|
@@ -302,7 +259,7 @@ export const makeLeaderSyncProcessor = ({
|
|
302
259
|
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
303
260
|
|
304
261
|
yield* backgroundBackendPulling({
|
305
|
-
initialBackendHead,
|
262
|
+
initialBackendHead: initialSyncState.upstreamHead.global,
|
306
263
|
isClientEvent,
|
307
264
|
restartBackendPushing: (filteredRebasedPending) =>
|
308
265
|
Effect.gen(function* () {
|
@@ -324,12 +281,10 @@ export const makeLeaderSyncProcessor = ({
|
|
324
281
|
initialBlockingSyncContext,
|
325
282
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
326
283
|
connectedClientSessionPullQueues,
|
327
|
-
mergeCounterRef,
|
328
|
-
mergePayloads,
|
329
284
|
advancePushHead,
|
330
285
|
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
331
286
|
|
332
|
-
return { initialLeaderHead:
|
287
|
+
return { initialLeaderHead: initialSyncState.localHead }
|
333
288
|
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
334
289
|
|
335
290
|
const pull: LeaderSyncProcessor['pull'] = ({ cursor }) =>
|
@@ -338,34 +293,23 @@ export const makeLeaderSyncProcessor = ({
|
|
338
293
|
return Stream.fromQueue(queue)
|
339
294
|
}).pipe(Stream.unwrapScoped)
|
340
295
|
|
341
|
-
|
342
|
-
|
343
|
-
return Effect.gen(function* () {
|
344
|
-
const queue = yield* connectedClientSessionPullQueues.makeQueue
|
345
|
-
const payloadsSinceCursor = Array.from(mergePayloads.entries())
|
346
|
-
.map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
|
347
|
-
.filter(({ mergeCounter }) => mergeCounter > cursor.mergeCounter)
|
348
|
-
.toSorted((a, b) => a.mergeCounter - b.mergeCounter)
|
349
|
-
.map(({ payload, mergeCounter }) => {
|
350
|
-
if (payload._tag === 'upstream-advance') {
|
351
|
-
return {
|
352
|
-
payload: {
|
353
|
-
_tag: 'upstream-advance' as const,
|
354
|
-
newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
|
355
|
-
EventSequenceNumber.isGreaterThanOrEqual(cursor.eventNum, eventEncoded.seqNum),
|
356
|
-
),
|
357
|
-
},
|
358
|
-
mergeCounter,
|
359
|
-
}
|
360
|
-
} else {
|
361
|
-
return { payload, mergeCounter }
|
362
|
-
}
|
363
|
-
})
|
296
|
+
/*
|
297
|
+
Notes for a potential new `LeaderSyncProcessor.pull` implementation:
|
364
298
|
|
365
|
-
|
299
|
+
- Doesn't take cursor but is "atomically called" in the leader during the snapshot phase
|
300
|
+
- TODO: how is this done "atomically" in the web adapter where the snapshot is read optimistically?
|
301
|
+
- Would require a new kind of "boot-phase" API which is stream based:
|
302
|
+
- initial message: state snapshot + seq num head
|
303
|
+
- subsequent messages: sync state payloads
|
366
304
|
|
367
|
-
|
368
|
-
|
305
|
+
- alternative: instead of session pulling sync state payloads from leader, we could send
|
306
|
+
- events in the "advance" case
|
307
|
+
- full new state db snapshot in the "rebase" case
|
308
|
+
- downside: importing the snapshot is expensive
|
309
|
+
*/
|
310
|
+
const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
|
311
|
+
const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
|
312
|
+
return connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(runtime))
|
369
313
|
}
|
370
314
|
|
371
315
|
const syncState = Subscribable.make({
|
@@ -384,7 +328,6 @@ export const makeLeaderSyncProcessor = ({
|
|
384
328
|
pushPartial,
|
385
329
|
boot,
|
386
330
|
syncState,
|
387
|
-
getMergeCounter: () => mergeCounterRef.current,
|
388
331
|
} satisfies LeaderSyncProcessor
|
389
332
|
})
|
390
333
|
|
@@ -397,10 +340,7 @@ const backgroundApplyLocalPushes = ({
|
|
397
340
|
schema,
|
398
341
|
isClientEvent,
|
399
342
|
otelSpan,
|
400
|
-
currentLocalPushGenerationRef,
|
401
343
|
connectedClientSessionPullQueues,
|
402
|
-
mergeCounterRef,
|
403
|
-
mergePayloads,
|
404
344
|
localPushBatchSize,
|
405
345
|
testing,
|
406
346
|
}: {
|
@@ -412,10 +352,7 @@ const backgroundApplyLocalPushes = ({
|
|
412
352
|
schema: LiveStoreSchema
|
413
353
|
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
414
354
|
otelSpan: otel.Span | undefined
|
415
|
-
currentLocalPushGenerationRef: { current: number }
|
416
355
|
connectedClientSessionPullQueues: PullQueueSet
|
417
|
-
mergeCounterRef: { current: number }
|
418
|
-
mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
|
419
356
|
localPushBatchSize: number
|
420
357
|
testing: {
|
421
358
|
delay: Effect.Effect<void> | undefined
|
@@ -435,24 +372,26 @@ const backgroundApplyLocalPushes = ({
|
|
435
372
|
// Prevent backend pull processing until this local push is finished
|
436
373
|
yield* pullLatch.close
|
437
374
|
|
438
|
-
|
375
|
+
const syncState = yield* syncStateSref
|
376
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
377
|
+
|
378
|
+
const currentRebaseGeneration = syncState.localHead.rebaseGeneration
|
379
|
+
|
380
|
+
// Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
|
439
381
|
// It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
|
440
|
-
const
|
441
|
-
|
442
|
-
.
|
382
|
+
const [newEvents, deferreds] = pipe(
|
383
|
+
batchItems,
|
384
|
+
ReadonlyArray.filter(([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration === currentRebaseGeneration),
|
385
|
+
ReadonlyArray.unzip,
|
386
|
+
)
|
443
387
|
|
444
|
-
if (
|
388
|
+
if (newEvents.length === 0) {
|
445
389
|
// console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
|
446
390
|
// Allow the backend pulling to start
|
447
391
|
yield* pullLatch.open
|
448
392
|
continue
|
449
393
|
}
|
450
394
|
|
451
|
-
const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems)
|
452
|
-
|
453
|
-
const syncState = yield* syncStateSref
|
454
|
-
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
455
|
-
|
456
395
|
const mergeResult = SyncState.merge({
|
457
396
|
syncState,
|
458
397
|
payload: { _tag: 'local-push', newEvents },
|
@@ -460,29 +399,25 @@ const backgroundApplyLocalPushes = ({
|
|
460
399
|
isEqualEvent: LiveStoreEvent.isEqualEncoded,
|
461
400
|
})
|
462
401
|
|
463
|
-
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
|
464
|
-
|
465
402
|
switch (mergeResult._tag) {
|
466
403
|
case 'unexpected-error': {
|
467
|
-
otelSpan?.addEvent(`
|
404
|
+
otelSpan?.addEvent(`push:unexpected-error`, {
|
468
405
|
batchSize: newEvents.length,
|
469
406
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
470
407
|
})
|
471
|
-
return yield*
|
408
|
+
return yield* new SyncError({ cause: mergeResult.message })
|
472
409
|
}
|
473
410
|
case 'rebase': {
|
474
411
|
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
475
412
|
}
|
476
413
|
case 'reject': {
|
477
|
-
otelSpan?.addEvent(`
|
414
|
+
otelSpan?.addEvent(`push:reject`, {
|
478
415
|
batchSize: newEvents.length,
|
479
416
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
480
417
|
})
|
481
418
|
|
482
419
|
// TODO: how to test this?
|
483
|
-
|
484
|
-
|
485
|
-
const nextGeneration = currentLocalPushGenerationRef.current
|
420
|
+
const nextRebaseGeneration = currentRebaseGeneration + 1
|
486
421
|
|
487
422
|
const providedNum = newEvents.at(0)!.seqNum
|
488
423
|
// All subsequent pushes with same generation should be rejected as well
|
@@ -490,13 +425,13 @@ const backgroundApplyLocalPushes = ({
|
|
490
425
|
// from the next generation which we preserve in the queue
|
491
426
|
const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
|
492
427
|
localPushesQueue,
|
493
|
-
(
|
428
|
+
([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= nextRebaseGeneration,
|
494
429
|
)
|
495
430
|
|
496
431
|
// TODO we still need to better understand and handle this scenario
|
497
432
|
if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
|
498
433
|
console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
|
499
|
-
// biome-ignore lint/suspicious/noDebugger:
|
434
|
+
// biome-ignore lint/suspicious/noDebugger: debugging
|
500
435
|
debugger
|
501
436
|
}
|
502
437
|
|
@@ -508,11 +443,7 @@ const backgroundApplyLocalPushes = ({
|
|
508
443
|
yield* Effect.forEach(allDeferredsToReject, (deferred) =>
|
509
444
|
Deferred.fail(
|
510
445
|
deferred,
|
511
|
-
LeaderAheadError.make({
|
512
|
-
minimumExpectedNum: mergeResult.expectedMinimumId,
|
513
|
-
providedNum,
|
514
|
-
// nextGeneration,
|
515
|
-
}),
|
446
|
+
LeaderAheadError.make({ minimumExpectedNum: mergeResult.expectedMinimumId, providedNum }),
|
516
447
|
),
|
517
448
|
)
|
518
449
|
|
@@ -535,11 +466,10 @@ const backgroundApplyLocalPushes = ({
|
|
535
466
|
|
536
467
|
yield* connectedClientSessionPullQueues.offer({
|
537
468
|
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
|
538
|
-
|
469
|
+
leaderHead: mergeResult.newSyncState.localHead,
|
539
470
|
})
|
540
|
-
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
|
541
471
|
|
542
|
-
otelSpan?.addEvent(`
|
472
|
+
otelSpan?.addEvent(`push:advance`, {
|
543
473
|
batchSize: newEvents.length,
|
544
474
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
545
475
|
})
|
@@ -621,8 +551,6 @@ const backgroundBackendPulling = ({
|
|
621
551
|
devtoolsLatch,
|
622
552
|
initialBlockingSyncContext,
|
623
553
|
connectedClientSessionPullQueues,
|
624
|
-
mergeCounterRef,
|
625
|
-
mergePayloads,
|
626
554
|
advancePushHead,
|
627
555
|
}: {
|
628
556
|
initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
|
@@ -638,8 +566,6 @@ const backgroundBackendPulling = ({
|
|
638
566
|
devtoolsLatch: Effect.Latch | undefined
|
639
567
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
640
568
|
connectedClientSessionPullQueues: PullQueueSet
|
641
|
-
mergeCounterRef: { current: number }
|
642
|
-
mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
|
643
569
|
advancePushHead: (eventNum: EventSequenceNumber.EventSequenceNumber) => void
|
644
570
|
}) =>
|
645
571
|
Effect.gen(function* () {
|
@@ -672,16 +598,14 @@ const backgroundBackendPulling = ({
|
|
672
598
|
ignoreClientEvents: true,
|
673
599
|
})
|
674
600
|
|
675
|
-
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
|
676
|
-
|
677
601
|
if (mergeResult._tag === 'reject') {
|
678
602
|
return shouldNeverHappen('The leader thread should never reject upstream advances')
|
679
603
|
} else if (mergeResult._tag === 'unexpected-error') {
|
680
|
-
otelSpan?.addEvent(`
|
604
|
+
otelSpan?.addEvent(`pull:unexpected-error`, {
|
681
605
|
newEventsCount: newEvents.length,
|
682
606
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
683
607
|
})
|
684
|
-
return yield*
|
608
|
+
return yield* new SyncError({ cause: mergeResult.message })
|
685
609
|
}
|
686
610
|
|
687
611
|
const newBackendHead = newEvents.at(-1)!.seqNum
|
@@ -689,7 +613,7 @@ const backgroundBackendPulling = ({
|
|
689
613
|
Eventlog.updateBackendHead(dbEventlog, newBackendHead)
|
690
614
|
|
691
615
|
if (mergeResult._tag === 'rebase') {
|
692
|
-
otelSpan?.addEvent(`[${
|
616
|
+
otelSpan?.addEvent(`pull:rebase[${mergeResult.newSyncState.localHead.rebaseGeneration}]`, {
|
693
617
|
newEventsCount: newEvents.length,
|
694
618
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
695
619
|
rollbackCount: mergeResult.rollbackEvents.length,
|
@@ -711,30 +635,19 @@ const backgroundBackendPulling = ({
|
|
711
635
|
}
|
712
636
|
|
713
637
|
yield* connectedClientSessionPullQueues.offer({
|
714
|
-
payload: SyncState.
|
715
|
-
|
716
|
-
rollbackEvents: mergeResult.rollbackEvents,
|
717
|
-
}),
|
718
|
-
mergeCounter,
|
638
|
+
payload: SyncState.payloadFromMergeResult(mergeResult),
|
639
|
+
leaderHead: mergeResult.newSyncState.localHead,
|
719
640
|
})
|
720
|
-
mergePayloads.set(
|
721
|
-
mergeCounter,
|
722
|
-
SyncState.PayloadUpstreamRebase.make({
|
723
|
-
newEvents: mergeResult.newEvents,
|
724
|
-
rollbackEvents: mergeResult.rollbackEvents,
|
725
|
-
}),
|
726
|
-
)
|
727
641
|
} else {
|
728
|
-
otelSpan?.addEvent(`
|
642
|
+
otelSpan?.addEvent(`pull:advance`, {
|
729
643
|
newEventsCount: newEvents.length,
|
730
644
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
731
645
|
})
|
732
646
|
|
733
647
|
yield* connectedClientSessionPullQueues.offer({
|
734
|
-
payload: SyncState.
|
735
|
-
|
648
|
+
payload: SyncState.payloadFromMergeResult(mergeResult),
|
649
|
+
leaderHead: mergeResult.newSyncState.localHead,
|
736
650
|
})
|
737
|
-
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
|
738
651
|
|
739
652
|
if (mergeResult.confirmedEvents.length > 0) {
|
740
653
|
// `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
|
@@ -763,7 +676,7 @@ const backgroundBackendPulling = ({
|
|
763
676
|
}
|
764
677
|
})
|
765
678
|
|
766
|
-
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo(initialBackendHead)
|
679
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: initialBackendHead })
|
767
680
|
|
768
681
|
const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
|
769
682
|
|
@@ -854,19 +767,25 @@ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.EventSeque
|
|
854
767
|
}
|
855
768
|
|
856
769
|
interface PullQueueSet {
|
857
|
-
makeQueue:
|
858
|
-
|
770
|
+
makeQueue: (
|
771
|
+
cursor: EventSequenceNumber.EventSequenceNumber,
|
772
|
+
) => Effect.Effect<
|
773
|
+
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>,
|
859
774
|
UnexpectedError,
|
860
775
|
Scope.Scope | LeaderThreadCtx
|
861
776
|
>
|
862
777
|
offer: (item: {
|
863
778
|
payload: typeof SyncState.PayloadUpstream.Type
|
864
|
-
|
779
|
+
leaderHead: EventSequenceNumber.EventSequenceNumber
|
865
780
|
}) => Effect.Effect<void, UnexpectedError>
|
866
781
|
}
|
867
782
|
|
868
783
|
const makePullQueueSet = Effect.gen(function* () {
|
869
|
-
const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type
|
784
|
+
const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>>()
|
785
|
+
|
786
|
+
type StringifiedSeqNum = string
|
787
|
+
// NOTE this could grow unbounded for long running sessions
|
788
|
+
const cachedPayloads = new Map<StringifiedSeqNum, (typeof SyncState.PayloadUpstream.Type)[]>()
|
870
789
|
|
871
790
|
yield* Effect.addFinalizer(() =>
|
872
791
|
Effect.gen(function* () {
|
@@ -878,21 +797,81 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
878
797
|
}),
|
879
798
|
)
|
880
799
|
|
881
|
-
const makeQueue: PullQueueSet['makeQueue'] =
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
800
|
+
const makeQueue: PullQueueSet['makeQueue'] = (cursor) =>
|
801
|
+
Effect.gen(function* () {
|
802
|
+
const queue = yield* Queue.unbounded<{
|
803
|
+
payload: typeof SyncState.PayloadUpstream.Type
|
804
|
+
}>().pipe(Effect.acquireRelease(Queue.shutdown))
|
886
805
|
|
887
|
-
|
806
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
|
888
807
|
|
889
|
-
|
808
|
+
const payloadsSinceCursor = Array.from(cachedPayloads.entries())
|
809
|
+
.flatMap(([seqNumStr, payloads]) =>
|
810
|
+
payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.fromString(seqNumStr) })),
|
811
|
+
)
|
812
|
+
.filter(({ seqNum }) => EventSequenceNumber.isGreaterThan(seqNum, cursor))
|
813
|
+
.toSorted((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
|
814
|
+
.map(({ payload }) => {
|
815
|
+
if (payload._tag === 'upstream-advance') {
|
816
|
+
return {
|
817
|
+
payload: {
|
818
|
+
_tag: 'upstream-advance' as const,
|
819
|
+
newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
|
820
|
+
EventSequenceNumber.isGreaterThanOrEqual(cursor, eventEncoded.seqNum),
|
821
|
+
),
|
822
|
+
},
|
823
|
+
}
|
824
|
+
} else {
|
825
|
+
return { payload }
|
826
|
+
}
|
827
|
+
})
|
890
828
|
|
891
|
-
|
892
|
-
|
829
|
+
// console.debug(
|
830
|
+
// 'seeding new queue',
|
831
|
+
// {
|
832
|
+
// cursor,
|
833
|
+
// },
|
834
|
+
// '\n mergePayloads',
|
835
|
+
// ...Array.from(cachedPayloads.entries())
|
836
|
+
// .flatMap(([seqNumStr, payloads]) =>
|
837
|
+
// payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.fromString(seqNumStr) })),
|
838
|
+
// )
|
839
|
+
// .map(({ payload, seqNum }) => [
|
840
|
+
// seqNum,
|
841
|
+
// payload._tag,
|
842
|
+
// 'newEvents',
|
843
|
+
// ...payload.newEvents.map((_) => _.toJSON()),
|
844
|
+
// 'rollbackEvents',
|
845
|
+
// ...(payload._tag === 'upstream-rebase' ? payload.rollbackEvents.map((_) => _.toJSON()) : []),
|
846
|
+
// ]),
|
847
|
+
// '\n payloadsSinceCursor',
|
848
|
+
// ...payloadsSinceCursor.map(({ payload }) => [
|
849
|
+
// payload._tag,
|
850
|
+
// 'newEvents',
|
851
|
+
// ...payload.newEvents.map((_) => _.toJSON()),
|
852
|
+
// 'rollbackEvents',
|
853
|
+
// ...(payload._tag === 'upstream-rebase' ? payload.rollbackEvents.map((_) => _.toJSON()) : []),
|
854
|
+
// ]),
|
855
|
+
// )
|
856
|
+
|
857
|
+
yield* queue.offerAll(payloadsSinceCursor)
|
858
|
+
|
859
|
+
set.add(queue)
|
860
|
+
|
861
|
+
return queue
|
862
|
+
})
|
893
863
|
|
894
864
|
const offer: PullQueueSet['offer'] = (item) =>
|
895
865
|
Effect.gen(function* () {
|
866
|
+
const seqNumStr = EventSequenceNumber.toString(item.leaderHead)
|
867
|
+
if (cachedPayloads.has(seqNumStr)) {
|
868
|
+
cachedPayloads.get(seqNumStr)!.push(item.payload)
|
869
|
+
} else {
|
870
|
+
cachedPayloads.set(seqNumStr, [item.payload])
|
871
|
+
}
|
872
|
+
|
873
|
+
// console.debug(`offering to ${set.size} queues`, item.leaderHead, JSON.stringify(item.payload, null, 2))
|
874
|
+
|
896
875
|
// Short-circuit if the payload is an empty upstream advance
|
897
876
|
if (item.payload._tag === 'upstream-advance' && item.payload.newEvents.length === 0) {
|
898
877
|
return
|
@@ -909,24 +888,6 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
909
888
|
}
|
910
889
|
})
|
911
890
|
|
912
|
-
const incrementMergeCounter = (mergeCounterRef: { current: number }) =>
|
913
|
-
Effect.gen(function* () {
|
914
|
-
const { dbState } = yield* LeaderThreadCtx
|
915
|
-
mergeCounterRef.current++
|
916
|
-
dbState.execute(
|
917
|
-
sql`INSERT OR REPLACE INTO ${SystemTables.LEADER_MERGE_COUNTER_TABLE} (id, mergeCounter) VALUES (0, ${mergeCounterRef.current})`,
|
918
|
-
)
|
919
|
-
return mergeCounterRef.current
|
920
|
-
})
|
921
|
-
|
922
|
-
const getMergeCounterFromDb = (dbState: SqliteDb) =>
|
923
|
-
Effect.gen(function* () {
|
924
|
-
const result = dbState.select<{ mergeCounter: number }>(
|
925
|
-
sql`SELECT mergeCounter FROM ${SystemTables.LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
|
926
|
-
)
|
927
|
-
return result[0]?.mergeCounter ?? 0
|
928
|
-
})
|
929
|
-
|
930
891
|
const validatePushBatch = (
|
931
892
|
batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
932
893
|
pushHead: EventSequenceNumber.EventSequenceNumber,
|