@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.
Files changed (148) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +4 -3
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js.map +1 -1
  5. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  6. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  7. package/dist/devtools/devtools-messages-leader.d.ts +24 -24
  8. package/dist/errors.d.ts +15 -4
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/errors.js +10 -2
  11. package/dist/errors.js.map +1 -1
  12. package/dist/leader-thread/LeaderSyncProcessor.d.ts +4 -3
  13. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  14. package/dist/leader-thread/LeaderSyncProcessor.js +43 -24
  15. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  16. package/dist/leader-thread/eventlog.d.ts +4 -10
  17. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  18. package/dist/leader-thread/eventlog.js +3 -5
  19. package/dist/leader-thread/eventlog.js.map +1 -1
  20. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  21. package/dist/leader-thread/leader-worker-devtools.js +1 -1
  22. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  23. package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
  24. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  25. package/dist/leader-thread/make-leader-thread-layer.js +34 -15
  26. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  27. package/dist/leader-thread/materialize-event.d.ts +2 -2
  28. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  29. package/dist/leader-thread/materialize-event.js +4 -6
  30. package/dist/leader-thread/materialize-event.js.map +1 -1
  31. package/dist/leader-thread/recreate-db.d.ts +2 -3
  32. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  33. package/dist/leader-thread/recreate-db.js +1 -1
  34. package/dist/leader-thread/recreate-db.js.map +1 -1
  35. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  36. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  37. package/dist/leader-thread/shutdown-channel.js +2 -2
  38. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  39. package/dist/leader-thread/types.d.ts +5 -5
  40. package/dist/leader-thread/types.d.ts.map +1 -1
  41. package/dist/materializer-helper.d.ts.map +1 -1
  42. package/dist/materializer-helper.js +8 -2
  43. package/dist/materializer-helper.js.map +1 -1
  44. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  45. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  46. package/dist/schema/EventDef.d.ts +3 -0
  47. package/dist/schema/EventDef.d.ts.map +1 -1
  48. package/dist/schema/EventDef.js.map +1 -1
  49. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  50. package/dist/schema/LiveStoreEvent.js +1 -2
  51. package/dist/schema/LiveStoreEvent.js.map +1 -1
  52. package/dist/schema/schema.js +1 -1
  53. package/dist/schema/schema.js.map +1 -1
  54. package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
  55. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  56. package/dist/schema/state/sqlite/client-document-def.js +93 -2
  57. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  58. package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
  59. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  60. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  61. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  62. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +21 -3
  63. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  64. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  65. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  66. package/dist/schema/state/sqlite/mod.js +1 -1
  67. package/dist/schema/state/sqlite/mod.js.map +1 -1
  68. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  69. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  70. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  71. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  72. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  73. package/dist/schema/state/sqlite/query-builder/impl.test.js +54 -0
  74. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  75. package/dist/schema/state/sqlite/system-tables.d.ts +36 -0
  76. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  77. package/dist/schema/state/sqlite/system-tables.js +2 -0
  78. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  79. package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -9
  80. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  81. package/dist/sync/ClientSessionSyncProcessor.js +17 -17
  82. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  83. package/dist/sync/errors.d.ts +61 -0
  84. package/dist/sync/errors.d.ts.map +1 -0
  85. package/dist/sync/errors.js +36 -0
  86. package/dist/sync/errors.js.map +1 -0
  87. package/dist/sync/index.d.ts +1 -0
  88. package/dist/sync/index.d.ts.map +1 -1
  89. package/dist/sync/index.js +1 -0
  90. package/dist/sync/index.js.map +1 -1
  91. package/dist/sync/mock-sync-backend.d.ts +14 -0
  92. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  93. package/dist/sync/mock-sync-backend.js +62 -0
  94. package/dist/sync/mock-sync-backend.js.map +1 -0
  95. package/dist/sync/next/history-dag.d.ts.map +1 -1
  96. package/dist/sync/next/history-dag.js +3 -1
  97. package/dist/sync/next/history-dag.js.map +1 -1
  98. package/dist/sync/sync-backend-kv.d.ts +7 -0
  99. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  100. package/dist/sync/sync-backend-kv.js +18 -0
  101. package/dist/sync/sync-backend-kv.js.map +1 -0
  102. package/dist/sync/sync-backend.d.ts +85 -0
  103. package/dist/sync/sync-backend.d.ts.map +1 -0
  104. package/dist/sync/sync-backend.js +24 -0
  105. package/dist/sync/sync-backend.js.map +1 -0
  106. package/dist/sync/sync.d.ts +6 -84
  107. package/dist/sync/sync.d.ts.map +1 -1
  108. package/dist/sync/sync.js +2 -27
  109. package/dist/sync/sync.js.map +1 -1
  110. package/dist/sync/validate-push-payload.d.ts +1 -1
  111. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  112. package/dist/sync/validate-push-payload.js +6 -6
  113. package/dist/sync/validate-push-payload.js.map +1 -1
  114. package/dist/version.d.ts +2 -2
  115. package/dist/version.js +2 -2
  116. package/package.json +4 -4
  117. package/src/adapter-types.ts +8 -3
  118. package/src/errors.ts +14 -3
  119. package/src/leader-thread/LeaderSyncProcessor.ts +79 -30
  120. package/src/leader-thread/eventlog.ts +9 -5
  121. package/src/leader-thread/leader-worker-devtools.ts +1 -1
  122. package/src/leader-thread/make-leader-thread-layer.ts +64 -22
  123. package/src/leader-thread/materialize-event.ts +5 -6
  124. package/src/leader-thread/recreate-db.ts +11 -3
  125. package/src/leader-thread/shutdown-channel.ts +16 -2
  126. package/src/leader-thread/types.ts +5 -5
  127. package/src/materializer-helper.ts +9 -3
  128. package/src/schema/EventDef.ts +3 -0
  129. package/src/schema/LiveStoreEvent.ts +1 -2
  130. package/src/schema/schema.ts +1 -1
  131. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  132. package/src/schema/state/sqlite/client-document-def.ts +115 -3
  133. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +24 -3
  134. package/src/schema/state/sqlite/mod.ts +1 -0
  135. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  136. package/src/schema/state/sqlite/query-builder/impl.test.ts +64 -0
  137. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  138. package/src/schema/state/sqlite/system-tables.ts +2 -0
  139. package/src/sync/ClientSessionSyncProcessor.ts +32 -32
  140. package/src/sync/errors.ts +38 -0
  141. package/src/sync/index.ts +1 -0
  142. package/src/sync/mock-sync-backend.ts +96 -0
  143. package/src/sync/next/history-dag.ts +3 -1
  144. package/src/sync/sync-backend-kv.ts +22 -0
  145. package/src/sync/sync-backend.ts +137 -0
  146. package/src/sync/sync.ts +6 -89
  147. package/src/sync/validate-push-payload.ts +6 -7
  148. 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
