@livestore/livestore 0.4.0-dev.21 → 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 (216) 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 +130 -2
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +185 -6
  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/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +3 -3
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +1 -1
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts.map +1 -1
  32. package/dist/live-queries/computed.js +2 -2
  33. package/dist/live-queries/computed.js.map +1 -1
  34. package/dist/live-queries/db-query.js +14 -14
  35. package/dist/live-queries/db-query.js.map +1 -1
  36. package/dist/live-queries/db-query.test.js +2 -2
  37. package/dist/live-queries/db-query.test.js.map +1 -1
  38. package/dist/live-queries/signal.test.js +2 -2
  39. package/dist/live-queries/signal.test.js.map +1 -1
  40. package/dist/mod.d.ts +2 -1
  41. package/dist/mod.d.ts.map +1 -1
  42. package/dist/mod.js +1 -0
  43. package/dist/mod.js.map +1 -1
  44. package/dist/reactive.d.ts +9 -9
  45. package/dist/reactive.d.ts.map +1 -1
  46. package/dist/reactive.js +9 -26
  47. package/dist/reactive.js.map +1 -1
  48. package/dist/reactive.test.js +2 -2
  49. package/dist/reactive.test.js.map +1 -1
  50. package/dist/store/StoreRegistry.d.ts +215 -0
  51. package/dist/store/StoreRegistry.d.ts.map +1 -0
  52. package/dist/store/StoreRegistry.js +267 -0
  53. package/dist/store/StoreRegistry.js.map +1 -0
  54. package/dist/store/StoreRegistry.test.d.ts +2 -0
  55. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  56. package/dist/store/StoreRegistry.test.js +381 -0
  57. package/dist/store/StoreRegistry.test.js.map +1 -0
  58. package/dist/store/create-store.d.ts +56 -6
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +32 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +1 -1
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +16 -3
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-eventstream.test.js +2 -2
  67. package/dist/store/store-eventstream.test.js.map +1 -1
  68. package/dist/store/store-types.d.ts +59 -9
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store-types.test.js +1 -1
  72. package/dist/store/store-types.test.js.map +1 -1
  73. package/dist/store/store.d.ts +102 -6
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +148 -47
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/dev.js.map +1 -1
  78. package/dist/utils/stack-info.js +2 -2
  79. package/dist/utils/stack-info.js.map +1 -1
  80. package/dist/utils/tests/fixture.d.ts +1 -1
  81. package/dist/utils/tests/fixture.d.ts.map +1 -1
  82. package/dist/utils/tests/fixture.js.map +1 -1
  83. package/dist/utils/tests/otel.d.ts.map +1 -1
  84. package/dist/utils/tests/otel.js +5 -5
  85. package/dist/utils/tests/otel.js.map +1 -1
  86. package/package.json +59 -18
  87. package/src/QueryCache.ts +1 -1
  88. package/src/SqliteDbWrapper.test.ts +4 -2
  89. package/src/SqliteDbWrapper.ts +12 -11
  90. package/src/ambient.d.ts +0 -7
  91. package/src/effect/LiveStore.test.ts +61 -0
  92. package/src/effect/LiveStore.ts +381 -8
  93. package/src/effect/mod.ts +13 -1
  94. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  95. package/src/live-queries/base-class.ts +7 -6
  96. package/src/live-queries/client-document-get-query.ts +4 -2
  97. package/src/live-queries/computed.ts +3 -2
  98. package/src/live-queries/db-query.test.ts +3 -2
  99. package/src/live-queries/db-query.ts +15 -15
  100. package/src/live-queries/signal.test.ts +3 -2
  101. package/src/mod.ts +2 -0
  102. package/src/reactive.test.ts +3 -2
  103. package/src/reactive.ts +22 -23
  104. package/src/store/StoreRegistry.test.ts +540 -0
  105. package/src/store/StoreRegistry.ts +418 -0
  106. package/src/store/create-store.ts +76 -15
  107. package/src/store/devtools.ts +20 -6
  108. package/src/store/store-eventstream.test.ts +4 -2
  109. package/src/store/store-types.test.ts +3 -1
  110. package/src/store/store-types.ts +64 -13
  111. package/src/store/store.ts +197 -60
  112. package/src/utils/dev.ts +2 -2
  113. package/src/utils/stack-info.ts +2 -2
  114. package/src/utils/tests/fixture.ts +2 -1
  115. package/src/utils/tests/otel.ts +8 -7
  116. package/docs/api/index.md +0 -3
  117. package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
  118. package/docs/building-with-livestore/crud/index.md +0 -5
  119. package/docs/building-with-livestore/data-modeling/index.md +0 -1
  120. package/docs/building-with-livestore/debugging/index.md +0 -17
  121. package/docs/building-with-livestore/devtools/index.md +0 -79
  122. package/docs/building-with-livestore/events/index.md +0 -355
  123. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  124. package/docs/building-with-livestore/examples/index.md +0 -30
  125. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
  126. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  127. package/docs/building-with-livestore/opentelemetry/index.md +0 -208
  128. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  129. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  130. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  131. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  132. package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
  133. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  134. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  135. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  136. package/docs/building-with-livestore/store/index.md +0 -281
  137. package/docs/building-with-livestore/syncing/index.md +0 -136
  138. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  139. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  140. package/docs/examples/cloudflare-adapter/index.md +0 -44
  141. package/docs/examples/expo-adapter/index.md +0 -44
  142. package/docs/examples/index.md +0 -55
  143. package/docs/examples/node-adapter/index.md +0 -44
  144. package/docs/examples/web-adapter/index.md +0 -52
  145. package/docs/framework-integrations/custom-elements/index.md +0 -142
  146. package/docs/framework-integrations/react-integration/index.md +0 -918
  147. package/docs/framework-integrations/solid-integration/index.md +0 -293
  148. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  149. package/docs/framework-integrations/vue-integration/index.md +0 -294
  150. package/docs/getting-started/expo/index.md +0 -736
  151. package/docs/getting-started/node/index.md +0 -115
  152. package/docs/getting-started/react-web/index.md +0 -573
  153. package/docs/getting-started/solid/index.md +0 -3
  154. package/docs/getting-started/vue/index.md +0 -471
  155. package/docs/index.md +0 -209
  156. package/docs/llms.txt +0 -147
  157. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  158. package/docs/misc/FAQ/index.md +0 -37
  159. package/docs/misc/community/index.md +0 -88
  160. package/docs/misc/credits/index.md +0 -14
  161. package/docs/misc/design-partners/index.md +0 -13
  162. package/docs/misc/package-management/index.md +0 -21
  163. package/docs/misc/performance/index.md +0 -25
  164. package/docs/misc/resources/index.md +0 -46
  165. package/docs/misc/state-of-the-project/index.md +0 -37
  166. package/docs/misc/troubleshooting/index.md +0 -82
  167. package/docs/overview/concepts/index.md +0 -78
  168. package/docs/overview/how-livestore-works/index.md +0 -56
  169. package/docs/overview/introduction/index.md +0 -5
  170. package/docs/overview/technology-comparison/index.md +0 -40
  171. package/docs/overview/when-livestore/index.md +0 -81
  172. package/docs/overview/why-livestore/index.md +0 -5
  173. package/docs/patterns/ai/index.md +0 -15
  174. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  175. package/docs/patterns/app-evolution/index.md +0 -72
  176. package/docs/patterns/auth/index.md +0 -226
  177. package/docs/patterns/effect/index.md +0 -1495
  178. package/docs/patterns/encryption/index.md +0 -6
  179. package/docs/patterns/external-data/index.md +0 -5
  180. package/docs/patterns/file-management/index.md +0 -11
  181. package/docs/patterns/file-structure/index.md +0 -14
  182. package/docs/patterns/list-ordering/index.md +0 -369
  183. package/docs/patterns/offline/index.md +0 -32
  184. package/docs/patterns/orm/index.md +0 -18
  185. package/docs/patterns/presence/index.md +0 -11
  186. package/docs/patterns/rich-text-editing/index.md +0 -11
  187. package/docs/patterns/server-side-clients/index.md +0 -97
  188. package/docs/patterns/side-effects/index.md +0 -11
  189. package/docs/patterns/state-machines/index.md +0 -11
  190. package/docs/patterns/storybook/index.md +0 -192
  191. package/docs/patterns/undo-redo/index.md +0 -9
  192. package/docs/patterns/version-control/index.md +0 -8
  193. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  194. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  195. package/docs/platform-adapters/expo-adapter/index.md +0 -245
  196. package/docs/platform-adapters/node-adapter/index.md +0 -160
  197. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  198. package/docs/platform-adapters/web-adapter/index.md +0 -218
  199. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  200. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  201. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  202. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  203. package/docs/sync-providers/cloudflare/index.md +0 -773
  204. package/docs/sync-providers/custom/index.md +0 -65
  205. package/docs/sync-providers/electricsql/index.md +0 -159
  206. package/docs/sync-providers/s2/index.md +0 -230
  207. package/docs/tutorial/0-welcome/index.md +0 -48
  208. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  209. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  210. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
  211. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  212. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  213. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  214. package/docs/tutorial/7-next-steps/index.md +0 -22
  215. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  216. package/docs/understanding-livestore/event-sourcing/index.md +0 -40
