@livestore/common 0.4.0-dev.2 → 0.4.0-dev.5
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 +4 -3
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js.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-leader.d.ts +24 -24
- package/dist/errors.d.ts +15 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -2
- package/dist/errors.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +4 -3
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +43 -24
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +4 -10
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +3 -5
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +1 -1
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +34 -15
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts +2 -2
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +4 -6
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +2 -3
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -1
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -2
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -2
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +5 -5
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +8 -2
- package/dist/materializer-helper.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/schema/EventDef.d.ts +3 -0
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +1 -2
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +93 -2
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +21 -3
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.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/mod.js +1 -1
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
- package/dist/schema/state/sqlite/query-builder/api.d.ts.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 +6 -2
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +54 -0
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +36 -0
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +2 -0
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -9
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +17 -17
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/errors.d.ts +61 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +36 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/index.d.ts +1 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mock-sync-backend.d.ts +14 -0
- package/dist/sync/mock-sync-backend.d.ts.map +1 -0
- package/dist/sync/mock-sync-backend.js +62 -0
- package/dist/sync/mock-sync-backend.js.map +1 -0
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +3 -1
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/sync-backend-kv.d.ts +7 -0
- package/dist/sync/sync-backend-kv.d.ts.map +1 -0
- package/dist/sync/sync-backend-kv.js +18 -0
- package/dist/sync/sync-backend-kv.js.map +1 -0
- package/dist/sync/sync-backend.d.ts +85 -0
- package/dist/sync/sync-backend.d.ts.map +1 -0
- package/dist/sync/sync-backend.js +24 -0
- package/dist/sync/sync-backend.js.map +1 -0
- package/dist/sync/sync.d.ts +6 -84
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +2 -27
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/validate-push-payload.d.ts +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +6 -6
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +4 -4
- package/src/adapter-types.ts +8 -3
- package/src/errors.ts +14 -3
- package/src/leader-thread/LeaderSyncProcessor.ts +79 -30
- package/src/leader-thread/eventlog.ts +9 -5
- package/src/leader-thread/leader-worker-devtools.ts +1 -1
- package/src/leader-thread/make-leader-thread-layer.ts +64 -22
- package/src/leader-thread/materialize-event.ts +5 -6
- package/src/leader-thread/recreate-db.ts +11 -3
- package/src/leader-thread/shutdown-channel.ts +16 -2
- package/src/leader-thread/types.ts +5 -5
- package/src/materializer-helper.ts +9 -3
- package/src/schema/EventDef.ts +3 -0
- package/src/schema/LiveStoreEvent.ts +1 -2
- package/src/schema/schema.ts +1 -1
- package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
- package/src/schema/state/sqlite/client-document-def.ts +115 -3
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +24 -3
- package/src/schema/state/sqlite/mod.ts +1 -0
- package/src/schema/state/sqlite/query-builder/api.ts +7 -2
- package/src/schema/state/sqlite/query-builder/impl.test.ts +64 -0
- package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
- package/src/schema/state/sqlite/system-tables.ts +2 -0
- package/src/sync/ClientSessionSyncProcessor.ts +32 -32
- package/src/sync/errors.ts +38 -0
- package/src/sync/index.ts +1 -0
- package/src/sync/mock-sync-backend.ts +96 -0
- package/src/sync/next/history-dag.ts +3 -1
- package/src/sync/sync-backend-kv.ts +22 -0
- package/src/sync/sync-backend.ts +137 -0
- package/src/sync/sync.ts +6 -89
- package/src/sync/validate-push-payload.ts +6 -7
- package/src/version.ts +2 -2
@@ -2,10 +2,12 @@ import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE
|
|
2
2
|
import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
|
3
3
|
import {
|
4
4
|
BucketQueue,
|
5
|
+
Cause,
|
5
6
|
Deferred,
|
6
7
|
Effect,
|
7
8
|
Exit,
|
8
9
|
FiberHandle,
|
10
|
+
Layer,
|
9
11
|
Option,
|
10
12
|
OtelTracer,
|
11
13
|
pipe,
|
@@ -16,13 +18,22 @@ import {
|
|
16
18
|
SubscriptionRef,
|
17
19
|
} from '@livestore/utils/effect'
|
18
20
|
import type * as otel from '@opentelemetry/api'
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
import {
|
22
|
+
type IntentionalShutdownCause,
|
23
|
+
type MaterializeError,
|
24
|
+
type SqliteDb,
|
25
|
+
UnexpectedError,
|
26
|
+
} from '../adapter-types.ts'
|
22
27
|
import { makeMaterializerHash } from '../materializer-helper.ts'
|
23
28
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
24
29
|
import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.ts'
|
25
|
-
import {
|
30
|
+
import {
|
31
|
+
type InvalidPullError,
|
32
|
+
type InvalidPushError,
|
33
|
+
type IsOfflineError,
|
34
|
+
LeaderAheadError,
|
35
|
+
type SyncBackend,
|
36
|
+
} from '../sync/sync.ts'
|
26
37
|
import * as SyncState from '../sync/syncstate.ts'
|
27
38
|
import { sql } from '../util.ts'
|
28
39
|
import * as Eventlog from './eventlog.ts'
|
@@ -71,6 +82,7 @@ export const makeLeaderSyncProcessor = ({
|
|
71
82
|
initialBlockingSyncContext,
|
72
83
|
initialSyncState,
|
73
84
|
onError,
|
85
|
+
livePull,
|
74
86
|
params,
|
75
87
|
testing,
|
76
88
|
}: {
|
@@ -90,6 +102,8 @@ export const makeLeaderSyncProcessor = ({
|
|
90
102
|
*/
|
91
103
|
backendPushBatchSize?: number
|
92
104
|
}
|
105
|
+
/** * Whether the sync backend should reactively pull new events from the sync backend */
|
106
|
+
livePull: boolean
|
93
107
|
testing: {
|
94
108
|
delays?: {
|
95
109
|
localPushProcessing?: Effect.Effect<void>
|
@@ -224,12 +238,31 @@ export const makeLeaderSyncProcessor = ({
|
|
224
238
|
}
|
225
239
|
}
|
226
240
|
|
227
|
-
const
|
241
|
+
const maybeShutdownOnError = (
|
242
|
+
cause: Cause.Cause<
|
243
|
+
| UnexpectedError
|
244
|
+
| IntentionalShutdownCause
|
245
|
+
| IsOfflineError
|
246
|
+
| InvalidPushError
|
247
|
+
| InvalidPullError
|
248
|
+
| MaterializeError
|
249
|
+
>,
|
250
|
+
) =>
|
228
251
|
Effect.gen(function* () {
|
229
|
-
if (onError === '
|
230
|
-
|
231
|
-
|
252
|
+
if (onError === 'ignore') {
|
253
|
+
if (LS_DEV) {
|
254
|
+
yield* Effect.logDebug(
|
255
|
+
`Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
|
256
|
+
Cause.pretty(cause),
|
257
|
+
)
|
258
|
+
}
|
259
|
+
return
|
232
260
|
}
|
261
|
+
|
262
|
+
const errorToSend = Cause.isFailType(cause) ? cause.error : UnexpectedError.make({ cause })
|
263
|
+
yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
|
264
|
+
|
265
|
+
return yield* Effect.die(cause)
|
233
266
|
})
|
234
267
|
|
235
268
|
yield* backgroundApplyLocalPushes({
|
@@ -246,20 +279,19 @@ export const makeLeaderSyncProcessor = ({
|
|
246
279
|
testing: {
|
247
280
|
delay: testing?.delays?.localPushProcessing,
|
248
281
|
},
|
249
|
-
}).pipe(Effect.
|
282
|
+
}).pipe(Effect.catchAllCause(maybeShutdownOnError), Effect.forkScoped)
|
250
283
|
|
251
|
-
const backendPushingFiberHandle = yield* FiberHandle.make()
|
284
|
+
const backendPushingFiberHandle = yield* FiberHandle.make<undefined, never>()
|
252
285
|
const backendPushingEffect = backgroundBackendPushing({
|
253
286
|
syncBackendPushQueue,
|
254
287
|
otelSpan,
|
255
288
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
256
289
|
backendPushBatchSize,
|
257
|
-
}).pipe(Effect.
|
290
|
+
}).pipe(Effect.catchAllCause(maybeShutdownOnError))
|
258
291
|
|
259
292
|
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
260
293
|
|
261
294
|
yield* backgroundBackendPulling({
|
262
|
-
initialBackendHead: initialSyncState.upstreamHead.global,
|
263
295
|
isClientEvent,
|
264
296
|
restartBackendPushing: (filteredRebasedPending) =>
|
265
297
|
Effect.gen(function* () {
|
@@ -276,13 +308,24 @@ export const makeLeaderSyncProcessor = ({
|
|
276
308
|
syncStateSref,
|
277
309
|
localPushesLatch,
|
278
310
|
pullLatch,
|
311
|
+
livePull,
|
279
312
|
dbState,
|
280
313
|
otelSpan,
|
281
314
|
initialBlockingSyncContext,
|
282
315
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
283
316
|
connectedClientSessionPullQueues,
|
284
317
|
advancePushHead,
|
285
|
-
}).pipe(
|
318
|
+
}).pipe(
|
319
|
+
Effect.retry({
|
320
|
+
// We want to retry pulling if we've lost connection to the sync backend
|
321
|
+
while: (cause) => cause._tag === 'IsOfflineError',
|
322
|
+
}),
|
323
|
+
Effect.catchAllCause(maybeShutdownOnError),
|
324
|
+
// Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
|
325
|
+
// This might be a bug in Effect. Only seems to happen in the browser.
|
326
|
+
Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
|
327
|
+
Effect.forkScoped,
|
328
|
+
)
|
286
329
|
|
287
330
|
return { initialLeaderHead: initialSyncState.localHead }
|
288
331
|
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
@@ -405,7 +448,7 @@ const backgroundApplyLocalPushes = ({
|
|
405
448
|
batchSize: newEvents.length,
|
406
449
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
407
450
|
})
|
408
|
-
return yield* new
|
451
|
+
return yield* new UnexpectedError({ cause: mergeResult.message })
|
409
452
|
}
|
410
453
|
case 'rebase': {
|
411
454
|
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
@@ -496,7 +539,7 @@ type MaterializeEventsBatch = (_: {
|
|
496
539
|
* Indexes are aligned with `batchItems`
|
497
540
|
*/
|
498
541
|
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
499
|
-
}) => Effect.Effect<void,
|
542
|
+
}) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
|
500
543
|
|
501
544
|
// TODO how to handle errors gracefully
|
502
545
|
const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds }) =>
|
@@ -536,24 +579,22 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
|
|
536
579
|
attributes: { batchSize: batchItems.length },
|
537
580
|
}),
|
538
581
|
Effect.tapCauseLogPretty,
|
539
|
-
UnexpectedError.mapToUnexpectedError,
|
540
582
|
)
|
541
583
|
|
542
584
|
const backgroundBackendPulling = ({
|
543
|
-
initialBackendHead,
|
544
585
|
isClientEvent,
|
545
586
|
restartBackendPushing,
|
546
587
|
otelSpan,
|
547
588
|
dbState,
|
548
589
|
syncStateSref,
|
549
590
|
localPushesLatch,
|
591
|
+
livePull,
|
550
592
|
pullLatch,
|
551
593
|
devtoolsLatch,
|
552
594
|
initialBlockingSyncContext,
|
553
595
|
connectedClientSessionPullQueues,
|
554
596
|
advancePushHead,
|
555
597
|
}: {
|
556
|
-
initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
|
557
598
|
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
558
599
|
restartBackendPushing: (
|
559
600
|
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
@@ -563,6 +604,7 @@ const backgroundBackendPulling = ({
|
|
563
604
|
dbState: SqliteDb
|
564
605
|
localPushesLatch: Effect.Latch
|
565
606
|
pullLatch: Effect.Latch
|
607
|
+
livePull: boolean
|
566
608
|
devtoolsLatch: Effect.Latch | undefined
|
567
609
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
568
610
|
connectedClientSessionPullQueues: PullQueueSet
|
@@ -573,7 +615,7 @@ const backgroundBackendPulling = ({
|
|
573
615
|
|
574
616
|
if (syncBackend === undefined) return
|
575
617
|
|
576
|
-
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[],
|
618
|
+
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
|
577
619
|
Effect.gen(function* () {
|
578
620
|
if (newEvents.length === 0) return
|
579
621
|
|
@@ -605,7 +647,7 @@ const backgroundBackendPulling = ({
|
|
605
647
|
newEventsCount: newEvents.length,
|
606
648
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
607
649
|
})
|
608
|
-
return yield* new
|
650
|
+
return yield* new UnexpectedError({ cause: mergeResult.message })
|
609
651
|
}
|
610
652
|
|
611
653
|
const newBackendHead = newEvents.at(-1)!.seqNum
|
@@ -657,7 +699,7 @@ const backgroundBackendPulling = ({
|
|
657
699
|
EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
|
658
700
|
),
|
659
701
|
)
|
660
|
-
yield* Eventlog.updateSyncMetadata(confirmedNewEvents)
|
702
|
+
yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(UnexpectedError.mapToUnexpectedError)
|
661
703
|
}
|
662
704
|
}
|
663
705
|
|
@@ -671,18 +713,20 @@ const backgroundBackendPulling = ({
|
|
671
713
|
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
672
714
|
|
673
715
|
// Allow local pushes to be processed again
|
674
|
-
if (
|
716
|
+
if (pageInfo._tag === 'NoMore') {
|
675
717
|
yield* localPushesLatch.open
|
676
718
|
}
|
677
719
|
})
|
678
720
|
|
679
|
-
const
|
721
|
+
const syncState = yield* syncStateSref
|
722
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
723
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
|
680
724
|
|
681
725
|
const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
|
682
726
|
|
683
|
-
yield* syncBackend.pull(cursorInfo).pipe(
|
727
|
+
yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
|
684
728
|
// TODO only take from queue while connected
|
685
|
-
Stream.tap(({ batch,
|
729
|
+
Stream.tap(({ batch, pageInfo }) =>
|
686
730
|
Effect.gen(function* () {
|
687
731
|
// yield* Effect.spanEvent('batch', {
|
688
732
|
// attributes: {
|
@@ -690,12 +734,10 @@ const backgroundBackendPulling = ({
|
|
690
734
|
// batch: TRACE_VERBOSE ? batch : undefined,
|
691
735
|
// },
|
692
736
|
// })
|
693
|
-
|
694
737
|
// NOTE we only want to take process events when the sync backend is connected
|
695
738
|
// (e.g. needed for simulating being offline)
|
696
739
|
// TODO remove when there's a better way to handle this in stream above
|
697
740
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
698
|
-
|
699
741
|
yield* onNewPullChunk(
|
700
742
|
batch.map((_) =>
|
701
743
|
LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, {
|
@@ -706,10 +748,9 @@ const backgroundBackendPulling = ({
|
|
706
748
|
materializerHashSession: Option.none(),
|
707
749
|
}),
|
708
750
|
),
|
709
|
-
|
751
|
+
pageInfo,
|
710
752
|
)
|
711
|
-
|
712
|
-
yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
|
753
|
+
yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
|
713
754
|
}),
|
714
755
|
),
|
715
756
|
Stream.runDrain,
|
@@ -752,6 +793,14 @@ const backgroundBackendPushing = ({
|
|
752
793
|
const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
|
753
794
|
|
754
795
|
if (pushResult._tag === 'Left') {
|
796
|
+
if (
|
797
|
+
pushResult.left._tag === 'InvalidPushError' &&
|
798
|
+
// server ahead errors are gracefully handled
|
799
|
+
pushResult.left.cause._tag !== 'ServerAheadError'
|
800
|
+
) {
|
801
|
+
return yield* pushResult.left
|
802
|
+
}
|
803
|
+
|
755
804
|
if (LS_DEV) {
|
756
805
|
yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() })
|
757
806
|
}
|
@@ -123,6 +123,14 @@ export const getBackendHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.
|
|
123
123
|
export const updateBackendHead = (dbEventlog: SqliteDb, head: EventSequenceNumber.EventSequenceNumber) =>
|
124
124
|
dbEventlog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
|
125
125
|
|
126
|
+
export const getBackendIdFromDb = (dbEventlog: SqliteDb): Option.Option<string> =>
|
127
|
+
Option.fromNullable(
|
128
|
+
dbEventlog.select<{ backendId: string | null }>(sql`select backendId from ${SYNC_STATUS_TABLE}`)[0]?.backendId,
|
129
|
+
)
|
130
|
+
|
131
|
+
export const updateBackendId = (dbEventlog: SqliteDb, backendId: string) =>
|
132
|
+
dbEventlog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET backendId = '${backendId}'`)
|
133
|
+
|
126
134
|
export const insertIntoEventlog = (
|
127
135
|
eventEncoded: LiveStoreEvent.EncodedWithMeta,
|
128
136
|
dbEventlog: SqliteDb,
|
@@ -213,11 +221,7 @@ export const getSyncBackendCursorInfo = ({
|
|
213
221
|
).pipe(Effect.andThen(Schema.decode(EventlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
|
214
222
|
|
215
223
|
return Option.some({
|
216
|
-
|
217
|
-
global: remoteHead,
|
218
|
-
client: EventSequenceNumber.clientDefault,
|
219
|
-
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
220
|
-
},
|
224
|
+
eventSequenceNumber: remoteHead,
|
221
225
|
metadata: syncMetadataOption,
|
222
226
|
}) satisfies InitialSyncInfo
|
223
227
|
}).pipe(Effect.withSpan('@livestore/common:eventlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
|
@@ -262,7 +262,7 @@ const listenToDevtools = ({
|
|
262
262
|
|
263
263
|
if (syncBackend !== undefined) {
|
264
264
|
// TODO consider piggybacking on the existing leader-thread sync-pulling
|
265
|
-
yield* syncBackend.pull(Option.none()).pipe(
|
265
|
+
yield* syncBackend.pull(Option.none(), { live: true }).pipe(
|
266
266
|
Stream.map((_) => _.batch),
|
267
267
|
Stream.flattenIterables,
|
268
268
|
Stream.tap(({ eventEncoded, metadata }) =>
|
@@ -1,9 +1,15 @@
|
|
1
1
|
import { shouldNeverHappen } from '@livestore/utils'
|
2
2
|
import type { HttpClient, Schema, Scope } from '@livestore/utils/effect'
|
3
|
-
import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
import { Deferred, Effect, KeyValueStore, Layer, PlatformError, Queue, SubscriptionRef } from '@livestore/utils/effect'
|
4
|
+
import {
|
5
|
+
type BootStatus,
|
6
|
+
type MakeSqliteDb,
|
7
|
+
type MaterializerHashMismatchError,
|
8
|
+
type SqliteDb,
|
9
|
+
type SqliteError,
|
10
|
+
UnexpectedError,
|
11
|
+
} from '../adapter-types.ts'
|
12
|
+
import type { MigrationsReport } from '../defs.ts'
|
7
13
|
import type * as Devtools from '../devtools/mod.ts'
|
8
14
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
9
15
|
import { EventSequenceNumber, LiveStoreEvent, SystemTables } from '../schema/mod.ts'
|
@@ -71,10 +77,45 @@ export const makeLeaderThreadLayer = ({
|
|
71
77
|
// Either happens on initial boot or if schema changes
|
72
78
|
const dbStateMissing = !hasStateTables(dbState)
|
73
79
|
|
80
|
+
yield* Eventlog.initEventlogDb(dbEventlog)
|
81
|
+
|
74
82
|
const syncBackend =
|
75
83
|
syncOptions?.backend === undefined
|
76
84
|
? undefined
|
77
|
-
: yield* syncOptions.backend({ storeId, clientId, payload: syncPayload })
|
85
|
+
: yield* syncOptions.backend({ storeId, clientId, payload: syncPayload }).pipe(
|
86
|
+
Effect.provide(
|
87
|
+
Layer.succeed(
|
88
|
+
KeyValueStore.KeyValueStore,
|
89
|
+
KeyValueStore.makeStringOnly({
|
90
|
+
get: (_key) =>
|
91
|
+
Effect.sync(() => Eventlog.getBackendIdFromDb(dbEventlog)).pipe(
|
92
|
+
Effect.catchAllDefect((cause) =>
|
93
|
+
PlatformError.BadArgument.make({
|
94
|
+
method: 'getBackendIdFromDb',
|
95
|
+
description: 'Failed to get backendId',
|
96
|
+
module: 'KeyValueStore',
|
97
|
+
cause,
|
98
|
+
}),
|
99
|
+
),
|
100
|
+
),
|
101
|
+
set: (_key, value) =>
|
102
|
+
Effect.sync(() => Eventlog.updateBackendId(dbEventlog, value)).pipe(
|
103
|
+
Effect.catchAllDefect((cause) =>
|
104
|
+
PlatformError.BadArgument.make({
|
105
|
+
method: 'updateBackendId',
|
106
|
+
module: 'KeyValueStore',
|
107
|
+
description: 'Failed to update backendId',
|
108
|
+
cause,
|
109
|
+
}),
|
110
|
+
),
|
111
|
+
),
|
112
|
+
clear: Effect.dieMessage(`Not implemented. Should never be used.`),
|
113
|
+
remove: () => Effect.dieMessage(`Not implemented. Should never be used.`),
|
114
|
+
size: Effect.dieMessage(`Not implemented. Should never be used.`),
|
115
|
+
}),
|
116
|
+
),
|
117
|
+
),
|
118
|
+
)
|
78
119
|
|
79
120
|
if (syncBackend !== undefined) {
|
80
121
|
// We're already connecting to the sync backend concurrently
|
@@ -86,12 +127,21 @@ export const makeLeaderThreadLayer = ({
|
|
86
127
|
bootStatusQueue,
|
87
128
|
})
|
88
129
|
|
130
|
+
const materializeEvent = yield* makeMaterializeEvent({ schema, dbState, dbEventlog })
|
131
|
+
|
132
|
+
// Recreate state database if needed BEFORE creating sync processor
|
133
|
+
// This ensures all system tables exist before any queries are made
|
134
|
+
const { migrationsReport } = dbStateMissing
|
135
|
+
? yield* recreateDb({ dbState, dbEventlog, schema, bootStatusQueue, materializeEvent })
|
136
|
+
: { migrationsReport: { migrations: [] } }
|
137
|
+
|
89
138
|
const syncProcessor = yield* makeLeaderSyncProcessor({
|
90
139
|
schema,
|
91
140
|
dbState,
|
92
141
|
initialSyncState: getInitialSyncState({ dbEventlog, dbState, dbEventlogMissing }),
|
93
142
|
initialBlockingSyncContext,
|
94
143
|
onError: syncOptions?.onSyncError ?? 'ignore',
|
144
|
+
livePull: syncOptions?.livePull ?? true,
|
95
145
|
params: {
|
96
146
|
localPushBatchSize: params?.localPushBatchSize,
|
97
147
|
backendPushBatchSize: params?.backendPushBatchSize,
|
@@ -113,8 +163,6 @@ export const makeLeaderThreadLayer = ({
|
|
113
163
|
}
|
114
164
|
: { enabled: false as const }
|
115
165
|
|
116
|
-
const materializeEvent = yield* makeMaterializeEvent({ schema, dbState, dbEventlog })
|
117
|
-
|
118
166
|
const ctx = {
|
119
167
|
schema,
|
120
168
|
bootStatusQueue,
|
@@ -141,7 +189,7 @@ export const makeLeaderThreadLayer = ({
|
|
141
189
|
const layer = Layer.succeed(LeaderThreadCtx, ctx)
|
142
190
|
|
143
191
|
ctx.initialState = yield* bootLeaderThread({
|
144
|
-
|
192
|
+
migrationsReport,
|
145
193
|
initialBlockingSyncContext,
|
146
194
|
devtoolsOptions,
|
147
195
|
}).pipe(Effect.provide(layer))
|
@@ -244,12 +292,12 @@ const makeInitialBlockingSyncContext = ({
|
|
244
292
|
|
245
293
|
return {
|
246
294
|
blockingDeferred,
|
247
|
-
update: ({ processed,
|
295
|
+
update: ({ processed, pageInfo }) =>
|
248
296
|
Effect.gen(function* () {
|
249
297
|
if (ctx.isDone === true) return
|
250
298
|
|
251
|
-
if (ctx.total === -1) {
|
252
|
-
ctx.total = remaining + processed
|
299
|
+
if (ctx.total === -1 && pageInfo._tag === 'MoreKnown') {
|
300
|
+
ctx.total = pageInfo.remaining + processed
|
253
301
|
}
|
254
302
|
|
255
303
|
ctx.processedEvents += processed
|
@@ -258,7 +306,7 @@ const makeInitialBlockingSyncContext = ({
|
|
258
306
|
progress: { done: ctx.processedEvents, total: ctx.total },
|
259
307
|
})
|
260
308
|
|
261
|
-
if (
|
309
|
+
if (pageInfo._tag === 'NoMore' && blockingDeferred !== undefined) {
|
262
310
|
yield* Deferred.succeed(blockingDeferred, void 0)
|
263
311
|
ctx.isDone = true
|
264
312
|
}
|
@@ -271,26 +319,20 @@ const makeInitialBlockingSyncContext = ({
|
|
271
319
|
* It also starts various background processes (e.g. syncing)
|
272
320
|
*/
|
273
321
|
const bootLeaderThread = ({
|
274
|
-
|
322
|
+
migrationsReport,
|
275
323
|
initialBlockingSyncContext,
|
276
324
|
devtoolsOptions,
|
277
325
|
}: {
|
278
|
-
|
326
|
+
migrationsReport: MigrationsReport
|
279
327
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
280
328
|
devtoolsOptions: DevtoolsOptions
|
281
329
|
}): Effect.Effect<
|
282
330
|
LeaderThreadCtx['Type']['initialState'],
|
283
|
-
UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
|
331
|
+
UnexpectedError | SqliteError | IsOfflineError | InvalidPullError | MaterializerHashMismatchError,
|
284
332
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
285
333
|
> =>
|
286
334
|
Effect.gen(function* () {
|
287
|
-
const {
|
288
|
-
|
289
|
-
yield* Eventlog.initEventlogDb(dbEventlog)
|
290
|
-
|
291
|
-
const { migrationsReport } = dbStateMissing
|
292
|
-
? yield* recreateDb({ dbState, dbEventlog, schema, bootStatusQueue, materializeEvent })
|
293
|
-
: { migrationsReport: { migrations: [] } }
|
335
|
+
const { bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
|
294
336
|
|
295
337
|
// NOTE the sync processor depends on the dbs being initialized properly
|
296
338
|
const { initialLeaderHead } = yield* syncProcessor.boot
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { isDevEnv, LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
2
2
|
import { Effect, Option, ReadonlyArray, Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
-
import { type SqliteDb
|
4
|
+
import { MaterializeError, MaterializerHashMismatchError, type SqliteDb } from '../adapter-types.ts'
|
5
5
|
import { getExecStatementsFromMaterializer, hashMaterializerResults } from '../materializer-helper.ts'
|
6
6
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
7
7
|
import { EventSequenceNumber, getEventDef, SystemTables } from '../schema/mod.ts'
|
@@ -11,6 +11,7 @@ import { execSql, execSqlPrepared } from './connection.ts'
|
|
11
11
|
import * as Eventlog from './eventlog.ts'
|
12
12
|
import type { MaterializeEvent } from './types.ts'
|
13
13
|
|
14
|
+
// TODO refactor `makeMaterializeEvent` to not return an Effect for the constructor as it's not needed
|
14
15
|
export const makeMaterializeEvent = ({
|
15
16
|
schema,
|
16
17
|
dbState,
|
@@ -19,7 +20,7 @@ export const makeMaterializeEvent = ({
|
|
19
20
|
schema: LiveStoreSchema
|
20
21
|
dbState: SqliteDb
|
21
22
|
dbEventlog: SqliteDb
|
22
|
-
}): Effect.Effect<MaterializeEvent
|
23
|
+
}): Effect.Effect<MaterializeEvent> =>
|
23
24
|
Effect.gen(function* () {
|
24
25
|
const eventDefSchemaHashMap = new Map(
|
25
26
|
// TODO Running `Schema.hash` can be a bottleneck for larger schemas. There is an opportunity to run this
|
@@ -49,10 +50,7 @@ export const makeMaterializeEvent = ({
|
|
49
50
|
eventEncoded.meta.materializerHashSession._tag === 'Some' &&
|
50
51
|
eventEncoded.meta.materializerHashSession.value !== materializerHash.value
|
51
52
|
) {
|
52
|
-
yield*
|
53
|
-
cause: `Materializer hash mismatch detected for event "${eventEncoded.name}".`,
|
54
|
-
note: `Please make sure your event materializer is a pure function without side effects.`,
|
55
|
-
})
|
53
|
+
return yield* MaterializerHashMismatchError.make({ eventName: eventEncoded.name })
|
56
54
|
}
|
57
55
|
|
58
56
|
// NOTE we might want to bring this back if we want to debug no-op events
|
@@ -126,6 +124,7 @@ export const makeMaterializeEvent = ({
|
|
126
124
|
hash: materializerHash,
|
127
125
|
}
|
128
126
|
}).pipe(
|
127
|
+
Effect.mapError((cause) => MaterializeError.make({ cause })),
|
129
128
|
Effect.withSpan(`@livestore/common:leader-thread:materializeEvent`, {
|
130
129
|
attributes: {
|
131
130
|
eventName: eventEncoded.name,
|
@@ -2,8 +2,16 @@ import { casesHandled } from '@livestore/utils'
|
|
2
2
|
import { Effect, Queue } from '@livestore/utils/effect'
|
3
3
|
|
4
4
|
import type { MigrationsReport } from '../defs.ts'
|
5
|
-
import
|
6
|
-
|
5
|
+
import {
|
6
|
+
type BootStatus,
|
7
|
+
type MaterializeError,
|
8
|
+
type MigrationHooks,
|
9
|
+
migrateDb,
|
10
|
+
rematerializeFromEventlog,
|
11
|
+
type SqliteDb,
|
12
|
+
type SqliteError,
|
13
|
+
UnexpectedError,
|
14
|
+
} from '../index.ts'
|
7
15
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
8
16
|
import { configureConnection } from './connection.ts'
|
9
17
|
import type { MaterializeEvent } from './types.ts'
|
@@ -20,7 +28,7 @@ export const recreateDb = ({
|
|
20
28
|
schema: LiveStoreSchema
|
21
29
|
bootStatusQueue: Queue.Queue<BootStatus>
|
22
30
|
materializeEvent: MaterializeEvent
|
23
|
-
}): Effect.Effect<{ migrationsReport: MigrationsReport }, UnexpectedError | SqliteError> =>
|
31
|
+
}): Effect.Effect<{ migrationsReport: MigrationsReport }, UnexpectedError | MaterializeError | SqliteError> =>
|
24
32
|
Effect.gen(function* () {
|
25
33
|
const migrationOptions = schema.state.sqlite.migrations
|
26
34
|
let migrationsReport: MigrationsReport
|
@@ -1,9 +1,23 @@
|
|
1
1
|
import type { WebChannel } from '@livestore/utils/effect'
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
-
import {
|
4
|
+
import {
|
5
|
+
IntentionalShutdownCause,
|
6
|
+
InvalidPullError,
|
7
|
+
InvalidPushError,
|
8
|
+
IsOfflineError,
|
9
|
+
MaterializeError,
|
10
|
+
UnexpectedError,
|
11
|
+
} from '../index.ts'
|
5
12
|
|
6
|
-
export class All extends Schema.Union(
|
13
|
+
export class All extends Schema.Union(
|
14
|
+
IntentionalShutdownCause,
|
15
|
+
UnexpectedError,
|
16
|
+
IsOfflineError,
|
17
|
+
InvalidPushError,
|
18
|
+
InvalidPullError,
|
19
|
+
MaterializeError,
|
20
|
+
) {}
|
7
21
|
|
8
22
|
/**
|
9
23
|
* Used internally by an adapter to shutdown gracefully.
|
@@ -13,7 +13,7 @@ import { Context, Schema } from '@livestore/utils/effect'
|
|
13
13
|
import type { MeshNode } from '@livestore/webmesh'
|
14
14
|
|
15
15
|
import type { MigrationsReport } from '../defs.ts'
|
16
|
-
import type {
|
16
|
+
import type { MaterializeError } from '../errors.ts'
|
17
17
|
import type {
|
18
18
|
BootStatus,
|
19
19
|
Devtools,
|
@@ -43,7 +43,7 @@ export const InitialSyncOptions = Schema.Union(InitialSyncOptionsSkip, InitialSy
|
|
43
43
|
export type InitialSyncOptions = typeof InitialSyncOptions.Type
|
44
44
|
|
45
45
|
export type InitialSyncInfo = Option.Option<{
|
46
|
-
|
46
|
+
eventSequenceNumber: EventSequenceNumber.GlobalEventSequenceNumber
|
47
47
|
metadata: Option.Option<Schema.JsonValue>
|
48
48
|
}>
|
49
49
|
|
@@ -98,7 +98,7 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
|
|
98
98
|
shutdownChannel: ShutdownChannel
|
99
99
|
eventSchema: LiveStoreEvent.ForEventDefRecord<any>
|
100
100
|
devtools: DevtoolsContext
|
101
|
-
syncBackend: SyncBackend | undefined
|
101
|
+
syncBackend: SyncBackend.SyncBackend | undefined
|
102
102
|
syncProcessor: LeaderSyncProcessor
|
103
103
|
materializeEvent: MaterializeEvent
|
104
104
|
initialState: {
|
@@ -125,12 +125,12 @@ export type MaterializeEvent = (
|
|
125
125
|
sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any } | { _tag: 'no-op' }
|
126
126
|
hash: Option.Option<number>
|
127
127
|
},
|
128
|
-
|
128
|
+
MaterializeError
|
129
129
|
>
|
130
130
|
|
131
131
|
export type InitialBlockingSyncContext = {
|
132
132
|
blockingDeferred: Deferred.Deferred<void> | undefined
|
133
|
-
update: (_: {
|
133
|
+
update: (_: { pageInfo: SyncBackend.PullResPageInfo; processed: number }) => Effect.Effect<void>
|
134
134
|
}
|
135
135
|
|
136
136
|
export interface LeaderSyncProcessor {
|
@@ -31,8 +31,13 @@ export const getExecStatementsFromMaterializer = ({
|
|
31
31
|
bindValues: PreparedBindValues
|
32
32
|
writeTables: ReadonlySet<string> | undefined
|
33
33
|
}> => {
|
34
|
-
const
|
35
|
-
event.decoded === undefined
|
34
|
+
const eventDecoded =
|
35
|
+
event.decoded === undefined
|
36
|
+
? {
|
37
|
+
...event.encoded!,
|
38
|
+
args: Schema.decodeUnknownSync(eventDef.schema)(event.encoded!.args),
|
39
|
+
}
|
40
|
+
: event.decoded
|
36
41
|
|
37
42
|
const eventArgsEncoded = isNil(event.decoded?.args)
|
38
43
|
? undefined
|
@@ -58,11 +63,12 @@ export const getExecStatementsFromMaterializer = ({
|
|
58
63
|
}
|
59
64
|
|
60
65
|
const statementResults = fromMaterializerResult(
|
61
|
-
materializer(
|
66
|
+
materializer(eventDecoded.args, {
|
62
67
|
eventDef,
|
63
68
|
query,
|
64
69
|
// TODO properly implement this
|
65
70
|
currentFacts: new Map(),
|
71
|
+
event: eventDecoded,
|
66
72
|
}),
|
67
73
|
)
|
68
74
|
|
package/src/schema/EventDef.ts
CHANGED
@@ -4,6 +4,7 @@ import { Schema } from '@livestore/utils/effect'
|
|
4
4
|
|
5
5
|
import type { BindValues } from '../sql-queries/sql-queries.ts'
|
6
6
|
import type { ParamsObject } from '../util.ts'
|
7
|
+
import type * as LiveStoreEvent from './LiveStoreEvent.ts'
|
7
8
|
import type { QueryBuilder } from './state/sqlite/query-builder/mod.ts'
|
8
9
|
|
9
10
|
export type EventDefMap = {
|
@@ -191,6 +192,8 @@ export type Materializer<TEventDef extends EventDef.AnyWithoutFn = EventDef.AnyW
|
|
191
192
|
eventDef: TEventDef
|
192
193
|
/** Can be used to query the current state */
|
193
194
|
query: MaterializerContextQuery
|
195
|
+
/** The full LiveStore event with clientId, sessionId, etc. */
|
196
|
+
event: LiveStoreEvent.AnyDecoded
|
194
197
|
},
|
195
198
|
) => SingleOrReadonlyArray<MaterializerResult>
|
196
199
|
|