@livestore/common 0.4.0-dev.3 → 0.4.0-dev.6
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 +17 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -3
- 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 +40 -19
- 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 +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 +30 -2
- 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 +3 -2
- 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 +23 -6
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.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/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 +56 -2
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
- 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/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js +2 -1
- package/dist/sql-queries/sql-query-builder.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 +22 -22
- 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 +24 -4
- 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 +70 -26
- 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 +3 -2
- package/src/schema/state/sqlite/client-document-def.ts +108 -2
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
- package/src/schema/state/sqlite/mod.ts +1 -0
- package/src/schema/state/sqlite/query-builder/impl.test.ts +66 -6
- package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
- package/src/schema/state/sqlite/system-tables.ts +2 -0
- package/src/sql-queries/sql-query-builder.ts +2 -1
- package/src/sync/ClientSessionSyncProcessor.ts +37 -37
- 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
package/src/errors.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Effect, Schema, Stream } from '@livestore/utils/effect'
|
1
|
+
import { Cause, Effect, Layer, Schema, Stream } from '@livestore/utils/effect'
|
2
2
|
|
3
3
|
export class UnexpectedError extends Schema.TaggedError<UnexpectedError>()('LiveStore.UnexpectedError', {
|
4
4
|
cause: Schema.Defect,
|
@@ -11,15 +11,30 @@ export class UnexpectedError extends Schema.TaggedError<UnexpectedError>()('Live
|
|
11
11
|
Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
|
12
12
|
)
|
13
13
|
|
14
|
+
static mapToUnexpectedErrorLayer = <A, E, R>(layer: Layer.Layer<A, E, R>) =>
|
15
|
+
layer.pipe(
|
16
|
+
Layer.catchAllCause((cause) =>
|
17
|
+
Cause.isFailType(cause) && Schema.is(UnexpectedError)(cause.error)
|
18
|
+
? Layer.fail(cause.error)
|
19
|
+
: Layer.fail(new UnexpectedError({ cause: cause })),
|
20
|
+
),
|
21
|
+
)
|
22
|
+
|
14
23
|
static mapToUnexpectedErrorStream = <A, E, R>(stream: Stream.Stream<A, E, R>) =>
|
15
24
|
stream.pipe(
|
16
25
|
Stream.mapError((cause) => (Schema.is(UnexpectedError)(cause) ? cause : new UnexpectedError({ cause }))),
|
17
26
|
)
|
18
27
|
}
|
19
28
|
|
20
|
-
export class
|
21
|
-
|
22
|
-
|
29
|
+
export class MaterializerHashMismatchError extends Schema.TaggedError<MaterializerHashMismatchError>()(
|
30
|
+
'LiveStore.MaterializerHashMismatchError',
|
31
|
+
{
|
32
|
+
eventName: Schema.String,
|
33
|
+
note: Schema.optionalWith(Schema.String, {
|
34
|
+
default: () => 'Please make sure your event materializer is a pure function without side effects.',
|
35
|
+
}),
|
36
|
+
},
|
37
|
+
) {}
|
23
38
|
|
24
39
|
export class IntentionalShutdownCause extends Schema.TaggedError<IntentionalShutdownCause>()(
|
25
40
|
'LiveStore.IntentionalShutdownCause',
|
@@ -47,3 +62,8 @@ export class SqliteError extends Schema.TaggedError<SqliteError>()('LiveStore.Sq
|
|
47
62
|
cause: Schema.Defect,
|
48
63
|
note: Schema.optional(Schema.String),
|
49
64
|
}) {}
|
65
|
+
|
66
|
+
export class MaterializeError extends Schema.TaggedError<MaterializeError>()('LiveStore.MaterializeError', {
|
67
|
+
cause: Schema.Union(MaterializerHashMismatchError, SqliteError),
|
68
|
+
note: Schema.optional(Schema.String),
|
69
|
+
}) {}
|
@@ -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
|
-
import { shouldNeverHappen } from '@livestore/utils'
|
1
|
+
import { omitUndefineds, 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,18 +127,29 @@ 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
|
-
|
97
|
-
|
146
|
+
...omitUndefineds({
|
147
|
+
localPushBatchSize: params?.localPushBatchSize,
|
148
|
+
backendPushBatchSize: params?.backendPushBatchSize,
|
149
|
+
}),
|
98
150
|
},
|
99
151
|
testing: {
|
100
|
-
delays: testing?.syncProcessor?.delays,
|
152
|
+
...omitUndefineds({ delays: testing?.syncProcessor?.delays }),
|
101
153
|
},
|
102
154
|
})
|
103
155
|
|
@@ -113,8 +165,6 @@ export const makeLeaderThreadLayer = ({
|
|
113
165
|
}
|
114
166
|
: { enabled: false as const }
|
115
167
|
|
116
|
-
const materializeEvent = yield* makeMaterializeEvent({ schema, dbState, dbEventlog })
|
117
|
-
|
118
168
|
const ctx = {
|
119
169
|
schema,
|
120
170
|
bootStatusQueue,
|
@@ -141,7 +191,7 @@ export const makeLeaderThreadLayer = ({
|
|
141
191
|
const layer = Layer.succeed(LeaderThreadCtx, ctx)
|
142
192
|
|
143
193
|
ctx.initialState = yield* bootLeaderThread({
|
144
|
-
|
194
|
+
migrationsReport,
|
145
195
|
initialBlockingSyncContext,
|
146
196
|
devtoolsOptions,
|
147
197
|
}).pipe(Effect.provide(layer))
|
@@ -244,12 +294,12 @@ const makeInitialBlockingSyncContext = ({
|
|
244
294
|
|
245
295
|
return {
|
246
296
|
blockingDeferred,
|
247
|
-
update: ({ processed,
|
297
|
+
update: ({ processed, pageInfo }) =>
|
248
298
|
Effect.gen(function* () {
|
249
299
|
if (ctx.isDone === true) return
|
250
300
|
|
251
|
-
if (ctx.total === -1) {
|
252
|
-
ctx.total = remaining + processed
|
301
|
+
if (ctx.total === -1 && pageInfo._tag === 'MoreKnown') {
|
302
|
+
ctx.total = pageInfo.remaining + processed
|
253
303
|
}
|
254
304
|
|
255
305
|
ctx.processedEvents += processed
|
@@ -258,7 +308,7 @@ const makeInitialBlockingSyncContext = ({
|
|
258
308
|
progress: { done: ctx.processedEvents, total: ctx.total },
|
259
309
|
})
|
260
310
|
|
261
|
-
if (
|
311
|
+
if (pageInfo._tag === 'NoMore' && blockingDeferred !== undefined) {
|
262
312
|
yield* Deferred.succeed(blockingDeferred, void 0)
|
263
313
|
ctx.isDone = true
|
264
314
|
}
|
@@ -271,26 +321,20 @@ const makeInitialBlockingSyncContext = ({
|
|
271
321
|
* It also starts various background processes (e.g. syncing)
|
272
322
|
*/
|
273
323
|
const bootLeaderThread = ({
|
274
|
-
|
324
|
+
migrationsReport,
|
275
325
|
initialBlockingSyncContext,
|
276
326
|
devtoolsOptions,
|
277
327
|
}: {
|
278
|
-
|
328
|
+
migrationsReport: MigrationsReport
|
279
329
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
280
330
|
devtoolsOptions: DevtoolsOptions
|
281
331
|
}): Effect.Effect<
|
282
332
|
LeaderThreadCtx['Type']['initialState'],
|
283
|
-
UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
|
333
|
+
UnexpectedError | SqliteError | IsOfflineError | InvalidPullError | MaterializerHashMismatchError,
|
284
334
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
285
335
|
> =>
|
286
336
|
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: [] } }
|
337
|
+
const { bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
|
294
338
|
|
295
339
|
// NOTE the sync processor depends on the dbs being initialized properly
|
296
340
|
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.
|