@livestore/common 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230 → 0.0.0-snapshot-7bcbc24bb8873481e482d982636957f0c1f791f6

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 (139) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +6 -6
  3. package/dist/adapter-types.d.ts +39 -16
  4. package/dist/adapter-types.d.ts.map +1 -1
  5. package/dist/adapter-types.js +12 -0
  6. package/dist/adapter-types.js.map +1 -1
  7. package/dist/derived-mutations.js +3 -3
  8. package/dist/derived-mutations.js.map +1 -1
  9. package/dist/devtools/devtools-messages-client-session.d.ts +23 -23
  10. package/dist/devtools/devtools-messages-common.d.ts +19 -6
  11. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  12. package/dist/devtools/devtools-messages-common.js +14 -0
  13. package/dist/devtools/devtools-messages-common.js.map +1 -1
  14. package/dist/devtools/devtools-messages-leader.d.ts +94 -75
  15. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  16. package/dist/devtools/devtools-messages-leader.js +18 -17
  17. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  23. package/dist/leader-thread/LeaderSyncProcessor.js +50 -35
  24. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  25. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  26. package/dist/leader-thread/apply-mutation.js +8 -6
  27. package/dist/leader-thread/apply-mutation.js.map +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  29. package/dist/leader-thread/leader-worker-devtools.js +26 -18
  30. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  31. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.js +23 -6
  33. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  34. package/dist/leader-thread/mutationlog.d.ts +1 -1
  35. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  36. package/dist/leader-thread/mutationlog.js +7 -5
  37. package/dist/leader-thread/mutationlog.js.map +1 -1
  38. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  39. package/dist/leader-thread/recreate-db.d.ts +4 -2
  40. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  41. package/dist/leader-thread/recreate-db.js +12 -7
  42. package/dist/leader-thread/recreate-db.js.map +1 -1
  43. package/dist/leader-thread/types.d.ts +19 -7
  44. package/dist/leader-thread/types.d.ts.map +1 -1
  45. package/dist/leader-thread/types.js +1 -1
  46. package/dist/leader-thread/types.js.map +1 -1
  47. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  48. package/dist/rehydrate-from-mutationlog.js +8 -6
  49. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  50. package/dist/schema/EventId.d.ts +10 -9
  51. package/dist/schema/EventId.d.ts.map +1 -1
  52. package/dist/schema/EventId.js +14 -11
  53. package/dist/schema/EventId.js.map +1 -1
  54. package/dist/schema/EventId.test.js +3 -3
  55. package/dist/schema/EventId.test.js.map +1 -1
  56. package/dist/schema/MutationEvent.d.ts +37 -12
  57. package/dist/schema/MutationEvent.d.ts.map +1 -1
  58. package/dist/schema/MutationEvent.js +20 -4
  59. package/dist/schema/MutationEvent.js.map +1 -1
  60. package/dist/schema/mutations.d.ts +4 -8
  61. package/dist/schema/mutations.d.ts.map +1 -1
  62. package/dist/schema/mutations.js +2 -2
  63. package/dist/schema/mutations.js.map +1 -1
  64. package/dist/schema/system-tables.d.ts +38 -20
  65. package/dist/schema/system-tables.d.ts.map +1 -1
  66. package/dist/schema/system-tables.js +9 -6
  67. package/dist/schema/system-tables.js.map +1 -1
  68. package/dist/schema/table-def.d.ts +7 -7
  69. package/dist/schema/table-def.d.ts.map +1 -1
  70. package/dist/schema/table-def.js +2 -2
  71. package/dist/schema/table-def.js.map +1 -1
  72. package/dist/schema-management/migrations.d.ts +2 -2
  73. package/dist/schema-management/migrations.d.ts.map +1 -1
  74. package/dist/schema-management/migrations.js +6 -1
  75. package/dist/schema-management/migrations.js.map +1 -1
  76. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  77. package/dist/sync/ClientSessionSyncProcessor.js +20 -5
  78. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  79. package/dist/sync/next/facts.js +1 -1
  80. package/dist/sync/next/facts.js.map +1 -1
  81. package/dist/sync/next/history-dag-common.d.ts +2 -0
  82. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  83. package/dist/sync/next/history-dag-common.js +3 -1
  84. package/dist/sync/next/history-dag-common.js.map +1 -1
  85. package/dist/sync/next/history-dag.d.ts.map +1 -1
  86. package/dist/sync/next/history-dag.js +1 -1
  87. package/dist/sync/next/history-dag.js.map +1 -1
  88. package/dist/sync/next/rebase-events.d.ts +3 -1
  89. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  90. package/dist/sync/next/rebase-events.js +5 -3
  91. package/dist/sync/next/rebase-events.js.map +1 -1
  92. package/dist/sync/next/test/compact-events.calculator.test.js +12 -12
  93. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
  94. package/dist/sync/next/test/compact-events.test.js +30 -30
  95. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  96. package/dist/sync/next/test/mutation-fixtures.d.ts +1 -1
  97. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  98. package/dist/sync/next/test/mutation-fixtures.js +9 -7
  99. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  100. package/dist/sync/sync.d.ts +2 -2
  101. package/dist/sync/syncstate.d.ts +9 -9
  102. package/dist/sync/syncstate.js +6 -6
  103. package/dist/sync/syncstate.js.map +1 -1
  104. package/dist/sync/syncstate.test.js +18 -16
  105. package/dist/sync/syncstate.test.js.map +1 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +3 -3
  109. package/src/adapter-types.ts +33 -15
  110. package/src/derived-mutations.ts +3 -3
  111. package/src/devtools/devtools-messages-common.ts +34 -0
  112. package/src/devtools/devtools-messages-leader.ts +28 -18
  113. package/src/index.ts +1 -1
  114. package/src/leader-thread/LeaderSyncProcessor.ts +59 -38
  115. package/src/leader-thread/apply-mutation.ts +15 -5
  116. package/src/leader-thread/leader-worker-devtools.ts +33 -19
  117. package/src/leader-thread/make-leader-thread-layer.ts +28 -8
  118. package/src/leader-thread/mutationlog.ts +8 -6
  119. package/src/leader-thread/recreate-db.ts +18 -9
  120. package/src/leader-thread/types.ts +20 -5
  121. package/src/rehydrate-from-mutationlog.ts +8 -6
  122. package/src/schema/EventId.test.ts +3 -3
  123. package/src/schema/EventId.ts +20 -16
  124. package/src/schema/MutationEvent.ts +31 -6
  125. package/src/schema/mutations.ts +6 -19
  126. package/src/schema/system-tables.ts +9 -6
  127. package/src/schema/table-def.ts +8 -8
  128. package/src/schema-management/migrations.ts +9 -5
  129. package/src/sync/ClientSessionSyncProcessor.ts +25 -6
  130. package/src/sync/next/facts.ts +1 -1
  131. package/src/sync/next/history-dag-common.ts +5 -1
  132. package/src/sync/next/history-dag.ts +1 -1
  133. package/src/sync/next/rebase-events.ts +8 -2
  134. package/src/sync/next/test/compact-events.calculator.test.ts +12 -12
  135. package/src/sync/next/test/compact-events.test.ts +30 -30
  136. package/src/sync/next/test/mutation-fixtures.ts +10 -6
  137. package/src/sync/syncstate.test.ts +19 -17
  138. package/src/sync/syncstate.ts +6 -6
  139. package/src/version.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { isNotUndefined, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
1
+ import { isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
2
2
  import type { HttpClient, Scope, Tracer } from '@livestore/utils/effect'
3
3
  import {
4
4
  BucketQueue,
@@ -32,7 +32,7 @@ import * as SyncState from '../sync/syncstate.js'
32
32
  import { sql } from '../util.js'
33
33
  import { makeApplyMutation } from './apply-mutation.js'
34
34
  import { execSql } from './connection.js'
35
- import { getBackendHeadFromDb, getLocalHeadFromDb, getMutationEventsSince, updateBackendHead } from './mutationlog.js'
35
+ import { getBackendHeadFromDb, getClientHeadFromDb, getMutationEventsSince, updateBackendHead } from './mutationlog.js'
36
36
  import type { InitialBlockingSyncContext, InitialSyncInfo, LeaderSyncProcessor } from './types.js'
37
37
  import { LeaderThreadCtx } from './types.js'
38
38
 
@@ -83,7 +83,7 @@ export const makeLeaderSyncProcessor = ({
83
83
 
84
84
  const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
85
85
  const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
86
- return mutationDef.options.localOnly
86
+ return mutationDef.options.clientOnly
87
87
  }
88
88
 
89
89
  // This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
@@ -93,8 +93,7 @@ export const makeLeaderSyncProcessor = ({
93
93
  | {
94
94
  otelSpan: otel.Span | undefined
95
95
  span: Tracer.Span
96
- devtoolsPullLatch: Effect.Latch | undefined
97
- devtoolsPushLatch: Effect.Latch | undefined
96
+ devtoolsLatch: Effect.Latch | undefined
98
97
  },
99
98
  }
100
99
 
@@ -107,10 +106,6 @@ export const makeLeaderSyncProcessor = ({
107
106
  // TODO validate batch
108
107
  if (newEvents.length === 0) return
109
108
 
110
- if (ctxRef.current?.devtoolsPushLatch !== undefined) {
111
- yield* ctxRef.current.devtoolsPushLatch.await
112
- }
113
-
114
109
  const waitForProcessing = options?.waitForProcessing ?? false
115
110
 
116
111
  if (waitForProcessing) {
@@ -137,18 +132,24 @@ export const makeLeaderSyncProcessor = ({
137
132
  }),
138
133
  )
139
134
 
140
- const pushPartial: LeaderSyncProcessor['pushPartial'] = (mutationEventEncoded_) =>
135
+ const pushPartial: LeaderSyncProcessor['pushPartial'] = ({
136
+ mutationEvent: partialMutationEvent,
137
+ clientId,
138
+ sessionId,
139
+ }) =>
141
140
  Effect.gen(function* () {
142
141
  const syncState = yield* syncStateSref
143
142
  if (syncState === undefined) return shouldNeverHappen('Not initialized')
144
143
 
145
144
  const mutationDef =
146
- schema.mutations.get(mutationEventEncoded_.mutation) ??
147
- shouldNeverHappen(`Unknown mutation: ${mutationEventEncoded_.mutation}`)
145
+ schema.mutations.get(partialMutationEvent.mutation) ??
146
+ shouldNeverHappen(`Unknown mutation: ${partialMutationEvent.mutation}`)
148
147
 
149
148
  const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
150
- ...mutationEventEncoded_,
151
- ...EventId.nextPair(syncState.localHead, mutationDef.options.localOnly),
149
+ ...partialMutationEvent,
150
+ clientId,
151
+ sessionId,
152
+ ...EventId.nextPair(syncState.localHead, mutationDef.options.clientOnly),
152
153
  })
153
154
 
154
155
  yield* push([mutationEventEncoded])
@@ -164,12 +165,11 @@ export const makeLeaderSyncProcessor = ({
164
165
  ctxRef.current = {
165
166
  otelSpan,
166
167
  span,
167
- devtoolsPullLatch: devtools.enabled ? devtools.syncBackendPullLatch : undefined,
168
- devtoolsPushLatch: devtools.enabled ? devtools.syncBackendPushLatch : undefined,
168
+ devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
169
169
  }
170
170
 
171
171
  const initialBackendHead = dbMissing ? EventId.ROOT.global : getBackendHeadFromDb(dbMutationLog)
172
- const initialLocalHead = dbMissing ? EventId.ROOT : getLocalHeadFromDb(dbMutationLog)
172
+ const initialLocalHead = dbMissing ? EventId.ROOT : getClientHeadFromDb(dbMutationLog)
173
173
 
174
174
  if (initialBackendHead > initialLocalHead.global) {
175
175
  return shouldNeverHappen(
@@ -179,14 +179,14 @@ export const makeLeaderSyncProcessor = ({
179
179
 
180
180
  const pendingMutationEvents = yield* getMutationEventsSince({
181
181
  global: initialBackendHead,
182
- local: EventId.localDefault,
182
+ client: EventId.clientDefault,
183
183
  }).pipe(Effect.map(ReadonlyArray.map((_) => new MutationEvent.EncodedWithMeta(_))))
184
184
 
185
185
  const initialSyncState = new SyncState.SyncState({
186
186
  pending: pendingMutationEvents,
187
187
  // On the leader we don't need a rollback tail beyond `pending` items
188
188
  rollbackTail: [],
189
- upstreamHead: { global: initialBackendHead, local: EventId.localDefault },
189
+ upstreamHead: { global: initialBackendHead, client: EventId.clientDefault },
190
190
  localHead: initialLocalHead,
191
191
  })
192
192
 
@@ -196,10 +196,10 @@ export const makeLeaderSyncProcessor = ({
196
196
  // Rehydrate sync queue
197
197
  if (pendingMutationEvents.length > 0) {
198
198
  const filteredBatch = pendingMutationEvents
199
- // Don't sync localOnly mutations
199
+ // Don't sync clientOnly mutations
200
200
  .filter((mutationEventEncoded) => {
201
201
  const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
202
- return mutationDef.options.localOnly === false
202
+ return mutationDef.options.clientOnly === false
203
203
  })
204
204
 
205
205
  yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
@@ -220,7 +220,12 @@ export const makeLeaderSyncProcessor = ({
220
220
 
221
221
  yield* FiberHandle.run(
222
222
  backendPushingFiberHandle,
223
- backgroundBackendPushing({ dbReady, syncBackendQueue, otelSpan }).pipe(Effect.tapCauseLogPretty),
223
+ backgroundBackendPushing({
224
+ dbReady,
225
+ syncBackendQueue,
226
+ otelSpan,
227
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
228
+ }).pipe(Effect.tapCauseLogPretty),
224
229
  )
225
230
 
226
231
  yield* backgroundBackendPulling({
@@ -239,7 +244,12 @@ export const makeLeaderSyncProcessor = ({
239
244
  // Restart pushing fiber
240
245
  yield* FiberHandle.run(
241
246
  backendPushingFiberHandle,
242
- backgroundBackendPushing({ dbReady, syncBackendQueue, otelSpan }).pipe(Effect.tapCauseLogPretty),
247
+ backgroundBackendPushing({
248
+ dbReady,
249
+ syncBackendQueue,
250
+ otelSpan,
251
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
252
+ }).pipe(Effect.tapCauseLogPretty),
243
253
  )
244
254
  }),
245
255
  syncStateSref,
@@ -247,8 +257,10 @@ export const makeLeaderSyncProcessor = ({
247
257
  pullLatch,
248
258
  otelSpan,
249
259
  initialBlockingSyncContext,
250
- devtoolsPullLatch: ctxRef.current?.devtoolsPullLatch,
260
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
251
261
  }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
262
+
263
+ return { initialLeaderHead: initialLocalHead }
252
264
  }).pipe(Effect.withSpanScoped('@livestore/common:leader-thread:syncing'))
253
265
 
254
266
  return {
@@ -357,10 +369,10 @@ const backgroundApplyLocalPushes = ({
357
369
  updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
358
370
  })
359
371
 
360
- // Don't sync localOnly mutations
372
+ // Don't sync clientOnly mutations
361
373
  const filteredBatch = updateResult.newEvents.filter((mutationEventEncoded) => {
362
374
  const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
363
- return mutationDef.options.localOnly === false
375
+ return mutationDef.options.clientOnly === false
364
376
  })
365
377
 
366
378
  yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
@@ -431,7 +443,7 @@ const backgroundBackendPulling = ({
431
443
  syncStateSref,
432
444
  localPushesLatch,
433
445
  pullLatch,
434
- devtoolsPullLatch,
446
+ devtoolsLatch,
435
447
  initialBlockingSyncContext,
436
448
  }: {
437
449
  dbReady: Deferred.Deferred<void>
@@ -444,7 +456,7 @@ const backgroundBackendPulling = ({
444
456
  syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
445
457
  localPushesLatch: Effect.Latch
446
458
  pullLatch: Effect.Latch
447
- devtoolsPullLatch: Effect.Latch | undefined
459
+ devtoolsLatch: Effect.Latch | undefined
448
460
  initialBlockingSyncContext: InitialBlockingSyncContext
449
461
  }) =>
450
462
  Effect.gen(function* () {
@@ -466,8 +478,8 @@ const backgroundBackendPulling = ({
466
478
  Effect.gen(function* () {
467
479
  if (newEvents.length === 0) return
468
480
 
469
- if (devtoolsPullLatch !== undefined) {
470
- yield* devtoolsPullLatch.await
481
+ if (devtoolsLatch !== undefined) {
482
+ yield* devtoolsLatch.await
471
483
  }
472
484
 
473
485
  // Prevent more local pushes from being processed until this pull is finished
@@ -507,7 +519,7 @@ const backgroundBackendPulling = ({
507
519
 
508
520
  const filteredRebasedPending = updateResult.newSyncState.pending.filter((mutationEvent) => {
509
521
  const mutationDef = schema.mutations.get(mutationEvent.mutation)!
510
- return mutationDef.options.localOnly === false
522
+ return mutationDef.options.clientOnly === false
511
523
  })
512
524
  yield* restartBackendPushing(filteredRebasedPending)
513
525
 
@@ -592,9 +604,9 @@ const rollback = ({
592
604
  Effect.gen(function* () {
593
605
  const rollbackEvents = db
594
606
  .select<SessionChangesetMetaRow>(
595
- sql`SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idLocal) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.local})`).join(', ')})`,
607
+ sql`SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
596
608
  )
597
- .map((_) => ({ id: { global: _.idGlobal, local: _.idLocal }, changeset: _.changeset, debug: _.debug }))
609
+ .map((_) => ({ id: { global: _.idGlobal, client: _.idClient }, changeset: _.changeset, debug: _.debug }))
598
610
  .toSorted((a, b) => EventId.compare(a.id, b.id))
599
611
 
600
612
  // Apply changesets in reverse order
@@ -607,12 +619,12 @@ const rollback = ({
607
619
 
608
620
  // Delete the changeset rows
609
621
  db.execute(
610
- sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idLocal) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.local})`).join(', ')})`,
622
+ sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
611
623
  )
612
624
 
613
625
  // Delete the mutation log rows
614
626
  dbMutationLog.execute(
615
- sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idLocal) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.local})`).join(', ')})`,
627
+ sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
616
628
  )
617
629
  }).pipe(
618
630
  Effect.withSpan('@livestore/common:leader-thread:syncing:rollback', {
@@ -632,12 +644,12 @@ const getCursorInfo = (remoteHead: EventId.GlobalEventId) =>
632
644
 
633
645
  const syncMetadataOption = yield* Effect.sync(() =>
634
646
  dbMutationLog.select<{ syncMetadataJson: string }>(
635
- sql`SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idLocal ASC LIMIT 1`,
647
+ sql`SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idClient ASC LIMIT 1`,
636
648
  ),
637
649
  ).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
638
650
 
639
651
  return Option.some({
640
- cursor: { global: remoteHead, local: EventId.localDefault },
652
+ cursor: { global: remoteHead, client: EventId.clientDefault },
641
653
  metadata: syncMetadataOption,
642
654
  }) satisfies InitialSyncInfo
643
655
  }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:getCursorInfo', { attributes: { remoteHead } }))
@@ -646,10 +658,12 @@ const backgroundBackendPushing = ({
646
658
  dbReady,
647
659
  syncBackendQueue,
648
660
  otelSpan,
661
+ devtoolsLatch,
649
662
  }: {
650
663
  dbReady: Deferred.Deferred<void>
651
664
  syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
652
665
  otelSpan: otel.Span | undefined
666
+ devtoolsLatch: Effect.Latch | undefined
653
667
  }) =>
654
668
  Effect.gen(function* () {
655
669
  const { syncBackend, dbMutationLog } = yield* LeaderThreadCtx
@@ -665,6 +679,10 @@ const backgroundBackendPushing = ({
665
679
 
666
680
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
667
681
 
682
+ if (devtoolsLatch !== undefined) {
683
+ yield* devtoolsLatch.await
684
+ }
685
+
668
686
  otelSpan?.addEvent('backend-push', {
669
687
  batchSize: queueItems.length,
670
688
  batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
@@ -674,6 +692,9 @@ const backgroundBackendPushing = ({
674
692
  const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
675
693
 
676
694
  if (pushResult._tag === 'Left') {
695
+ if (LS_DEV) {
696
+ yield* Effect.logDebug('backend-push-error', { error: pushResult.left.toString() })
697
+ }
677
698
  otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
678
699
  // wait for interrupt caused by background pulling which will then restart pushing
679
700
  return yield* Effect.never
@@ -689,7 +710,7 @@ const backgroundBackendPushing = ({
689
710
  ...updateRows({
690
711
  tableName: MUTATION_LOG_META_TABLE,
691
712
  columns: mutationLogMetaTable.sqliteDef.columns,
692
- where: { idGlobal: mutationEventEncoded.id.global, idLocal: mutationEventEncoded.id.local },
713
+ where: { idGlobal: mutationEventEncoded.id.global, idClient: mutationEventEncoded.id.client },
693
714
  updateValues: { syncMetadataJson: metadata[i]! },
694
715
  }),
695
716
  )
@@ -79,7 +79,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
79
79
  columns: sessionChangesetMetaTable.sqliteDef.columns,
80
80
  values: {
81
81
  idGlobal: mutationEventEncoded.id.global,
82
- idLocal: mutationEventEncoded.id.local,
82
+ idClient: mutationEventEncoded.id.client,
83
83
  // NOTE the changeset will be empty (i.e. null) for no-op mutations
84
84
  changeset: changeset ?? null,
85
85
  debug: execArgsArr,
@@ -92,7 +92,13 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
92
92
  // write to mutation_log
93
93
  const excludeFromMutationLog = shouldExcludeMutationFromLog(mutationName, mutationEventEncoded)
94
94
  if (skipMutationLog === false && excludeFromMutationLog === false) {
95
- yield* insertIntoMutationLog(mutationEventEncoded, dbMutationLog, mutationDefSchemaHashMap)
95
+ yield* insertIntoMutationLog(
96
+ mutationEventEncoded,
97
+ dbMutationLog,
98
+ mutationDefSchemaHashMap,
99
+ mutationEventEncoded.clientId,
100
+ mutationEventEncoded.sessionId,
101
+ )
96
102
  } else {
97
103
  // console.debug('[@livestore/common:leader-thread] skipping mutation log write', mutation, statementSql, bindValues)
98
104
  }
@@ -101,7 +107,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
101
107
  attributes: {
102
108
  mutationName: mutationEventEncoded.mutation,
103
109
  mutationId: mutationEventEncoded.id,
104
- 'span.label': `(${mutationEventEncoded.id.global},${mutationEventEncoded.id.local}) ${mutationEventEncoded.mutation}`,
110
+ 'span.label': `(${mutationEventEncoded.id.global},${mutationEventEncoded.id.client}) ${mutationEventEncoded.mutation}`,
105
111
  },
106
112
  }),
107
113
  // Effect.logDuration('@livestore/common:leader-thread:applyMutation'),
@@ -113,6 +119,8 @@ const insertIntoMutationLog = (
113
119
  mutationEventEncoded: MutationEvent.AnyEncoded,
114
120
  dbMutationLog: SqliteDb,
115
121
  mutationDefSchemaHashMap: Map<string, number>,
122
+ clientId: string,
123
+ sessionId: string | undefined,
116
124
  ) =>
117
125
  Effect.gen(function* () {
118
126
  const mutationName = mutationEventEncoded.mutation
@@ -127,11 +135,13 @@ const insertIntoMutationLog = (
127
135
  columns: mutationLogMetaTable.sqliteDef.columns,
128
136
  values: {
129
137
  idGlobal: mutationEventEncoded.id.global,
130
- idLocal: mutationEventEncoded.id.local,
138
+ idClient: mutationEventEncoded.id.client,
131
139
  parentIdGlobal: mutationEventEncoded.parentId.global,
132
- parentIdLocal: mutationEventEncoded.parentId.local,
140
+ parentIdClient: mutationEventEncoded.parentId.client,
133
141
  mutation: mutationEventEncoded.mutation,
134
142
  argsJson: mutationEventEncoded.args ?? {},
143
+ clientId,
144
+ sessionId: sessionId ?? null,
135
145
  schemaHash: mutationDefSchemaHash,
136
146
  syncMetadataJson: Option.none(),
137
147
  },
@@ -38,18 +38,7 @@ export const bootDevtools = (options: DevtoolsOptions) =>
38
38
  const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(localHead)
39
39
 
40
40
  yield* Stream.fromQueue(pullQueue).pipe(
41
- Stream.tap((msg) =>
42
- Effect.gen(function* () {
43
- if (msg.payload._tag === 'upstream-advance') {
44
- for (const mutationEventEncoded of msg.payload.newEvents) {
45
- // TODO refactor with push semantics
46
- yield* sendMessage(Devtools.Leader.MutationBroadcast.make({ mutationEventEncoded, liveStoreVersion }))
47
- }
48
- } else {
49
- yield* Effect.logWarning('TODO implement rebases in devtools')
50
- }
51
- }),
52
- ),
41
+ Stream.tap((msg) => sendMessage(Devtools.Leader.SyncPull.make({ payload: msg.payload, liveStoreVersion }))),
53
42
  Stream.runDrain,
54
43
  Effect.forkScoped,
55
44
  )
@@ -80,6 +69,7 @@ const listenToDevtools = ({
80
69
  shutdownChannel,
81
70
  syncProcessor,
82
71
  clientId,
72
+ devtools,
83
73
  } = yield* LeaderThreadCtx
84
74
 
85
75
  type RequestId = string
@@ -158,7 +148,7 @@ const listenToDevtools = ({
158
148
 
159
149
  return
160
150
  }
161
- case 'LSD.Leader.ResetAllDataReq': {
151
+ case 'LSD.Leader.ResetAllData.Request': {
162
152
  const { mode } = decodedEvent
163
153
 
164
154
  yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
@@ -169,7 +159,7 @@ const listenToDevtools = ({
169
159
  dbMutationLog.destroy()
170
160
  }
171
161
 
172
- yield* sendMessage(Devtools.Leader.ResetAllDataRes.make({ ...reqPayload }))
162
+ yield* sendMessage(Devtools.Leader.ResetAllData.Response.make({ ...reqPayload }))
173
163
 
174
164
  yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-reset' })) ?? Effect.void
175
165
 
@@ -203,7 +193,11 @@ const listenToDevtools = ({
203
193
  return
204
194
  }
205
195
  case 'LSD.Leader.RunMutationReq': {
206
- yield* syncProcessor.pushPartial(decodedEvent.mutationEventEncoded)
196
+ yield* syncProcessor.pushPartial({
197
+ mutationEvent: decodedEvent.mutationEventEncoded,
198
+ clientId: `devtools-${clientId}`,
199
+ sessionId: undefined,
200
+ })
207
201
 
208
202
  yield* sendMessage(Devtools.Leader.RunMutationRes.make({ ...reqPayload }))
209
203
 
@@ -257,11 +251,14 @@ const listenToDevtools = ({
257
251
  // This is probably the same "flaky databrowser loading" bug as we're seeing in the playwright tests
258
252
  yield* Effect.sleep(1000)
259
253
 
260
- yield* syncBackend.isConnected.changes.pipe(
261
- Stream.tap((isConnected) =>
254
+ yield* Stream.zipLatest(
255
+ syncBackend.isConnected.changes,
256
+ devtools.enabled ? devtools.syncBackendLatchState.changes : Stream.make({ latchClosed: false }),
257
+ ).pipe(
258
+ Stream.tap(([isConnected, { latchClosed }]) =>
262
259
  sendMessage(
263
260
  Devtools.Leader.NetworkStatusRes.make({
264
- networkStatus: { isConnected, timestampMs: Date.now() },
261
+ networkStatus: { isConnected, timestampMs: Date.now(), latchClosed },
265
262
  ...reqPayload,
266
263
  }),
267
264
  ),
@@ -310,8 +307,25 @@ const listenToDevtools = ({
310
307
 
311
308
  return
312
309
  }
310
+ case 'LSD.Leader.SetSyncLatch.Request': {
311
+ const { closeLatch } = decodedEvent
312
+
313
+ if (devtools.enabled === false) return
314
+
315
+ if (closeLatch === true) {
316
+ yield* devtools.syncBackendLatch.close
317
+ } else {
318
+ yield* devtools.syncBackendLatch.open
319
+ }
320
+
321
+ yield* SubscriptionRef.set(devtools.syncBackendLatchState, { latchClosed: closeLatch })
322
+
323
+ yield* sendMessage(Devtools.Leader.SetSyncLatch.Response.make({ ...reqPayload }))
324
+
325
+ return
326
+ }
313
327
  default: {
314
- yield* Effect.logWarning(`TODO implement ${decodedEvent._tag}`, decodedEvent)
328
+ yield* Effect.logWarning(`TODO implement devtools message`, decodedEvent)
315
329
  }
316
330
  }
317
331
  }).pipe(Effect.withSpan(`@livestore/common:leader-thread:onDevtoolsMessage:${decodedEvent._tag}`)),
@@ -1,7 +1,7 @@
1
1
  import type { HttpClient, Scope } from '@livestore/utils/effect'
2
2
  import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
3
3
 
4
- import type { BootStatus, MakeSqliteDb, SqliteError } from '../adapter-types.js'
4
+ import type { BootStatus, MakeSqliteDb, MigrationsReport, SqliteError } from '../adapter-types.js'
5
5
  import { UnexpectedError } from '../adapter-types.js'
6
6
  import type * as Devtools from '../devtools/index.js'
7
7
  import type { LiveStoreSchema } from '../schema/mod.js'
@@ -74,8 +74,8 @@ export const makeLeaderThreadLayer = ({
74
74
  const devtoolsContext = devtoolsOptions.enabled
75
75
  ? {
76
76
  enabled: true as const,
77
- syncBackendPullLatch: yield* Effect.makeLatch(true),
78
- syncBackendPushLatch: yield* Effect.makeLatch(true),
77
+ syncBackendLatch: yield* Effect.makeLatch(true),
78
+ syncBackendLatchState: yield* SubscriptionRef.make<{ latchClosed: boolean }>({ latchClosed: false }),
79
79
  }
80
80
  : { enabled: false as const }
81
81
 
@@ -95,6 +95,8 @@ export const makeLeaderThreadLayer = ({
95
95
  connectedClientSessionPullQueues: yield* makePullQueueSet,
96
96
  extraIncomingMessagesQueue,
97
97
  devtools: devtoolsContext,
98
+ // State will be set during `bootLeaderThread`
99
+ initialState: {} as any as LeaderThreadCtx['Type']['initialState'],
98
100
  } satisfies typeof LeaderThreadCtx.Service
99
101
 
100
102
  // @ts-expect-error For debugging purposes
@@ -102,7 +104,11 @@ export const makeLeaderThreadLayer = ({
102
104
 
103
105
  const layer = Layer.succeed(LeaderThreadCtx, ctx)
104
106
 
105
- yield* bootLeaderThread({ dbMissing, initialBlockingSyncContext, devtoolsOptions }).pipe(Effect.provide(layer))
107
+ ctx.initialState = yield* bootLeaderThread({
108
+ dbMissing,
109
+ initialBlockingSyncContext,
110
+ devtoolsOptions,
111
+ }).pipe(Effect.provide(layer))
106
112
 
107
113
  return layer
108
114
  }).pipe(
@@ -172,7 +178,7 @@ const bootLeaderThread = ({
172
178
  initialBlockingSyncContext: InitialBlockingSyncContext
173
179
  devtoolsOptions: DevtoolsOptions
174
180
  }): Effect.Effect<
175
- void,
181
+ LeaderThreadCtx['Type']['initialState'],
176
182
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
177
183
  LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
178
184
  > =>
@@ -206,19 +212,33 @@ const bootLeaderThread = ({
206
212
 
207
213
  // We're already starting pulling from the sync backend concurrently but wait until the db is ready before
208
214
  // processing any incoming mutations
209
- yield* syncProcessor.boot({ dbReady })
215
+ const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
210
216
 
217
+ let migrationsReport: MigrationsReport
211
218
  if (dbMissing) {
212
- yield* recreateDb
219
+ const recreateResult = yield* recreateDb
220
+ migrationsReport = recreateResult.migrationsReport
221
+ } else {
222
+ migrationsReport = { migrations: [] }
213
223
  }
214
224
 
215
225
  yield* Deferred.succeed(dbReady, void 0)
216
226
 
217
227
  if (initialBlockingSyncContext.blockingDeferred !== undefined) {
218
- yield* initialBlockingSyncContext.blockingDeferred
228
+ // Provides a syncing status right away before the first pull response comes in
229
+ yield* Queue.offer(bootStatusQueue, {
230
+ stage: 'syncing',
231
+ progress: { done: 0, total: -1 },
232
+ })
233
+
234
+ yield* initialBlockingSyncContext.blockingDeferred.pipe(
235
+ Effect.withSpan('@livestore/common:leader-thread:initial-sync-blocking'),
236
+ )
219
237
  }
220
238
 
221
239
  yield* Queue.offer(bootStatusQueue, { stage: 'done' })
222
240
 
223
241
  yield* bootDevtools(devtoolsOptions).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
242
+
243
+ return { migrationsReport, leaderHead: initialLeaderHead }
224
244
  })
@@ -23,18 +23,20 @@ export const getMutationEventsSince = (
23
23
  .map((_) => ({
24
24
  mutation: _.mutation,
25
25
  args: _.argsJson,
26
- id: { global: _.idGlobal, local: _.idLocal },
27
- parentId: { global: _.parentIdGlobal, local: _.parentIdLocal },
26
+ id: { global: _.idGlobal, client: _.idClient },
27
+ parentId: { global: _.parentIdGlobal, client: _.parentIdClient },
28
+ clientId: _.clientId,
29
+ sessionId: _.sessionId ?? undefined,
28
30
  }))
29
31
  .filter((_) => EventId.compare(_.id, since) > 0)
30
32
  })
31
33
 
32
- export const getLocalHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
33
- const res = dbMutationLog.select<{ idGlobal: EventId.GlobalEventId; idLocal: EventId.LocalEventId }>(
34
- sql`select idGlobal, idLocal from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idLocal DESC limit 1`,
34
+ export const getClientHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
35
+ const res = dbMutationLog.select<{ idGlobal: EventId.GlobalEventId; idClient: EventId.ClientEventId }>(
36
+ sql`select idGlobal, idClient from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idClient DESC limit 1`,
35
37
  )[0]
36
38
 
37
- return res ? { global: res.idGlobal, local: res.idLocal } : EventId.ROOT
39
+ return res ? { global: res.idGlobal, client: res.idClient } : EventId.ROOT
38
40
  }
39
41
 
40
42
  export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEventId =>
@@ -2,19 +2,20 @@ import { casesHandled } from '@livestore/utils'
2
2
  import type { HttpClient } from '@livestore/utils/effect'
3
3
  import { Effect, Queue } from '@livestore/utils/effect'
4
4
 
5
- import type { InvalidPullError, IsOfflineError, MigrationHooks, SqliteError } from '../index.js'
5
+ import type { InvalidPullError, IsOfflineError, MigrationHooks, MigrationsReport, SqliteError } from '../index.js'
6
6
  import { initializeSingletonTables, migrateDb, rehydrateFromMutationLog, UnexpectedError } from '../index.js'
7
7
  import { configureConnection } from './connection.js'
8
8
  import { LeaderThreadCtx } from './types.js'
9
9
 
10
10
  export const recreateDb: Effect.Effect<
11
- void,
11
+ { migrationsReport: MigrationsReport },
12
12
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
13
13
  LeaderThreadCtx | HttpClient.HttpClient
14
14
  > = Effect.gen(function* () {
15
15
  const { dbReadModel, dbMutationLog, schema, bootStatusQueue } = yield* LeaderThreadCtx
16
16
 
17
17
  const migrationOptions = schema.migrationOptions
18
+ let migrationsReport: MigrationsReport
18
19
 
19
20
  yield* Effect.addFinalizer(
20
21
  Effect.fn('recreateDb:finalizer')(function* (ex) {
@@ -33,7 +34,7 @@ export const recreateDb: Effect.Effect<
33
34
  Effect.gen(function* () {
34
35
  yield* Effect.tryAll(() => hooks?.init?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
35
36
 
36
- yield* migrateDb({
37
+ const migrationsReport = yield* migrateDb({
37
38
  db: tmpDb,
38
39
  schema,
39
40
  onProgress: ({ done, total }) =>
@@ -44,16 +45,18 @@ export const recreateDb: Effect.Effect<
44
45
 
45
46
  yield* Effect.tryAll(() => hooks?.pre?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
46
47
 
47
- return tmpDb
48
+ return { migrationsReport, tmpDb }
48
49
  })
49
50
 
50
51
  switch (migrationOptions.strategy) {
51
52
  case 'from-mutation-log': {
52
53
  const hooks = migrationOptions.hooks
53
- const tmpDb = yield* initDb(hooks)
54
+ const initResult = yield* initDb(hooks)
55
+
56
+ migrationsReport = initResult.migrationsReport
54
57
 
55
58
  yield* rehydrateFromMutationLog({
56
- db: tmpDb,
59
+ db: initResult.tmpDb,
57
60
  logDb: dbMutationLog,
58
61
  schema,
59
62
  migrationOptions,
@@ -61,23 +64,27 @@ export const recreateDb: Effect.Effect<
61
64
  Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
62
65
  })
63
66
 
64
- yield* Effect.tryAll(() => hooks?.post?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
67
+ yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
65
68
 
66
69
  break
67
70
  }
68
71
  case 'hard-reset': {
69
72
  const hooks = migrationOptions.hooks
70
- const tmpInMemoryDb = yield* initDb(hooks)
73
+ const initResult = yield* initDb(hooks)
74
+
75
+ migrationsReport = initResult.migrationsReport
71
76
 
72
77
  // The database is migrated but empty now, so nothing else to do
73
78
 
74
- yield* Effect.tryAll(() => hooks?.post?.(tmpInMemoryDb)).pipe(UnexpectedError.mapToUnexpectedError)
79
+ yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
75
80
 
76
81
  break
77
82
  }
78
83
  case 'manual': {
79
84
  const oldDbData = dbReadModel.export()
80
85
 
86
+ migrationsReport = { migrations: [] }
87
+
81
88
  const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
82
89
  UnexpectedError.mapToUnexpectedError,
83
90
  )
@@ -106,6 +113,8 @@ export const recreateDb: Effect.Effect<
106
113
 
107
114
  // TODO bring back
108
115
  // tmpDb.close()
116
+
117
+ return { migrationsReport }
109
118
  }).pipe(
110
119
  Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
111
120
  Effect.withSpan('@livestore/common:leader-thread:recreateDb'),