@@ -15,12 +15,14 @@ import {
15
15
  prepareBindValues,
16
16
  QueryBuilderAstSymbol,
17
17
  replaceSessionIdSymbol,
18
+ type StorageMode,
19
+ type SyncState,
18
20
  UnknownError,
19
21
  } from '@livestore/common'
20
22
  import type { StreamEventsOptions } from '@livestore/common/leader-thread'
21
23
  import type { LiveStoreSchema } from '@livestore/common/schema'
22
- import { LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
23
- 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'
24
26
  import type { Scope } from '@livestore/utils/effect'
25
27
  import {
26
28
  Cause,
@@ -49,12 +51,13 @@ import {
49
51
  type Queryable,
50
52
  type RefreshReason,
51
53
  type StoreCommitOptions,
54
+ type StoreConstructorParams,
52
55
  type StoreEventsOptions,
53
56
  type StoreInternals,
54
57
  StoreInternalsSymbol,
55
- type StoreOptions,
56
58
  type StoreOtel,
57
59
  type SubscribeOptions,
60
+ type SyncStatus,
58
61
  type Unsubscribe,
59
62
  } from './store-types.ts'
60
63
 
@@ -67,7 +70,7 @@ export type SubscribeFn = {
67
70
  <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): AsyncIterable<TResult>
68
71
  }
69
72
 
70
- if (isDevEnv()) {
73
+ if (isDevEnv() === true) {
71
74
  exposeDebugUtils()
72
75
  }
73
76
 
@@ -90,8 +93,8 @@ export const STORE_DEFAULT_PARAMS = {
90
93
  * ## Creating a Store
91
94
  *
92
95
  * Use `createStore` (Effect-based) or `createStorePromise` to obtain a Store instance.
93
- * In React applications, use the `<LiveStoreProvider>` component which manages the Store lifecycle
94
- * and exposes it via React context.
96
+ * In React applications, use `StoreRegistry` with `<StoreRegistryProvider>` and the `useStore()` hook
97
+ * which manages the Store lifecycle.
95
98
  *
96
99
  * ## Querying Data
97
100
  *
@@ -136,7 +139,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
136
139
  readonly context: TContext
137
140
 
138
141
  /** Options provided to the Store constructor. */
139
- readonly params: StoreOptions<TSchema, TContext>['params']
142
+ readonly params: StoreConstructorParams<TSchema, TContext>['params']
140
143
 
141
144
  /**
142
145
  * Reactive connectivity updates emitted by the backing sync backend.
@@ -157,12 +160,30 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
157
160
  */
158
161
  readonly networkStatus: ClientSession['leaderThread']['networkStatus']
159
162
 
163
+ /**
164
+ * Indicates how data is being stored.
165
+ *
166
+ * - `persisted`: Data is persisted to disk (e.g., via OPFS on web, SQLite file on native)
167
+ * - `in-memory`: Data is only stored in memory and will be lost on page refresh
168
+ *
169
+ * The store operates in `in-memory` mode when persistent storage is unavailable,
170
+ * such as in Safari/Firefox private browsing mode where OPFS is restricted.
171
+ *
172
+ * @example
173
+ * ```tsx
174
+ * if (store.storageMode === 'in-memory') {
175
+ * showWarning('Data will not be persisted in private browsing mode')
176
+ * }
177
+ * ```
178
+ */
179
+ readonly storageMode: StorageMode
180
+
160
181
  /**
161
182
  * Store internals. Not part of the public API — shapes and semantics may change without notice.
162
183
  */
163
184
  readonly [StoreInternalsSymbol]: StoreInternals
164
185
 
165
- // #region constructor
186
+ //#region constructor
166
187
  constructor({
167
188
  clientSession,
168
189
  schema,
@@ -174,7 +195,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
174
195
  params,
175
196
  confirmUnsavedChanges,
176
197
  __runningInDevtools,
177
- }: StoreOptions<TSchema, TContext>) {
198
+ }: StoreConstructorParams<TSchema, TContext>) {
178
199
  super()
179
200
 
180
201
  this.storeId = storeId
@@ -182,15 +203,13 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
182
203
  this.context = context
183
204
  this.params = params
184
205
  this.networkStatus = clientSession.leaderThread.networkStatus
206
+ this.storageMode = clientSession.leaderThread.initialState.storageMode
185
207
 
186
208
  const reactivityGraph = makeReactivityGraph()
187
209
 
188
- const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
189
-
190
210
  const syncProcessor = makeClientSessionSyncProcessor({
191
211
  schema,
192
212
  clientSession,
193
- runtime: effectContext.runtime,
194
213
  materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
195
214
  (eventEncoded, { withChangeset, materializerHashLeader }) =>
196
215
  // We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
@@ -219,7 +238,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
219
238
  event: { decoded: undefined, encoded: eventEncoded },
220
239
  })
221
240
 
222
- const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
241
+ const materializerHash =
242
+ isDevEnv() === true ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
223
243
 
224
244
  // Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
225
245
  // During push path (local commits), materializerHashLeader is always Option.none(), so this condition
@@ -291,7 +311,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
291
311
  }
292
312
  reactivityGraph.setRefs(tablesToUpdate)
293
313
  },
294
- span: syncSpan,
295
314
  params: {
296
315
  ...omitUndefineds({
297
316
  leaderPushBatchSize: params.leaderPushBatchSize,
@@ -301,7 +320,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
301
320
  : {}),
302
321
  },
303
322
  confirmUnsavedChanges,
304
- })
323
+ }).pipe(Runtime.runSync(effectContext.runtime))
305
324
 
