@livestore/livestore 0.4.0-dev.22 → 0.4.0-dev.23

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 (207) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +2 -2
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +14 -7
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +0 -15
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +42 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +3 -3
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +2 -2
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  25. package/dist/live-queries/client-document-get-query.js +1 -1
  26. package/dist/live-queries/client-document-get-query.js.map +1 -1
  27. package/dist/live-queries/computed.d.ts.map +1 -1
  28. package/dist/live-queries/computed.js +2 -2
  29. package/dist/live-queries/computed.js.map +1 -1
  30. package/dist/live-queries/db-query.js +14 -14
  31. package/dist/live-queries/db-query.js.map +1 -1
  32. package/dist/live-queries/db-query.test.js +2 -2
  33. package/dist/live-queries/db-query.test.js.map +1 -1
  34. package/dist/live-queries/signal.test.js +2 -2
  35. package/dist/live-queries/signal.test.js.map +1 -1
  36. package/dist/mod.d.ts +1 -1
  37. package/dist/mod.d.ts.map +1 -1
  38. package/dist/mod.js.map +1 -1
  39. package/dist/reactive.d.ts +9 -9
  40. package/dist/reactive.d.ts.map +1 -1
  41. package/dist/reactive.js +9 -26
  42. package/dist/reactive.js.map +1 -1
  43. package/dist/reactive.test.js +2 -2
  44. package/dist/reactive.test.js.map +1 -1
  45. package/dist/store/StoreRegistry.d.ts +30 -5
  46. package/dist/store/StoreRegistry.d.ts.map +1 -1
  47. package/dist/store/StoreRegistry.js +54 -31
  48. package/dist/store/StoreRegistry.js.map +1 -1
  49. package/dist/store/StoreRegistry.test.js +251 -250
  50. package/dist/store/StoreRegistry.test.js.map +1 -1
  51. package/dist/store/create-store.d.ts +6 -2
  52. package/dist/store/create-store.d.ts.map +1 -1
  53. package/dist/store/create-store.js +13 -7
  54. package/dist/store/create-store.js.map +1 -1
  55. package/dist/store/devtools.d.ts +1 -1
  56. package/dist/store/devtools.d.ts.map +1 -1
  57. package/dist/store/devtools.js +3 -3
  58. package/dist/store/devtools.js.map +1 -1
  59. package/dist/store/store-eventstream.test.js +2 -2
  60. package/dist/store/store-eventstream.test.js.map +1 -1
  61. package/dist/store/store-types.d.ts +70 -5
  62. package/dist/store/store-types.d.ts.map +1 -1
  63. package/dist/store/store-types.js.map +1 -1
  64. package/dist/store/store-types.test.js +1 -1
  65. package/dist/store/store-types.test.js.map +1 -1
  66. package/dist/store/store.d.ts +81 -2
  67. package/dist/store/store.d.ts.map +1 -1
  68. package/dist/store/store.js +128 -45
  69. package/dist/store/store.js.map +1 -1
  70. package/dist/utils/dev.js.map +1 -1
  71. package/dist/utils/stack-info.js +2 -2
  72. package/dist/utils/stack-info.js.map +1 -1
  73. package/dist/utils/tests/fixture.d.ts +1 -1
  74. package/dist/utils/tests/fixture.d.ts.map +1 -1
  75. package/dist/utils/tests/fixture.js.map +1 -1
  76. package/dist/utils/tests/otel.d.ts.map +1 -1
  77. package/dist/utils/tests/otel.js +5 -5
  78. package/dist/utils/tests/otel.js.map +1 -1
  79. package/package.json +58 -17
  80. package/src/QueryCache.ts +1 -1
  81. package/src/SqliteDbWrapper.test.ts +4 -2
  82. package/src/SqliteDbWrapper.ts +12 -11
  83. package/src/ambient.d.ts +0 -7
  84. package/src/effect/LiveStore.test.ts +61 -0
  85. package/src/effect/LiveStore.ts +17 -26
  86. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  87. package/src/live-queries/base-class.ts +7 -6
  88. package/src/live-queries/client-document-get-query.ts +4 -2
  89. package/src/live-queries/computed.ts +3 -2
  90. package/src/live-queries/db-query.test.ts +3 -2
  91. package/src/live-queries/db-query.ts +15 -15
  92. package/src/live-queries/signal.test.ts +3 -2
  93. package/src/mod.ts +1 -0
  94. package/src/reactive.test.ts +3 -2
  95. package/src/reactive.ts +22 -23
  96. package/src/store/StoreRegistry.test.ts +317 -293
  97. package/src/store/StoreRegistry.ts +63 -38
  98. package/src/store/create-store.ts +26 -11
  99. package/src/store/devtools.ts +5 -6
  100. package/src/store/store-eventstream.test.ts +4 -2
  101. package/src/store/store-types.test.ts +3 -1
  102. package/src/store/store-types.ts +47 -8
  103. package/src/store/store.ts +172 -55
  104. package/src/utils/dev.ts +2 -2
  105. package/src/utils/stack-info.ts +2 -2
  106. package/src/utils/tests/fixture.ts +2 -1
  107. package/src/utils/tests/otel.ts +8 -7
  108. package/docs/api/index.md +0 -3
  109. package/docs/building-with-livestore/complex-ui-state/index.md +0 -3
  110. package/docs/building-with-livestore/crud/index.md +0 -3
  111. package/docs/building-with-livestore/data-modeling/index.md +0 -30
  112. package/docs/building-with-livestore/debugging/index.md +0 -17
  113. package/docs/building-with-livestore/devtools/index.md +0 -79
  114. package/docs/building-with-livestore/events/index.md +0 -355
  115. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  116. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -885
  117. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  118. package/docs/building-with-livestore/opentelemetry/index.md +0 -227
  119. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  120. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  121. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  122. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  123. package/docs/building-with-livestore/state/sql-queries/index.md +0 -94
  124. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  125. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  126. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  127. package/docs/building-with-livestore/store/index.md +0 -625
  128. package/docs/building-with-livestore/syncing/index.md +0 -136
  129. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  130. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  131. package/docs/examples/cloudflare-adapter/index.md +0 -44
  132. package/docs/examples/expo-adapter/index.md +0 -44
  133. package/docs/examples/index.md +0 -55
  134. package/docs/examples/node-adapter/index.md +0 -44
  135. package/docs/examples/web-adapter/index.md +0 -52
  136. package/docs/framework-integrations/custom-elements/index.md +0 -142
  137. package/docs/framework-integrations/react-integration/index.md +0 -937
  138. package/docs/framework-integrations/solid-integration/index.md +0 -293
  139. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  140. package/docs/framework-integrations/vue-integration/index.md +0 -294
  141. package/docs/getting-started/expo/index.md +0 -882
  142. package/docs/getting-started/node/index.md +0 -115
  143. package/docs/getting-started/react-web/index.md +0 -626
  144. package/docs/getting-started/solid/index.md +0 -3
  145. package/docs/getting-started/vue/index.md +0 -471
  146. package/docs/index.md +0 -208
  147. package/docs/llms.txt +0 -146
  148. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  149. package/docs/misc/FAQ/index.md +0 -37
  150. package/docs/misc/community/index.md +0 -88
  151. package/docs/misc/credits/index.md +0 -14
  152. package/docs/misc/design-partners/index.md +0 -13
  153. package/docs/misc/package-management/index.md +0 -21
  154. package/docs/misc/performance/index.md +0 -25
  155. package/docs/misc/resources/index.md +0 -46
  156. package/docs/misc/state-of-the-project/index.md +0 -37
  157. package/docs/misc/troubleshooting/index.md +0 -82
  158. package/docs/overview/concepts/index.md +0 -78
  159. package/docs/overview/how-livestore-works/index.md +0 -56
  160. package/docs/overview/introduction/index.md +0 -413
  161. package/docs/overview/technology-comparison/index.md +0 -40
  162. package/docs/overview/when-livestore/index.md +0 -81
  163. package/docs/overview/why-livestore/index.md +0 -111
  164. package/docs/patterns/ai/index.md +0 -15
  165. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  166. package/docs/patterns/app-evolution/index.md +0 -72
  167. package/docs/patterns/auth/index.md +0 -377
  168. package/docs/patterns/effect/index.md +0 -1505
  169. package/docs/patterns/encryption/index.md +0 -6
  170. package/docs/patterns/external-data/index.md +0 -5
  171. package/docs/patterns/file-management/index.md +0 -11
  172. package/docs/patterns/file-structure/index.md +0 -14
  173. package/docs/patterns/list-ordering/index.md +0 -369
  174. package/docs/patterns/offline/index.md +0 -32
  175. package/docs/patterns/orm/index.md +0 -18
  176. package/docs/patterns/presence/index.md +0 -11
  177. package/docs/patterns/rich-text-editing/index.md +0 -11
  178. package/docs/patterns/server-side-clients/index.md +0 -97
  179. package/docs/patterns/side-effects/index.md +0 -11
  180. package/docs/patterns/state-machines/index.md +0 -11
  181. package/docs/patterns/storybook/index.md +0 -209
  182. package/docs/patterns/undo-redo/index.md +0 -9
  183. package/docs/patterns/version-control/index.md +0 -8
  184. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  185. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  186. package/docs/platform-adapters/expo-adapter/index.md +0 -262
  187. package/docs/platform-adapters/node-adapter/index.md +0 -160
  188. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  189. package/docs/platform-adapters/web-adapter/index.md +0 -287
  190. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  191. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  192. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  193. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  194. package/docs/sync-providers/cloudflare/index.md +0 -773
  195. package/docs/sync-providers/custom/index.md +0 -65
  196. package/docs/sync-providers/electricsql/index.md +0 -159
  197. package/docs/sync-providers/s2/index.md +0 -230
  198. package/docs/tutorial/0-welcome/index.md +0 -48
  199. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  200. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  201. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -530
  202. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  203. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  204. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  205. package/docs/tutorial/7-next-steps/index.md +0 -22
  206. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  207. package/docs/understanding-livestore/event-sourcing/index.md +0 -40