- import type { SqliteDb } from '../adapter-types.ts'
21
- import { SyncError, UnexpectedError } from '../adapter-types.ts'
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 { LeaderAheadError } from '../sync/sync.ts'
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 shutdownOnError = (cause: unknown) =>
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 === 'shutdown') {
230
- yield* shutdownChannel.send(UnexpectedError.make({ cause }))
231
- yield* Effect.die(cause)
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.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
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.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError))
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(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
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 SyncError({ cause: mergeResult.message })
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, UnexpectedError, LeaderThreadCtx>
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[], remaining: number) =>
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 SyncError({ cause: mergeResult.message })
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 (remaining === 0) {
716
+ if (pageInfo._tag === 'NoMore') {
675
717
  yield* localPushesLatch.open
676
718
  }
677
719
  })
678
720
 
679
- const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: initialBackendHead })
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, remaining }) =>
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
- remaining,
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
- cursor: {
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
- import type { BootStatus, MakeSqliteDb, SqliteDb, SqliteError } from '../adapter-types.ts'
6
- import { UnexpectedError } from '../adapter-types.ts'
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
- dbStateMissing,
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, remaining }) =>
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 (remaining === 0 && blockingDeferred !== undefined) {
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
- dbStateMissing,
322
+ migrationsReport,
275
323
  initialBlockingSyncContext,
276
324
  devtoolsOptions,
277
325
  }: {
278
- dbStateMissing: boolean
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 { dbEventlog, bootStatusQueue, syncProcessor, schema, materializeEvent, dbState } = yield* LeaderThreadCtx
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, UnexpectedError } from '../adapter-types.ts'
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, UnexpectedError> =>
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* UnexpectedError.make({
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 type { BootStatus, MigrationHooks, SqliteDb, SqliteError } from '../index.ts'
6
- import { migrateDb, rematerializeFromEventlog, UnexpectedError } from '../index.ts'
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 { IntentionalShutdownCause, SyncError, UnexpectedError } from '../index.ts'
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(IntentionalShutdownCause, UnexpectedError, SyncError) {}
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 { SqliteError } from '../errors.ts'
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
- cursor: EventSequenceNumber.EventSequenceNumber
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
- SqliteError | UnexpectedError
128
+ MaterializeError
129
129
  >
130
130
 
131
131
  export type InitialBlockingSyncContext = {
132
132
  blockingDeferred: Deferred.Deferred<void> | undefined
133
- update: (_: { remaining: number; processed: number }) => Effect.Effect<void>
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 eventArgsDecoded =
35
- event.decoded === undefined ? Schema.decodeUnknownSync(eventDef.schema)(event.encoded!.args) : event.decoded.args
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(eventArgsDecoded, {
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
 
@@ -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