306
325
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
307
326
  const tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> } = {}
@@ -332,7 +351,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
332
351
  const allTableNames = new Set(
333
352
  // NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
334
353
  // unless LiveStore is running in the devtools
335
- __runningInDevtools
354
+ __runningInDevtools === true
336
355
  ? this.schema.state.sqlite.tables.keys()
337
356
  : Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)),
338
357
  )
@@ -346,7 +365,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
346
365
  existingTableRefs.get(tableName) ??
347
366
  reactivityGraph.makeRef(null, {
348
367
  equal: () => false,
349
- label: `tableRef:${tableName}`,
368
+ label: `tableRef:${String(tableName)}`,
350
369
  meta: { liveStoreRefType: 'table' },
351
370
  })
352
371
  }
@@ -362,7 +381,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
362
381
  }
363
382
 
364
383
  // End the otel spans
365
- syncSpan.end()
366
384
  commitsSpan.end()
367
385
  queriesSpan.end()
368
386
  }),
@@ -395,7 +413,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
395
413
  // Initialize stable network status property from client session
396
414
  this.networkStatus = clientSession.leaderThread.networkStatus
397
415
  }
398
- // #endregion constructor
416
+ //#endregion constructor
399
417
 
400
418
  /**
401
419
  * Current session identifier for this Store instance.
@@ -418,7 +436,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
418
436
  }
419
437
 
420
438
  private checkShutdown = (operation: string): void => {
421
- if (this[StoreInternalsSymbol].isShutdown) {
439
+ if (this[StoreInternalsSymbol].isShutdown === true) {
422
440
  throw new UnknownError({
423
441
  cause: `Store has been shut down (while performing "${operation}").`,
424
442
  note: `You cannot perform this operation after the store has been shut down.`,
@@ -465,19 +483,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
465
483
 
466
484
  return this[StoreInternalsSymbol].otel.tracer.startActiveSpan(
467
485
  `LiveStore.subscribe`,
468
- { 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
+ },
469
492
  options?.otelContext ?? this[StoreInternalsSymbol].otel.queriesSpanContext,
470
493
  (span) => {
471
494
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
472
495
 
473
- const queryRcRef = isQueryBuilder(query)
474
- ? queryDb(query).make(this[StoreInternalsSymbol].reactivityGraph.context!)
475
- : query._tag === 'def' || query._tag === 'signal-def'
476
- ? query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
477
- : {
478
- value: query as LiveQuery<TResult>,
479
- deref: () => {},
480
- }
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
+ }
481
505
  const query$ = queryRcRef.value
482
506
 
483
507
  const label = `subscribe:${options?.label}`
@@ -485,7 +509,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
485
509
  const effect = this[StoreInternalsSymbol].reactivityGraph.makeEffect(
486
510
  (get, _otelContext, debugRefreshReason) => {
487
511
  const result = get(query$.results$, otelContext, debugRefreshReason)
488
- if (suppressCallback) {
512
+ if (suppressCallback === true) {
489
513
  return
490
514
  }
491
515
  onUpdate(result)
@@ -499,7 +523,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
499
523
  })
500
524
  }
501
525
 
502
- if (options?.stackInfo) {
526
+ if (options?.stackInfo !== undefined) {
503
527
  query$.activeSubscriptions.add(options.stackInfo)
504
528
  }
505
529
 
@@ -507,8 +531,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
507
531
 
508
532
  this[StoreInternalsSymbol].activeQueries.add(query$ as LiveQuery<TResult>)
509
533
 
510
- if (!query$.isDestroyed) {
511
- if (suppressCallback) {
534
+ if (query$.isDestroyed === false) {
535
+ if (suppressCallback === true) {
512
536
  // We still run once to register dependencies in the reactive graph, but suppress the initial callback so the
513
537
  // caller truly skips the first emission; subsequent runs (after commits) will call the callback.
514
538
  runInitialEffect()
@@ -523,7 +547,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
523
547
  this[StoreInternalsSymbol].reactivityGraph.destroyNode(effect)
524
548
  this[StoreInternalsSymbol].activeQueries.remove(query$ as LiveQuery<TResult>)
525
549
 
526
- if (options?.stackInfo) {
550
+ if (options?.stackInfo !== undefined) {
527
551
  query$.activeSubscriptions.delete(options.stackInfo)
528
552
  }
529
553
 
@@ -555,12 +579,13 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
555
579
  const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
556
580
  Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
557
581
  )
558
- 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()
559
584
 
560
585
  yield* Effect.acquireRelease(
561
586
  Effect.sync(() =>
562
587
  this.subscribe(query, (result) => emit.single(result), {
563
- ...(options ?? {}),
588
+ ...options,
564
589
  otelContext,
565
590
  }),
566
591
  ),
@@ -597,11 +622,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
597
622
  ...omitUndefineds({ otelContext: options?.otelContext }),
598
623
  },
599
624
  ) as any
600
- if (query.schema) {
625
+ if (query.schema !== undefined) {
601
626
  return Schema.decodeSync(query.schema)(res)
602
627
  }
603
628
  return res
604
- } else if (isQueryBuilder(query)) {
629
+ } else if (isQueryBuilder(query) === true) {
605
630
  const ast = query[QueryBuilderAstSymbol]
606
631
  if (ast._tag === 'RowQuery') {
607
632
  makeExecBeforeFirstRun({
@@ -616,7 +641,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
616
641
  const schema = getResultSchema(query)
617
642
 
618
643
  // Replace SessionIdSymbol in bind values before executing the query
619
- if (sqlRes.bindValues) {
644
+ if (sqlRes.bindValues !== undefined) {
620
645
  replaceSessionIdSymbol(sqlRes.bindValues, this[StoreInternalsSymbol].clientSession.sessionId)
621
646
  }
622
647
 
@@ -634,8 +659,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
634
659
  return decodeResult.right
635
660
  } else {
636
661
  return shouldNeverHappen(
637
- `Failed to decode query result with for schema:`,
638
- schema.toString(),
662
+ 'Failed to decode query result with for schema:',
663
+ objectToString(schema),
639
664
  'raw result:',
640
665
  rawRes,
641
666
  'decode error:',
@@ -689,7 +714,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
689
714
  }
690
715
  }
691
716
 
692
- // #region commit
717
+ //#region commit
693
718
  /**
694
719
  * Commit a list of events to the store which will immediately update the local database
695
720
  * and sync the events across other clients (similar to a `git commit`).
@@ -777,23 +802,20 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
777
802
 
778
803
  const localRuntime = yield* Effect.runtime()
779
804
 
780
- const materializeEventsTx = Effect.try({
781
- try: () => {
782
- const runMaterializeEvents = () => {
783
- return this[StoreInternalsSymbol].syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
784
- }
805
+ const encodedEvents = yield* this[StoreInternalsSymbol].syncProcessor.encodeEvents(events)
785
806
 
786
- if (events.length > 1) {
787
- return this[StoreInternalsSymbol].sqliteDbWrapper.txn(runMaterializeEvents)
788
- } else {
789
- return runMaterializeEvents()
790
- }
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()
791
814
  },
792
815
  catch: (cause) => UnknownError.make({ cause }),
793
816
  })
794
817
 
795
- // Materialize events to state
796
- const { writeTables } = yield* materializeEventsTx
818
+ yield* this[StoreInternalsSymbol].syncProcessor.push(encodedEvents)
797
819
 
798
820
  const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
799
821
  for (const tableName of writeTables) {
@@ -837,7 +859,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
837
859
  Runtime.runSync(this[StoreInternalsSymbol].effectContext.runtime),
838
860
  )
839
861
  }
840
- // #endregion commit
862
+ //#endregion commit
841
863
 
842
864
  /**
843
865
  * Returns an async iterable of events from the eventlog.
@@ -918,6 +940,113 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
918
940
  )
919
941
  }
920
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
+
921
1050
  /**
922
1051
  * This can be used in combination with `skipRefresh` when committing events.
923
1052
  * We might need a better solution for this. Let's see.
@@ -947,7 +1076,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
947
1076
  this.checkShutdown('shutdownPromise')
948
1077
 
949
1078
  this[StoreInternalsSymbol].isShutdown = true
950
- 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
+ )
951
1084
  }
952
1085
 
953
1086
  /**
@@ -958,7 +1091,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
958
1091
  shutdown = (cause?: Cause.Cause<UnknownError | MaterializeError>): Effect.Effect<void> => {
959
1092
  this[StoreInternalsSymbol].isShutdown = true
960
1093
  return this[StoreInternalsSymbol].clientSession.shutdown(
961
- cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
1094
+ cause !== undefined ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
962
1095
  )
963
1096
  }
964
1097
 
@@ -1008,7 +1141,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1008
1141
  .pipe(this.runEffectFork)
1009
1142
  },
1010
1143
 
1011
- syncStates: () =>
1144
+ // NOTE: Explicit return type needed to avoid TS2742 (inferred type references internal path)
1145
+ syncStates: (): Promise<{ session: SyncState.SyncState; leader: SyncState.SyncState }> =>
1012
1146
  Effect.gen(this, function* () {
1013
1147
  const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
1014
1148
  const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
@@ -1019,11 +1153,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1019
1153
  Effect.gen(this, function* () {
1020
1154
  const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
1021
1155
  yield* Effect.log(
1022
- `Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`,
1156
+ `Session sync state: ${objectToString(session.localHead)} (upstream: ${objectToString(session.upstreamHead)})`,
1023
1157
  session.toJSON(),
1024
1158
  )
1025
1159
  const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
1026
- 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
+ )
1027
1164
  }).pipe(this.runEffectFork)
1028
1165
  },
1029
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,5 +0,0 @@
1
- # Complex UI state
2
-
3
- LiveStore is a great fit for building apps with complex UI state.
4
-
5
- TODO: actually write this section
@@ -1,5 +0,0 @@
1
- # CRUD
2
-
3
- ## CRUD
4
-
5
- TBD
@@ -1 +0,0 @@
1
- # Data modeling
@@ -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