@@ -16,12 +16,13 @@ import {
16
16
  QueryBuilderAstSymbol,
17
17
  replaceSessionIdSymbol,
18
18
  type StorageMode,
19
+ type SyncState,
19
20
  UnknownError,
20
21
  } from '@livestore/common'
21
22
  import type { StreamEventsOptions } from '@livestore/common/leader-thread'
22
23
  import type { LiveStoreSchema } from '@livestore/common/schema'
23
- import { LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
24
- import { assertNever, isDevEnv, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
24
+ import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
25
+ import { assertNever, isDevEnv, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
25
26
  import type { Scope } from '@livestore/utils/effect'
26
27
  import {
27
28
  Cause,
@@ -56,6 +57,7 @@ import {
56
57
  StoreInternalsSymbol,
57
58
  type StoreOtel,
58
59
  type SubscribeOptions,
60
+ type SyncStatus,
59
61
  type Unsubscribe,
60
62
  } from './store-types.ts'
61
63
 
@@ -68,7 +70,7 @@ export type SubscribeFn = {
68
70
  <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): AsyncIterable<TResult>
69
71
  }
70
72
 
71
- if (isDevEnv()) {
73
+ if (isDevEnv() === true) {
72
74
  exposeDebugUtils()
73
75
  }
74
76
 
@@ -181,7 +183,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
181
183
  */
182
184
  readonly [StoreInternalsSymbol]: StoreInternals
183
185
 
184
- // #region constructor
186
+ //#region constructor
185
187
  constructor({
186
188
  clientSession,
187
189
  schema,
@@ -205,12 +207,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
205
207
 
206
208
  const reactivityGraph = makeReactivityGraph()
207
209
 
208
- const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
209
-
210
210
  const syncProcessor = makeClientSessionSyncProcessor({
211
211
  schema,
212
212
  clientSession,
213
- runtime: effectContext.runtime,
214
213
  materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
215
214
  (eventEncoded, { withChangeset, materializerHashLeader }) =>
216
215
  // We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
@@ -239,7 +238,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
239
238
  event: { decoded: undefined, encoded: eventEncoded },
240
239
  })
241
240
 
242
- const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
241
+ const materializerHash =
242
+ isDevEnv() === true ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
243
243
 
244
244
  // Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
245
245
  // During push path (local commits), materializerHashLeader is always Option.none(), so this condition
@@ -311,7 +311,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
311
311
  }
312
312
  reactivityGraph.setRefs(tablesToUpdate)
313
313
  },
314
- span: syncSpan,
315
314
  params: {
316
315
  ...omitUndefineds({
317
316
  leaderPushBatchSize: params.leaderPushBatchSize,
@@ -321,7 +320,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
321
320
  : {}),
322
321
  },
323
322
  confirmUnsavedChanges,
324
- })
323
+ }).pipe(Runtime.runSync(effectContext.runtime))
325
324
 
326
325
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
327
326
  const tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> } = {}
