@livestore/common 0.3.0-dev.16 → 0.3.0-dev.18
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/adapter-types.d.ts +12 -4
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +4 -0
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts +1 -1
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/derived-mutations.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- 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 +28 -28
- package/dist/devtools/index.d.ts.map +1 -1
- package/dist/init-singleton-tables.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +3 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +130 -50
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
- package/dist/leader-thread/apply-mutation.js +11 -5
- package/dist/leader-thread/apply-mutation.js.map +1 -1
- package/dist/leader-thread/connection.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.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 +1 -0
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/mutationlog.d.ts.map +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts +3 -3
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
- package/dist/leader-thread/pull-queue-set.js +9 -0
- package/dist/leader-thread/pull-queue-set.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -5
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -4
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +7 -2
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/mutation.d.ts.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/query-builder/api.d.ts.map +1 -1
- package/dist/query-builder/impl.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +3 -3
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/EventId.d.ts +8 -0
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +14 -0
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +3 -3
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/db-schema/ast/validate.d.ts.map +1 -1
- package/dist/schema/db-schema/dsl/field-defs.d.ts.map +1 -1
- package/dist/schema/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/db-schema/hash.d.ts.map +1 -1
- package/dist/schema/mutations.d.ts +5 -2
- package/dist/schema/mutations.d.ts.map +1 -1
- package/dist/schema/mutations.js.map +1 -1
- package/dist/schema/schema-helpers.d.ts.map +1 -1
- package/dist/schema/schema.d.ts +4 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +19 -8
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/table-def.d.ts +1 -8
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
- package/dist/schema-management/validate-mutation-defs.js +2 -2
- package/dist/schema-management/validate-mutation-defs.js.map +1 -1
- package/dist/sql-queries/misc.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/types.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +11 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +51 -19
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts +7 -7
- package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
- package/dist/sync/sync.d.ts +14 -9
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +7 -3
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +132 -21
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +129 -41
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +19 -7
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/adapter-types.ts +9 -4
- package/src/leader-thread/LeaderSyncProcessor.ts +169 -61
- package/src/leader-thread/apply-mutation.ts +21 -5
- package/src/leader-thread/make-leader-thread-layer.ts +1 -0
- package/src/leader-thread/pull-queue-set.ts +10 -1
- package/src/leader-thread/shutdown-channel.ts +2 -4
- package/src/leader-thread/types.ts +8 -2
- package/src/rehydrate-from-mutationlog.ts +2 -2
- package/src/schema/EventId.ts +16 -0
- package/src/schema/MutationEvent.ts +3 -3
- package/src/schema/db-schema/dsl/field-defs.ts +1 -2
- package/src/schema/db-schema/dsl/mod.ts +1 -1
- package/src/schema/mutations.ts +4 -1
- package/src/schema/schema.ts +20 -8
- package/src/schema-management/validate-mutation-defs.ts +2 -2
- package/src/sync/ClientSessionSyncProcessor.ts +82 -19
- package/src/sync/sync.ts +7 -4
- package/src/sync/syncstate.test.ts +32 -14
- package/src/sync/syncstate.ts +145 -60
- package/src/version.ts +1 -1
- package/tmp/pack.tgz +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
import { isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
1
|
+
import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
2
2
|
import type { HttpClient, Scope, Tracer } from '@livestore/utils/effect'
|
3
3
|
import {
|
4
4
|
BucketQueue,
|
@@ -21,13 +21,14 @@ import { UnexpectedError } from '../adapter-types.js'
|
|
21
21
|
import type { LiveStoreSchema, SessionChangesetMetaRow } from '../schema/mod.js'
|
22
22
|
import {
|
23
23
|
EventId,
|
24
|
+
getMutationDef,
|
24
25
|
MUTATION_LOG_META_TABLE,
|
25
26
|
MutationEvent,
|
26
27
|
mutationLogMetaTable,
|
27
28
|
SESSION_CHANGESET_META_TABLE,
|
28
29
|
} from '../schema/mod.js'
|
29
30
|
import { updateRows } from '../sql-queries/index.js'
|
30
|
-
import {
|
31
|
+
import { LeaderAheadError } from '../sync/sync.js'
|
31
32
|
import * as SyncState from '../sync/syncstate.js'
|
32
33
|
import { sql } from '../util.js'
|
33
34
|
import { makeApplyMutation } from './apply-mutation.js'
|
@@ -36,9 +37,12 @@ import { getBackendHeadFromDb, getClientHeadFromDb, getMutationEventsSince, upda
|
|
36
37
|
import type { InitialBlockingSyncContext, InitialSyncInfo, LeaderSyncProcessor } from './types.js'
|
37
38
|
import { LeaderThreadCtx } from './types.js'
|
38
39
|
|
39
|
-
|
40
|
+
export const BACKEND_PUSH_BATCH_SIZE = 50
|
41
|
+
|
42
|
+
type LocalPushQueueItem = [
|
40
43
|
mutationEvent: MutationEvent.EncodedWithMeta,
|
41
|
-
deferred: Deferred.Deferred<void,
|
44
|
+
deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
|
45
|
+
generation: number,
|
42
46
|
]
|
43
47
|
|
44
48
|
/**
|
@@ -68,12 +72,14 @@ export const makeLeaderSyncProcessor = ({
|
|
68
72
|
schema,
|
69
73
|
dbMissing,
|
70
74
|
dbMutationLog,
|
75
|
+
clientId,
|
71
76
|
initialBlockingSyncContext,
|
72
77
|
}: {
|
73
78
|
schema: LiveStoreSchema
|
74
79
|
/** Only used to know whether we can safely query dbMutationLog during setup execution */
|
75
80
|
dbMissing: boolean
|
76
81
|
dbMutationLog: SqliteDb
|
82
|
+
clientId: string
|
77
83
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
78
84
|
}): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
|
79
85
|
Effect.gen(function* () {
|
@@ -82,10 +88,17 @@ export const makeLeaderSyncProcessor = ({
|
|
82
88
|
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
83
89
|
|
84
90
|
const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
|
85
|
-
const mutationDef = schema
|
91
|
+
const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
|
86
92
|
return mutationDef.options.clientOnly
|
87
93
|
}
|
88
94
|
|
95
|
+
/**
|
96
|
+
* Tracks generations of queued local push events.
|
97
|
+
* If a batch is rejected, all subsequent push queue items with the same generation are also rejected,
|
98
|
+
* even if they would be valid on their own.
|
99
|
+
*/
|
100
|
+
const currentLocalPushGenerationRef = { current: 0 }
|
101
|
+
|
89
102
|
// This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
|
90
103
|
const ctxRef = {
|
91
104
|
current: undefined as
|
@@ -97,7 +110,7 @@ export const makeLeaderSyncProcessor = ({
|
|
97
110
|
},
|
98
111
|
}
|
99
112
|
|
100
|
-
const localPushesQueue = yield* BucketQueue.make<
|
113
|
+
const localPushesQueue = yield* BucketQueue.make<LocalPushQueueItem>()
|
101
114
|
const localPushesLatch = yield* Effect.makeLatch(true)
|
102
115
|
const pullLatch = yield* Effect.makeLatch(true)
|
103
116
|
|
@@ -106,20 +119,36 @@ export const makeLeaderSyncProcessor = ({
|
|
106
119
|
// TODO validate batch
|
107
120
|
if (newEvents.length === 0) return
|
108
121
|
|
122
|
+
// if (options.generation < currentLocalPushGenerationRef.current) {
|
123
|
+
// debugger
|
124
|
+
// // We can safely drop this batch as it's from a previous push generation
|
125
|
+
// return
|
126
|
+
// }
|
127
|
+
|
128
|
+
if (clientId === 'client-b') {
|
129
|
+
// console.log(
|
130
|
+
// 'push from client session',
|
131
|
+
// newEvents.map((item) => item.toJSON()),
|
132
|
+
// )
|
133
|
+
}
|
134
|
+
|
109
135
|
const waitForProcessing = options?.waitForProcessing ?? false
|
136
|
+
const generation = currentLocalPushGenerationRef.current
|
110
137
|
|
111
138
|
if (waitForProcessing) {
|
112
|
-
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void,
|
139
|
+
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
|
113
140
|
|
114
141
|
const items = newEvents.map(
|
115
|
-
(mutationEventEncoded, i) => [mutationEventEncoded, deferreds[i]] as
|
142
|
+
(mutationEventEncoded, i) => [mutationEventEncoded, deferreds[i], generation] as LocalPushQueueItem,
|
116
143
|
)
|
117
144
|
|
118
145
|
yield* BucketQueue.offerAll(localPushesQueue, items)
|
119
146
|
|
120
147
|
yield* Effect.all(deferreds)
|
121
148
|
} else {
|
122
|
-
const items = newEvents.map(
|
149
|
+
const items = newEvents.map(
|
150
|
+
(mutationEventEncoded) => [mutationEventEncoded, undefined, generation] as LocalPushQueueItem,
|
151
|
+
)
|
123
152
|
yield* BucketQueue.offerAll(localPushesQueue, items)
|
124
153
|
}
|
125
154
|
}).pipe(
|
@@ -141,9 +170,7 @@ export const makeLeaderSyncProcessor = ({
|
|
141
170
|
const syncState = yield* syncStateSref
|
142
171
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
143
172
|
|
144
|
-
const mutationDef =
|
145
|
-
schema.mutations.get(partialMutationEvent.mutation) ??
|
146
|
-
shouldNeverHappen(`Unknown mutation: ${partialMutationEvent.mutation}`)
|
173
|
+
const mutationDef = getMutationDef(schema, partialMutationEvent.mutation)
|
147
174
|
|
148
175
|
const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
|
149
176
|
...partialMutationEvent,
|
@@ -153,14 +180,14 @@ export const makeLeaderSyncProcessor = ({
|
|
153
180
|
})
|
154
181
|
|
155
182
|
yield* push([mutationEventEncoded])
|
156
|
-
}).pipe(Effect.catchTag('
|
183
|
+
}).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie))
|
157
184
|
|
158
185
|
// Starts various background loops
|
159
186
|
const boot: LeaderSyncProcessor['boot'] = ({ dbReady }) =>
|
160
187
|
Effect.gen(function* () {
|
161
188
|
const span = yield* Effect.currentSpan.pipe(Effect.orDie)
|
162
189
|
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
163
|
-
const { devtools } = yield* LeaderThreadCtx
|
190
|
+
const { devtools, shutdownChannel } = yield* LeaderThreadCtx
|
164
191
|
|
165
192
|
ctxRef.current = {
|
166
193
|
otelSpan,
|
@@ -198,13 +225,19 @@ export const makeLeaderSyncProcessor = ({
|
|
198
225
|
const filteredBatch = pendingMutationEvents
|
199
226
|
// Don't sync clientOnly mutations
|
200
227
|
.filter((mutationEventEncoded) => {
|
201
|
-
const mutationDef = schema
|
228
|
+
const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
|
202
229
|
return mutationDef.options.clientOnly === false
|
203
230
|
})
|
204
231
|
|
205
232
|
yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
|
206
233
|
}
|
207
234
|
|
235
|
+
const shutdownOnError = (cause: unknown) =>
|
236
|
+
Effect.gen(function* () {
|
237
|
+
yield* shutdownChannel.send(UnexpectedError.make({ cause }))
|
238
|
+
yield* Effect.die(cause)
|
239
|
+
})
|
240
|
+
|
208
241
|
yield* backgroundApplyLocalPushes({
|
209
242
|
localPushesLatch,
|
210
243
|
localPushesQueue,
|
@@ -214,7 +247,8 @@ export const makeLeaderSyncProcessor = ({
|
|
214
247
|
schema,
|
215
248
|
isLocalEvent,
|
216
249
|
otelSpan,
|
217
|
-
|
250
|
+
currentLocalPushGenerationRef,
|
251
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
218
252
|
|
219
253
|
const backendPushingFiberHandle = yield* FiberHandle.make()
|
220
254
|
|
@@ -225,7 +259,7 @@ export const makeLeaderSyncProcessor = ({
|
|
225
259
|
syncBackendQueue,
|
226
260
|
otelSpan,
|
227
261
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
228
|
-
}).pipe(Effect.tapCauseLogPretty),
|
262
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
|
229
263
|
)
|
230
264
|
|
231
265
|
yield* backgroundBackendPulling({
|
@@ -249,7 +283,7 @@ export const makeLeaderSyncProcessor = ({
|
|
249
283
|
syncBackendQueue,
|
250
284
|
otelSpan,
|
251
285
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
252
|
-
}).pipe(Effect.tapCauseLogPretty),
|
286
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
|
253
287
|
)
|
254
288
|
}),
|
255
289
|
syncStateSref,
|
@@ -258,7 +292,7 @@ export const makeLeaderSyncProcessor = ({
|
|
258
292
|
otelSpan,
|
259
293
|
initialBlockingSyncContext,
|
260
294
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
261
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
295
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
262
296
|
|
263
297
|
return { initialLeaderHead: initialLocalHead }
|
264
298
|
}).pipe(Effect.withSpanScoped('@livestore/common:leader-thread:syncing'))
|
@@ -287,32 +321,48 @@ const backgroundApplyLocalPushes = ({
|
|
287
321
|
schema,
|
288
322
|
isLocalEvent,
|
289
323
|
otelSpan,
|
324
|
+
currentLocalPushGenerationRef,
|
290
325
|
}: {
|
291
326
|
pullLatch: Effect.Latch
|
292
327
|
localPushesLatch: Effect.Latch
|
293
|
-
localPushesQueue: BucketQueue.BucketQueue<
|
328
|
+
localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
|
294
329
|
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
295
330
|
syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
|
296
331
|
schema: LiveStoreSchema
|
297
332
|
isLocalEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
|
298
333
|
otelSpan: otel.Span | undefined
|
334
|
+
currentLocalPushGenerationRef: { current: number }
|
299
335
|
}) =>
|
300
336
|
Effect.gen(function* () {
|
301
|
-
const { connectedClientSessionPullQueues } = yield* LeaderThreadCtx
|
337
|
+
const { connectedClientSessionPullQueues, clientId } = yield* LeaderThreadCtx
|
302
338
|
|
303
339
|
const applyMutationItems = yield* makeApplyMutationItems
|
304
340
|
|
305
341
|
while (true) {
|
306
342
|
// TODO make batch size configurable
|
307
343
|
const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, 10)
|
308
|
-
const [newEvents, deferreds] = ReadonlyArray.unzip(batchItems)
|
309
344
|
|
310
345
|
// Wait for the backend pulling to finish
|
311
346
|
yield* localPushesLatch.await
|
312
347
|
|
313
|
-
// Prevent
|
348
|
+
// Prevent backend pull processing until this local push is finished
|
314
349
|
yield* pullLatch.close
|
315
350
|
|
351
|
+
// Since the generation might have changed since enqueuing, we need to filter out items with older generation
|
352
|
+
// It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
|
353
|
+
const filteredBatchItems = batchItems
|
354
|
+
.filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
|
355
|
+
.map(([mutationEventEncoded, deferred]) => [mutationEventEncoded, deferred] as const)
|
356
|
+
|
357
|
+
if (filteredBatchItems.length === 0) {
|
358
|
+
// console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
|
359
|
+
// Allow the backend pulling to start
|
360
|
+
yield* pullLatch.open
|
361
|
+
continue
|
362
|
+
}
|
363
|
+
|
364
|
+
const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems)
|
365
|
+
|
316
366
|
const syncState = yield* syncStateSref
|
317
367
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
318
368
|
|
@@ -323,42 +373,82 @@ const backgroundApplyLocalPushes = ({
|
|
323
373
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
324
374
|
})
|
325
375
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
376
|
+
switch (updateResult._tag) {
|
377
|
+
case 'unexpected-error': {
|
378
|
+
otelSpan?.addEvent('local-push:unexpected-error', {
|
379
|
+
batchSize: newEvents.length,
|
380
|
+
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
381
|
+
})
|
382
|
+
return yield* Effect.fail(updateResult.cause)
|
383
|
+
}
|
384
|
+
case 'rebase': {
|
385
|
+
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
386
|
+
}
|
387
|
+
case 'reject': {
|
388
|
+
otelSpan?.addEvent('local-push:reject', {
|
389
|
+
batchSize: newEvents.length,
|
390
|
+
updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
391
|
+
})
|
392
|
+
|
393
|
+
/*
|
394
|
+
|
395
|
+
TODO: how to test this?
|
396
|
+
*/
|
397
|
+
currentLocalPushGenerationRef.current++
|
398
|
+
|
399
|
+
const nextGeneration = currentLocalPushGenerationRef.current
|
333
400
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
401
|
+
const providedId = newEvents.at(0)!.id
|
402
|
+
// All subsequent pushes with same generation should be rejected as well
|
403
|
+
// We're also handling the case where the localPushQueue already contains events
|
404
|
+
// from the next generation which we preserve in the queue
|
405
|
+
const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
|
406
|
+
localPushesQueue,
|
407
|
+
(item) => item[2] >= nextGeneration,
|
408
|
+
)
|
409
|
+
|
410
|
+
if ((yield* BucketQueue.size(localPushesQueue)) > 0) {
|
411
|
+
console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
|
412
|
+
debugger
|
413
|
+
}
|
414
|
+
|
415
|
+
const allDeferredsToReject = [
|
416
|
+
...deferreds,
|
417
|
+
...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
|
418
|
+
].filter(isNotUndefined)
|
419
|
+
|
420
|
+
yield* Effect.forEach(allDeferredsToReject, (deferred) =>
|
421
|
+
Deferred.fail(
|
422
|
+
deferred,
|
423
|
+
LeaderAheadError.make({
|
345
424
|
minimumExpectedId: updateResult.expectedMinimumId,
|
346
425
|
providedId,
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
426
|
+
// nextGeneration,
|
427
|
+
}),
|
428
|
+
),
|
429
|
+
)
|
351
430
|
|
352
|
-
|
353
|
-
|
431
|
+
// Allow the backend pulling to start
|
432
|
+
yield* pullLatch.open
|
354
433
|
|
355
|
-
|
356
|
-
|
357
|
-
|
434
|
+
// In this case we're skipping state update and down/upstream processing
|
435
|
+
// We've cleared the local push queue and are now waiting for new local pushes / backend pulls
|
436
|
+
continue
|
437
|
+
}
|
438
|
+
case 'advance': {
|
439
|
+
break
|
440
|
+
}
|
441
|
+
default: {
|
442
|
+
casesHandled(updateResult)
|
443
|
+
}
|
358
444
|
}
|
359
445
|
|
360
446
|
yield* SubscriptionRef.set(syncStateSref, updateResult.newSyncState)
|
361
447
|
|
448
|
+
if (clientId === 'client-b') {
|
449
|
+
// yield* Effect.log('offer upstream-advance due to local-push')
|
450
|
+
// debugger
|
451
|
+
}
|
362
452
|
yield* connectedClientSessionPullQueues.offer({
|
363
453
|
payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents },
|
364
454
|
remaining: 0,
|
@@ -371,7 +461,7 @@ const backgroundApplyLocalPushes = ({
|
|
371
461
|
|
372
462
|
// Don't sync clientOnly mutations
|
373
463
|
const filteredBatch = updateResult.newEvents.filter((mutationEventEncoded) => {
|
374
|
-
const mutationDef = schema
|
464
|
+
const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
|
375
465
|
return mutationDef.options.clientOnly === false
|
376
466
|
})
|
377
467
|
|
@@ -387,7 +477,7 @@ const backgroundApplyLocalPushes = ({
|
|
387
477
|
type ApplyMutationItems = (_: {
|
388
478
|
batchItems: ReadonlyArray<MutationEvent.EncodedWithMeta>
|
389
479
|
/** Indexes are aligned with `batchItems` */
|
390
|
-
deferreds: ReadonlyArray<Deferred.Deferred<void,
|
480
|
+
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
391
481
|
}) => Effect.Effect<void, UnexpectedError>
|
392
482
|
|
393
483
|
// TODO how to handle errors gracefully
|
@@ -466,6 +556,7 @@ const backgroundBackendPulling = ({
|
|
466
556
|
dbMutationLog,
|
467
557
|
connectedClientSessionPullQueues,
|
468
558
|
schema,
|
559
|
+
clientId,
|
469
560
|
} = yield* LeaderThreadCtx
|
470
561
|
|
471
562
|
if (syncBackend === undefined) return
|
@@ -503,6 +594,12 @@ const backgroundBackendPulling = ({
|
|
503
594
|
|
504
595
|
if (updateResult._tag === 'reject') {
|
505
596
|
return shouldNeverHappen('The leader thread should never reject upstream advances')
|
597
|
+
} else if (updateResult._tag === 'unexpected-error') {
|
598
|
+
otelSpan?.addEvent('backend-pull:unexpected-error', {
|
599
|
+
newEventsCount: newEvents.length,
|
600
|
+
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
601
|
+
})
|
602
|
+
return yield* Effect.fail(updateResult.cause)
|
506
603
|
}
|
507
604
|
|
508
605
|
const newBackendHead = newEvents.at(-1)!.id
|
@@ -518,7 +615,7 @@ const backgroundBackendPulling = ({
|
|
518
615
|
})
|
519
616
|
|
520
617
|
const filteredRebasedPending = updateResult.newSyncState.pending.filter((mutationEvent) => {
|
521
|
-
const mutationDef = schema
|
618
|
+
const mutationDef = getMutationDef(schema, mutationEvent.mutation)
|
522
619
|
return mutationDef.options.clientOnly === false
|
523
620
|
})
|
524
621
|
yield* restartBackendPushing(filteredRebasedPending)
|
@@ -542,6 +639,9 @@ const backgroundBackendPulling = ({
|
|
542
639
|
updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
543
640
|
})
|
544
641
|
|
642
|
+
if (clientId === 'client-b') {
|
643
|
+
// yield* Effect.log('offer upstream-advance due to pull')
|
644
|
+
}
|
545
645
|
yield* connectedClientSessionPullQueues.offer({
|
546
646
|
payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents, trimRollbackUntil },
|
547
647
|
remaining,
|
@@ -617,15 +717,23 @@ const rollback = ({
|
|
617
717
|
}
|
618
718
|
}
|
619
719
|
|
620
|
-
|
621
|
-
|
622
|
-
sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
|
720
|
+
const eventIdPairChunks = ReadonlyArray.chunksOf(100)(
|
721
|
+
eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`),
|
623
722
|
)
|
624
723
|
|
724
|
+
// Delete the changeset rows
|
725
|
+
for (const eventIdPairChunk of eventIdPairChunks) {
|
726
|
+
db.execute(
|
727
|
+
sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
|
728
|
+
)
|
729
|
+
}
|
730
|
+
|
625
731
|
// Delete the mutation log rows
|
626
|
-
|
627
|
-
|
628
|
-
|
732
|
+
for (const eventIdPairChunk of eventIdPairChunks) {
|
733
|
+
dbMutationLog.execute(
|
734
|
+
sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
|
735
|
+
)
|
736
|
+
}
|
629
737
|
}).pipe(
|
630
738
|
Effect.withSpan('@livestore/common:leader-thread:syncing:rollback', {
|
631
739
|
attributes: { count: eventIdsToRollback.length },
|
@@ -675,7 +783,7 @@ const backgroundBackendPushing = ({
|
|
675
783
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
676
784
|
|
677
785
|
// TODO make batch size configurable
|
678
|
-
const queueItems = yield* BucketQueue.takeBetween(syncBackendQueue, 1,
|
786
|
+
const queueItems = yield* BucketQueue.takeBetween(syncBackendQueue, 1, BACKEND_PUSH_BATCH_SIZE)
|
679
787
|
|
680
788
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
681
789
|
|
@@ -693,7 +801,7 @@ const backgroundBackendPushing = ({
|
|
693
801
|
|
694
802
|
if (pushResult._tag === 'Left') {
|
695
803
|
if (LS_DEV) {
|
696
|
-
yield* Effect.logDebug('backend-push-error', { error: pushResult.left.toString() })
|
804
|
+
yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() })
|
697
805
|
}
|
698
806
|
otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
|
699
807
|
// wait for interrupt caused by background pulling which will then restart pushing
|
@@ -1,10 +1,12 @@
|
|
1
|
-
import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
1
|
+
import { LS_DEV, memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
2
2
|
import type { Scope } from '@livestore/utils/effect'
|
3
3
|
import { Effect, Option, Schema } from '@livestore/utils/effect'
|
4
4
|
|
5
|
-
import type { SqliteDb, SqliteError, UnexpectedError } from '../index.js'
|
5
|
+
import type { PreparedBindValues, SqliteDb, SqliteError, UnexpectedError } from '../index.js'
|
6
6
|
import { getExecArgsFromMutation } from '../mutation.js'
|
7
7
|
import {
|
8
|
+
EventId,
|
9
|
+
getMutationDef,
|
8
10
|
type LiveStoreSchema,
|
9
11
|
MUTATION_LOG_META_TABLE,
|
10
12
|
type MutationEvent,
|
@@ -33,7 +35,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
|
|
33
35
|
// TODO Running `Schema.hash` can be a bottleneck for larger schemas. There is an opportunity to run this
|
34
36
|
// at build time and lookup the pre-computed hash at runtime.
|
35
37
|
// Also see https://github.com/Effect-TS/effect/issues/2719
|
36
|
-
[...leaderThreadCtx.schema.mutations.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
|
38
|
+
[...leaderThreadCtx.schema.mutations.map.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
|
37
39
|
)
|
38
40
|
|
39
41
|
return (mutationEventEncoded, options) =>
|
@@ -42,7 +44,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
|
|
42
44
|
const skipMutationLog = options?.skipMutationLog ?? false
|
43
45
|
|
44
46
|
const mutationName = mutationEventEncoded.mutation
|
45
|
-
const mutationDef = schema
|
47
|
+
const mutationDef = getMutationDef(schema, mutationName)
|
46
48
|
|
47
49
|
const execArgsArr = getExecArgsFromMutation({
|
48
50
|
mutationDef,
|
@@ -127,6 +129,20 @@ const insertIntoMutationLog = (
|
|
127
129
|
const mutationDefSchemaHash =
|
128
130
|
mutationDefSchemaHashMap.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
|
129
131
|
|
132
|
+
if (LS_DEV && mutationEventEncoded.parentId.global !== EventId.ROOT.global) {
|
133
|
+
const parentMutationExists =
|
134
|
+
dbMutationLog.select<{ count: number }>(
|
135
|
+
`SELECT COUNT(*) as count FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ? AND idClient = ?`,
|
136
|
+
[mutationEventEncoded.parentId.global, mutationEventEncoded.parentId.client] as any as PreparedBindValues,
|
137
|
+
)[0]!.count === 1
|
138
|
+
|
139
|
+
if (parentMutationExists === false) {
|
140
|
+
shouldNeverHappen(
|
141
|
+
`Parent mutation ${mutationEventEncoded.parentId.global},${mutationEventEncoded.parentId.client} does not exist`,
|
142
|
+
)
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
130
146
|
// TODO use prepared statements
|
131
147
|
yield* execSql(
|
132
148
|
dbMutationLog,
|
@@ -160,7 +176,7 @@ const makeShouldExcludeMutationFromLog = memoizeByRef((schema: LiveStoreSchema)
|
|
160
176
|
return (mutationName: string, mutationEventEncoded: MutationEvent.AnyEncoded): boolean => {
|
161
177
|
if (mutationLogExclude.has(mutationName)) return true
|
162
178
|
|
163
|
-
const mutationDef = schema
|
179
|
+
const mutationDef = getMutationDef(schema, mutationName)
|
164
180
|
const execArgsArr = getExecArgsFromMutation({
|
165
181
|
mutationDef,
|
166
182
|
mutationEvent: { decoded: undefined, encoded: mutationEventEncoded },
|
@@ -2,7 +2,7 @@ import { Effect, Queue } from '@livestore/utils/effect'
|
|
2
2
|
|
3
3
|
import * as MutationEvent from '../schema/MutationEvent.js'
|
4
4
|
import { getMutationEventsSince } from './mutationlog.js'
|
5
|
-
import { type PullQueueItem, type PullQueueSet } from './types.js'
|
5
|
+
import { LeaderThreadCtx, type PullQueueItem, type PullQueueSet } from './types.js'
|
6
6
|
|
7
7
|
export const makePullQueueSet = Effect.gen(function* () {
|
8
8
|
const set = new Set<Queue.Queue<PullQueueItem>>()
|
@@ -46,6 +46,15 @@ export const makePullQueueSet = Effect.gen(function* () {
|
|
46
46
|
return
|
47
47
|
}
|
48
48
|
|
49
|
+
const { clientId } = yield* LeaderThreadCtx
|
50
|
+
if (clientId === 'client-b') {
|
51
|
+
// console.log(
|
52
|
+
// 'offer',
|
53
|
+
// item.payload._tag,
|
54
|
+
// item.payload.newEvents.map((_) => _.toJSON()),
|
55
|
+
// )
|
56
|
+
}
|
57
|
+
|
49
58
|
for (const queue of set) {
|
50
59
|
yield* Queue.offer(queue, item)
|
51
60
|
}
|
@@ -1,11 +1,9 @@
|
|
1
1
|
import type { WebChannel } from '@livestore/utils/effect'
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
-
import { IntentionalShutdownCause } from '../index.js'
|
4
|
+
import { IntentionalShutdownCause, UnexpectedError } from '../index.js'
|
5
5
|
|
6
|
-
export class
|
7
|
-
|
8
|
-
export class All extends Schema.Union(IntentionalShutdownCause, DedicatedWorkerDisconnectBroadcast) {}
|
6
|
+
export class All extends Schema.Union(IntentionalShutdownCause, UnexpectedError) {}
|
9
7
|
|
10
8
|
/**
|
11
9
|
* Used internally by an adapter to shutdown gracefully.
|
@@ -14,7 +14,7 @@ import { Context, Schema } from '@livestore/utils/effect'
|
|
14
14
|
import type {
|
15
15
|
BootStatus,
|
16
16
|
Devtools,
|
17
|
-
|
17
|
+
LeaderAheadError,
|
18
18
|
MakeSqliteDb,
|
19
19
|
MigrationsReport,
|
20
20
|
PersistenceInfo,
|
@@ -126,13 +126,19 @@ export interface LeaderSyncProcessor {
|
|
126
126
|
/** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
|
127
127
|
batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
128
128
|
options?: {
|
129
|
+
/**
|
130
|
+
* This generation number is used to automatically reject subsequent pushes
|
131
|
+
* of a previously rejected push from a client session. This might occur in
|
132
|
+
* certain concurrent scenarios.
|
133
|
+
*/
|
134
|
+
// generation: number
|
129
135
|
/**
|
130
136
|
* If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
|
131
137
|
* @default false
|
132
138
|
*/
|
133
139
|
waitForProcessing?: boolean
|
134
140
|
},
|
135
|
-
) => Effect.Effect<void,
|
141
|
+
) => Effect.Effect<void, LeaderAheadError>
|
136
142
|
|
137
143
|
pushPartial: (args: {
|
138
144
|
mutationEvent: MutationEvent.PartialAnyEncoded
|
@@ -4,7 +4,7 @@ import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
|
|
4
4
|
import { type MigrationOptionsFromMutationLog, type SqliteDb, UnexpectedError } from './adapter-types.js'
|
5
5
|
import { makeApplyMutation } from './leader-thread/apply-mutation.js'
|
6
6
|
import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/mod.js'
|
7
|
-
import { EventId, MUTATION_LOG_META_TABLE } from './schema/mod.js'
|
7
|
+
import { EventId, getMutationDef, MUTATION_LOG_META_TABLE } from './schema/mod.js'
|
8
8
|
import type { PreparedBindValues } from './util.js'
|
9
9
|
import { sql } from './util.js'
|
10
10
|
|
@@ -33,7 +33,7 @@ export const rehydrateFromMutationLog = ({
|
|
33
33
|
|
34
34
|
const processMutation = (row: MutationLogMetaRow) =>
|
35
35
|
Effect.gen(function* () {
|
36
|
-
const mutationDef = schema
|
36
|
+
const mutationDef = getMutationDef(schema, row.mutation)
|
37
37
|
|
38
38
|
if (migrationOptions.excludeMutations?.has(row.mutation) === true) return
|
39
39
|
|
package/src/schema/EventId.ts
CHANGED
@@ -33,6 +33,22 @@ export const compare = (a: EventId, b: EventId) => {
|
|
33
33
|
return a.client - b.client
|
34
34
|
}
|
35
35
|
|
36
|
+
/**
|
37
|
+
* Convert an event id to a string representation.
|
38
|
+
*/
|
39
|
+
export const toString = (id: EventId) => `(${id.global},${id.client})`
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Convert a string representation of an event id to an event id.
|
43
|
+
*/
|
44
|
+
export const fromString = (str: string): EventId => {
|
45
|
+
const [global, client] = str.slice(1, -1).split(',').map(Number)
|
46
|
+
if (global === undefined || client === undefined) {
|
47
|
+
throw new Error('Invalid event id string')
|
48
|
+
}
|
49
|
+
return { global, client } as EventId
|
50
|
+
}
|
51
|
+
|
36
52
|
export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.client === b.client
|
37
53
|
|
38
54
|
export type EventIdPair = { id: EventId; parentId: EventId }
|