@@ -352,7 +351,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
352
351
  const allTableNames = new Set(
353
352
  // NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
354
353
  // unless LiveStore is running in the devtools
355
- __runningInDevtools
354
+ __runningInDevtools === true
356
355
  ? this.schema.state.sqlite.tables.keys()
357
356
  : Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)),
358
357
  )
@@ -366,7 +365,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
366
365
  existingTableRefs.get(tableName) ??
367
366
  reactivityGraph.makeRef(null, {
368
367
  equal: () => false,
369
- label: `tableRef:${tableName}`,
368
+ label: `tableRef:${String(tableName)}`,
370
369
  meta: { liveStoreRefType: 'table' },
371
370
  })
372
371
  }
@@ -382,7 +381,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
382
381
  }
383
382
 
384
383
  // End the otel spans
385
- syncSpan.end()
386
384
  commitsSpan.end()
387
385
  queriesSpan.end()
388
386
  }),
@@ -415,7 +413,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
415
413
  // Initialize stable network status property from client session
416
414
  this.networkStatus = clientSession.leaderThread.networkStatus
417
415
  }
418
- // #endregion constructor
416
+ //#endregion constructor
419
417
 
420
418
  /**
421
419
  * Current session identifier for this Store instance.
@@ -438,7 +436,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
438
436
  }
439
437
 
440
438
  private checkShutdown = (operation: string): void => {
441
- if (this[StoreInternalsSymbol].isShutdown) {
439
+ if (this[StoreInternalsSymbol].isShutdown === true) {
442
440
  throw new UnknownError({
443
441
  cause: `Store has been shut down (while performing "${operation}").`,
444
442
  note: `You cannot perform this operation after the store has been shut down.`,
@@ -485,19 +483,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
485
483
 
486
484
  return this[StoreInternalsSymbol].otel.tracer.startActiveSpan(
487
485
  `LiveStore.subscribe`,
488
- { attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } },
486
+ {
487
+ attributes: {
488
+ label: options?.label,
489
+ queryLabel: isQueryBuilder(query) === true ? query.toString() : query.label,
490
+ },
491
+ },
489
492
  options?.otelContext ?? this[StoreInternalsSymbol].otel.queriesSpanContext,
490
493
  (span) => {
491
494
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
492
495
 
493
- const queryRcRef = isQueryBuilder(query)
494
- ? queryDb(query).make(this[StoreInternalsSymbol].reactivityGraph.context!)
495
- : query._tag === 'def' || query._tag === 'signal-def'
496
- ? query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
497
- : {
498
- value: query as LiveQuery<TResult>,
499
- deref: () => {},
500
- }
496
+ const queryRcRef =
497
+ isQueryBuilder(query) === true
498
+ ? queryDb(query).make(this[StoreInternalsSymbol].reactivityGraph.context!)
499
+ : query._tag === 'def' || query._tag === 'signal-def'
500
+ ? query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
501
+ : {
502
+ value: query,
503
+ deref: () => {},
504
+ }
501
505
  const query$ = queryRcRef.value
502
506
 
503
507
  const label = `subscribe:${options?.label}`
@@ -505,7 +509,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
505
509
  const effect = this[StoreInternalsSymbol].reactivityGraph.makeEffect(
506
510
  (get, _otelContext, debugRefreshReason) => {
507
511
  const result = get(query$.results$, otelContext, debugRefreshReason)
508
- if (suppressCallback) {
512
+ if (suppressCallback === true) {
509
513
  return
510
514
  }
511
515
  onUpdate(result)
@@ -519,7 +523,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
519
523
  })
520
524
  }
521
525
 
522
- if (options?.stackInfo) {
526
+ if (options?.stackInfo !== undefined) {
523
527
  query$.activeSubscriptions.add(options.stackInfo)
524
528
  }
525
529
 
@@ -527,8 +531,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
527
531
 
528
532
  this[StoreInternalsSymbol].activeQueries.add(query$ as LiveQuery<TResult>)
529
533
 
530
- if (!query$.isDestroyed) {
531
- if (suppressCallback) {
534
+ if (query$.isDestroyed === false) {
535
+ if (suppressCallback === true) {
532
536
  // We still run once to register dependencies in the reactive graph, but suppress the initial callback so the
533
537
  // caller truly skips the first emission; subsequent runs (after commits) will call the callback.
534
538
  runInitialEffect()
@@ -543,7 +547,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
543
547
  this[StoreInternalsSymbol].reactivityGraph.destroyNode(effect)
544
548
  this[StoreInternalsSymbol].activeQueries.remove(query$ as LiveQuery<TResult>)
545
549
 
546
- if (options?.stackInfo) {
550
+ if (options?.stackInfo !== undefined) {
547
551
  query$.activeSubscriptions.delete(options.stackInfo)
548
552
  }
549
553
 
@@ -575,12 +579,13 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
575
579
  const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
576
580
  Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
577
581
  )
578
- const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
582
+ const otelContext =
583
+ otelSpan !== undefined ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
579
584
 
580
585
  yield* Effect.acquireRelease(
581
586
  Effect.sync(() =>
582
587
  this.subscribe(query, (result) => emit.single(result), {
583
- ...(options ?? {}),
588
+ ...options,
584
589
  otelContext,
585
590
  }),
586
591
  ),
@@ -617,11 +622,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
617
622
  ...omitUndefineds({ otelContext: options?.otelContext }),
618
623
  },
619
624
  ) as any
620
- if (query.schema) {
625
+ if (query.schema !== undefined) {
621
626
  return Schema.decodeSync(query.schema)(res)
622
627
  }
623
628
  return res
624
- } else if (isQueryBuilder(query)) {
629
+ } else if (isQueryBuilder(query) === true) {
625
630
  const ast = query[QueryBuilderAstSymbol]
626
631
  if (ast._tag === 'RowQuery') {
627
632
  makeExecBeforeFirstRun({
@@ -636,7 +641,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
636
641
  const schema = getResultSchema(query)
637
642
 
638
643
  // Replace SessionIdSymbol in bind values before executing the query
639
- if (sqlRes.bindValues) {
644
+ if (sqlRes.bindValues !== undefined) {
640
645
  replaceSessionIdSymbol(sqlRes.bindValues, this[StoreInternalsSymbol].clientSession.sessionId)
641
646
  }
642
647
 
@@ -654,8 +659,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
654
659
  return decodeResult.right
655
660
  } else {
656
661
  return shouldNeverHappen(
657
- `Failed to decode query result with for schema:`,
658
- schema.toString(),
662
+ 'Failed to decode query result with for schema:',
663
+ objectToString(schema),
659
664
  'raw result:',
660
665
  rawRes,
661
666
  'decode error:',
@@ -709,7 +714,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
709
714
  }
710
715
  }
711
716
 
712
- // #region commit
717
+ //#region commit
713
718
  /**
714
719
  * Commit a list of events to the store which will immediately update the local database
715
720
  * and sync the events across other clients (similar to a `git commit`).
@@ -797,23 +802,20 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
797
802
 
798
803
  const localRuntime = yield* Effect.runtime()
799
804
 
800
- const materializeEventsTx = Effect.try({
801
- try: () => {
802
- const runMaterializeEvents = () => {
803
- return this[StoreInternalsSymbol].syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
804
- }
805
+ const encodedEvents = yield* this[StoreInternalsSymbol].syncProcessor.encodeEvents(events)
805
806
 
806
- if (events.length > 1) {
807
- return this[StoreInternalsSymbol].sqliteDbWrapper.txn(runMaterializeEvents)
808
- } else {
809
- return runMaterializeEvents()
810
- }
807
+ const { writeTables } = yield* Effect.try({
808
+ try: () => {
809
+ const materialize = () =>
810
+ this[StoreInternalsSymbol].syncProcessor.materializeEvents(encodedEvents).pipe(Runtime.runSync(localRuntime))
811
+ return events.length > 1
812
+ ? this[StoreInternalsSymbol].sqliteDbWrapper.txn(materialize)
813
+ : materialize()
811
814
  },
812
815
  catch: (cause) => UnknownError.make({ cause }),
813
816
  })
814
817
 
815
- // Materialize events to state
816
- const { writeTables } = yield* materializeEventsTx
818
+ yield* this[StoreInternalsSymbol].syncProcessor.push(encodedEvents)
817
819
 
818
820
  const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
819
821
  for (const tableName of writeTables) {
@@ -857,7 +859,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
857
859
  Runtime.runSync(this[StoreInternalsSymbol].effectContext.runtime),
858
860
  )
859
861
  }
860
- // #endregion commit
862
+ //#endregion commit
861
863
 
862
864
  /**
863
865
  * Returns an async iterable of events from the eventlog.
@@ -938,6 +940,113 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
938
940
  )
939
941
  }
940
942
 
943
+ /**
944
+ * Returns the current synchronization status of the store.
945
+ *
946
+ * This is a synchronous operation that returns the sync state between the
947
+ * client session and the leader thread. Use this to display sync indicators
948
+ * or check if local changes have been pushed to the leader.
949
+ *
950
+ * @example
951
+ * ```ts
952
+ * const status = store.syncStatus()
953
+ * console.log(status.isSynced ? 'Synced' : `${status.pendingCount} pending`)
954
+ * ```
955
+ *
956
+ * @example
957
+ * ```ts
958
+ * // Health check for backend connectivity
959
+ * const status = store.syncStatus()
960
+ * if (!status.isSynced && status.pendingCount > 100) {
961
+ * console.warn('Large backlog of unsynced events')
962
+ * }
963
+ * ```
964
+ */
965
+ syncStatus = (): SyncStatus => {
966
+ this.checkShutdown('syncStatus')
967
+
968
+ const syncState = this[StoreInternalsSymbol].syncProcessor.syncState.pipe(Effect.runSync)
969
+ const pendingCount = syncState.pending.length
970
+
971
+ return {
972
+ localHead: EventSequenceNumber.Client.toString(syncState.localHead),
973
+ upstreamHead: EventSequenceNumber.Client.toString(syncState.upstreamHead),
974
+ pendingCount,
975
+ isSynced: pendingCount === 0,
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Returns an Effect Stream of sync status updates.
981
+ *
982
+ * Emits the current status immediately and then whenever the sync state changes.
983
+ * Use this for Effect-based workflows or when you need more control over the stream.
984
+ *
985
+ * @example
986
+ * ```ts
987
+ * store.syncStatusStream().pipe(
988
+ * Stream.tap((status) => Effect.log(`Sync status: ${status.isSynced}`)),
989
+ * Stream.runDrain,
990
+ * )
991
+ * ```
992
+ */
993
+ syncStatusStream = (): Stream.Stream<SyncStatus> => {
994
+ const syncStateSubscribable = this[StoreInternalsSymbol].syncProcessor.syncState
995
+
996
+ return Stream.concat(
997
+ Stream.fromEffect(syncStateSubscribable.pipe(Effect.map(this.makeSyncStatus))),
998
+ syncStateSubscribable.changes.pipe(Stream.map(this.makeSyncStatus)),
999
+ )
1000
+ }
1001
+
1002
+ /**
1003
+ * Subscribes to sync status changes.
1004
+ *
1005
+ * The callback is invoked immediately with the current status and then
1006
+ * whenever the sync state changes (e.g., when events are pushed or confirmed).
1007
+ *
1008
+ * @param onUpdate - Callback invoked with the current sync status
1009
+ * @returns Unsubscribe function to stop receiving updates
1010
+ *
1011
+ * @example
1012
+ * ```ts
1013
+ * const unsubscribe = store.subscribeSyncStatus((status) => {
1014
+ * updateUI(status.isSynced ? 'Synced' : 'Syncing...')
1015
+ * })
1016
+ *
1017
+ * // Later, stop listening
1018
+ * unsubscribe()
1019
+ * ```
1020
+ */
1021
+ subscribeSyncStatus = (onUpdate: (status: SyncStatus) => void): Unsubscribe => {
1022
+ this.checkShutdown('subscribeSyncStatus')
1023
+
1024
+ const fiber = this.syncStatusStream().pipe(
1025
+ Stream.tap((status) => Effect.sync(() => onUpdate(status))),
1026
+ Stream.runDrain,
1027
+ this.runEffectFork,
1028
+ )
1029
+
1030
+ return () => {
1031
+ Fiber.interrupt(fiber).pipe(Runtime.runFork(this[StoreInternalsSymbol].effectContext.runtime))
1032
+ }
1033
+ }
1034
+
1035
+ private makeSyncStatus = (syncState: {
1036
+ localHead: EventSequenceNumber.Client.Composite
1037
+ upstreamHead: EventSequenceNumber.Client.Composite
1038
+ pending: readonly any[]
1039
+ }): SyncStatus => {
1040
+ const pendingCount = syncState.pending.length
1041
+
1042
+ return {
1043
+ localHead: EventSequenceNumber.Client.toString(syncState.localHead),
1044
+ upstreamHead: EventSequenceNumber.Client.toString(syncState.upstreamHead),
1045
+ pendingCount,
1046
+ isSynced: pendingCount === 0,
1047
+ }
1048
+ }
1049
+
941
1050
  /**
942
1051
  * This can be used in combination with `skipRefresh` when committing events.
943
1052
  * We might need a better solution for this. Let's see.
@@ -967,7 +1076,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
967
1076
  this.checkShutdown('shutdownPromise')
968
1077
 
969
1078
  this[StoreInternalsSymbol].isShutdown = true
970
- await this.shutdown(cause ? Cause.fail(cause) : undefined).pipe(this.runEffectFork, Fiber.join, Effect.runPromise)
1079
+ await this.shutdown(cause !== undefined ? Cause.fail(cause) : undefined).pipe(
1080
+ this.runEffectFork,
1081
+ Fiber.join,
1082
+ Effect.runPromise,
1083
+ )
971
1084
  }
972
1085
 
973
1086
  /**
@@ -978,7 +1091,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
978
1091
  shutdown = (cause?: Cause.Cause<UnknownError | MaterializeError>): Effect.Effect<void> => {
979
1092
  this[StoreInternalsSymbol].isShutdown = true
980
1093
  return this[StoreInternalsSymbol].clientSession.shutdown(
981
- cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
1094
+ cause !== undefined ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
982
1095
  )
983
1096
  }
984
1097
 
@@ -1028,7 +1141,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1028
1141
  .pipe(this.runEffectFork)
1029
1142
  },
1030
1143
 
1031
- syncStates: () =>
1144
+ // NOTE: Explicit return type needed to avoid TS2742 (inferred type references internal path)
1145
+ syncStates: (): Promise<{ session: SyncState.SyncState; leader: SyncState.SyncState }> =>
1032
1146
  Effect.gen(this, function* () {
1033
1147
  const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
1034
1148
  const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
@@ -1039,11 +1153,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1039
1153
  Effect.gen(this, function* () {
1040
1154
  const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
1041
1155
  yield* Effect.log(
1042
- `Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`,
1156
+ `Session sync state: ${objectToString(session.localHead)} (upstream: ${objectToString(session.upstreamHead)})`,
1043
1157
  session.toJSON(),
1044
1158
  )
1045
1159
  const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
1046
- yield* Effect.log(`Leader sync state: ${leader.localHead} (upstream: ${leader.upstreamHead})`, leader.toJSON())
1160
+ yield* Effect.log(
1161
+ `Leader sync state: ${objectToString(leader.localHead)} (upstream: ${objectToString(leader.upstreamHead)})`,
1162
+ leader.toJSON(),
1163
+ )
1047
1164
  }).pipe(this.runEffectFork)
1048
1165
  },
1049
1166
 
package/src/utils/dev.ts CHANGED
@@ -33,8 +33,8 @@ export const downloadURL = (data: string, fileName: string) => {
33
33
  export const exposeDebugUtils = () => {
34
34
  globalThis.__debugLiveStoreUtils = {
35
35
  downloadBlob,
36
- runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
37
- runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
36
+ runSync: <A, E>(effect: Effect.Effect<A, E>) => Effect.runSync(effect),
37
+ runFork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runFork(effect),
38
38
  dumpDb: (db: SqliteDb) => {
39
39
  const tables = db.select<{ name: string }>(`SELECT name FROM sqlite_master WHERE type='table'`)
40
40
  for (const table of tables) {
@@ -41,12 +41,12 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
41
41
  // console.debug(name, filePath)
42
42
 
43
43
  // NOTE No idea where this `Module.` comes from - possibly a Vite thing?
44
- if ((name.startsWith('use') || name.startsWith('Module.use')) && name.endsWith('QueryRef') === false) {
44
+ if ((name.startsWith('use') === true || name.startsWith('Module.use') === true) && name.endsWith('QueryRef') === false) {
45
45
  hasReachedStart = true
46
46
  // console.debug('hasReachedStart. adding one more frame.')
47
47
 
48
48
  frames.unshift({ name: name.replace(/^Module\./, ''), filePath })
49
- } else if (hasReachedStart) {
49
+ } else if (hasReachedStart === true) {
50
50
  // We've reached the end of the `use*` functions, so we're adding the component name and stop
51
51
  // Unless it's `react-stack-bottom-frame`, which we skip
52
52
  if (name !== 'Object.react-stack-bottom-frame') {
@@ -1,9 +1,10 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+
1
3
  import { makeInMemoryAdapter } from '@livestore/adapter-web'
2
4
  import { provideOtel } from '@livestore/common'
3
5
  import { createStore, Events, makeSchema, State } from '@livestore/livestore'
4
6
  import { omitUndefineds } from '@livestore/utils'
5
7
  import { Effect, Schema } from '@livestore/utils/effect'
6
- import type * as otel from '@opentelemetry/api'
7
8
 
8
9
  export type Todo = {
9
10
  id: string
@@ -1,8 +1,9 @@
1
- import { omitUndefineds } from '@livestore/utils'
2
- import { identity } from '@livestore/utils/effect'
3
1
  import type { Attributes } from '@opentelemetry/api'
4
2
  import type { InMemorySpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base'
5
3
 
4
+ import { omitUndefineds } from '@livestore/utils'
5
+ import { identity } from '@livestore/utils/effect'
6
+
6
7
  type SimplifiedNestedSpan = { _name: string; attributes: any; children: SimplifiedNestedSpan[] }
7
8
 
8
9
  type NestedSpan = { span: ReadableSpan; children: NestedSpan[] }
@@ -19,8 +20,8 @@ const buildSimplifiedRootSpans = (
19
20
 
20
21
  spansMap.forEach((nestedSpan) => {
21
22
  const parentId = nestedSpan.span.parentSpanContext?.spanId
22
- const parentSpan = parentId ? spansMap.get(parentId) : undefined
23
- if (parentSpan) {
23
+ const parentSpan = parentId !== undefined ? spansMap.get(parentId) : undefined
24
+ if (parentSpan !== undefined) {
24
25
  parentSpan.children.push(nestedSpan)
25
26
  }
26
27
  })
@@ -55,7 +56,7 @@ export const getSimplifiedRootSpan = (
55
56
  ): SimplifiedNestedSpan => {
56
57
  const results = buildSimplifiedRootSpans(exporter, rootSpanName, mapAttributes)
57
58
  const firstResult = results[0]
58
- if (!firstResult) throw new Error(`Could not find the root span named '${rootSpanName}'.`)
59
+ if (firstResult == null) throw new Error(`Could not find the root span named '${rootSpanName}'.`)
59
60
  return firstResult
60
61
  }
61
62
 
@@ -77,7 +78,7 @@ const omitEmpty = (obj: any) => {
77
78
  for (const key in obj) {
78
79
  if (
79
80
  obj[key] !== undefined &&
80
- !(Array.isArray(obj[key]) && obj[key].length === 0) &&
81
+ !(Array.isArray(obj[key]) === true && obj[key].length === 0) &&
81
82
  Object.keys(obj[key]).length > 0
82
83
  ) {
83
84
  result[key] = obj[key]
@@ -119,7 +120,7 @@ export const toTraceFile = (spans: ReadableSpan[]) => {
119
120
  typeof value === 'string'
120
121
  ? { stringValue: value }
121
122
  : typeof value === 'number'
122
- ? Number.isInteger(value)
123
+ ? Number.isInteger(value) === true
123
124
  ? { intValue: value }
124
125
  : { doubleValue: value }
125
126
  : typeof value === 'boolean'
package/docs/api/index.md DELETED
@@ -1,3 +0,0 @@
1
- # API reference
2
-
3
- The API reference is autogenerated from the source code.
@@ -1,3 +0,0 @@
1
- # Complex UI state
2
-
3
- LiveStore is a great fit for building apps with complex UI state.
@@ -1,3 +0,0 @@
1
- # CRUD
2
-
3
- ## CRUD
@@ -1,30 +0,0 @@
1
- # Data modeling
2
-
3
- ## Core idea
4
-
5
- - Data modeling is probably the most important part of any app and needs to be done carefully.
6
- - The core idea is to model the read and write model separately.
7
- - Depending on the use case, you might also want to split up the read/write model into separate "containers" (e.g. for data-sharing/scalability/access control reasons).
8
- - There is no transactional consistency between containers.
9
- - Caveat: Event sourcing is not ideal for all use cases - some apps might be better off with another approach (e.g. use CRDTs for rich text editing).
10
-
11
- ## Considerations for data modeling
12
-
13
- - How much data do you expect to have and what is the shape of the data?
14
- - Some kind of data needs special handling (e.g. blobs or rich text)
15
- - Access patterns (performance, ...)
16
- - Access control
17
- - Data integrity / consistency
18
- - Sharing / collaboration
19
- - Regulatory requirements (e.g. GDPR, audit logs, ...)
20
-
21
- ## TODO
22
-
23
- - TODO: actually write this section
24
- - questions to answer
25
- - When to split things into separate containers?
26
- - How do migrations work?
27
- - Read model migrations
28
- - Write model migrations
29
- - How to create new write models based on existing ones
30
- - Example: An app has multiple workspaces and you now want to introduce the concept of "projects" inside a workspace. You might want to pre-populate a "default workspace project" for each workspace.
@@ -1,17 +0,0 @@
1
- # Debugging a LiveStore app
2
-
3
- When working on a LiveStore app you might end up in situations where you need to debug things. LiveStore is built with debuggability in mind and tries to make your life as a developer as easy as possible.
4
-
5
- Here are a few things that LiveStore offers to help you debug your app:
6
-
7
- - [OpenTelemetry](/building-with-livestore/opentelemetry) integration for tracing / metrics
8
- - [Devtools](/building-with-livestore/devtools) for inspecting the state of the store
9
- - Store helper methods
10
-
11
- ## Debugging helpers on the store
12
-
13
- The `store` exposes a `_dev` property which contains a few helpers that can help you debug your app.
14
-
15
- ## Other recommended practices and tools
16
-
17
- - Use the step debugger