@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,7 +16,8 @@ import {
16
16
  type Schema,
17
17
  type Scope,
18
18
  } from '@livestore/utils/effect'
19
- import { type CreateStoreOptions, createStore } from './create-store.ts'
19
+
20
+ import { createStore, type CreateStoreOptions } from './create-store.ts'
20
21
  import type { Store } from './store.ts'
21
22
  import type { OtelOptions } from './store-types.ts'
22
23
 
@@ -65,10 +66,10 @@ export interface RegistryStoreOptions<
65
66
  * have unmounted.
66
67
  *
67
68
  * @remarks
68
- * - **Limitation:** Per-store values are not yet supported. Only the registry-level default
69
- * (via `StoreRegistry` constructor's `defaultOptions.unusedCacheTime`) is used.
70
- * See {@link https://github.com/livestorejs/livestore/issues/917 | #917} for per-store support
71
- * and {@link https://github.com/livestorejs/livestore/issues/918 | #918} for dynamic "longest wins" behavior.
69
+ * - Per-store values override the registry-level default (set via `StoreRegistry` constructor's
70
+ * `defaultOptions.unusedCacheTime`)
71
+ * - The value is fixed when the store is first loaded into the registry. If the same `storeId` is
72
+ * requested again with a different `unusedCacheTime`, the original value is kept.
72
73
  * - If set to `Infinity`, will disable automatic disposal
73
74
  * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
74
75
  *
@@ -155,15 +156,16 @@ export class StoreRegistry {
155
156
  readonly #runtime: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
156
157
 
157
158
  /**
158
- * In-flight loading promises keyed by storeId.
159
- * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
159
+ * Disposal callback for the runtime created by the registry.
160
+ * Undefined when caller provided their own runtime (caller owns cleanup in that case).
160
161
  */
161
- readonly #loadingPromises: Map<string, Promise<Store<any, any>>> = new Map()
162
+ readonly #disposeOwnedRuntime: (() => Promise<void>) | undefined
162
163
 
163
164
  /**
164
- * Default options merged into all store configurations at load time.
165
+ * In-flight loading promises keyed by storeId.
166
+ * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
165
167
  */
166
- readonly #defaultOptions: StoreRegistryConfig['defaultOptions']
168
+ readonly #loadingPromises: Map<string, Promise<Store<any, any>>> = new Map()
167
169
 
168
170
  /**
169
171
  * Creates a new StoreRegistry instance.
@@ -179,15 +181,18 @@ export class StoreRegistry {
179
181
  * ```
180
182
  */
181
183
  constructor(config: StoreRegistryConfig = {}) {
182
- this.#defaultOptions = config.defaultOptions
183
- this.#runtime =
184
- config.runtime ??
185
- ManagedRuntime.make(Layer.mergeAll(Layer.scope, OtelLiveDummy)).runtimeEffect.pipe(Effect.runSync)
184
+ if (config.runtime !== undefined) {
185
+ this.#runtime = config.runtime
186
+ } else {
187
+ const ownedRuntime = ManagedRuntime.make(Layer.mergeAll(Layer.scope, OtelLiveDummy))
188
+ this.#runtime = ownedRuntime.runtimeEffect.pipe(Effect.runSync)
189
+ this.#disposeOwnedRuntime = () => ownedRuntime.dispose()
190
+ }
186
191
 
187
192
  this.#rcMap = RcMap.make({
188
193
  lookup: ({ options }: StoreCacheKey) => {
189
194
  // Merge registry defaults with call-site options (call-site takes precedence)
190
- const mergedOptions = { ...this.#defaultOptions, ...options }
195
+ const mergedOptions = { ...config.defaultOptions, ...options }
191
196
  return createStore(mergedOptions).pipe(
192
197
  Effect.catchAllDefect((cause) => UnknownError.make({ cause })),
193
198
  Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`),
@@ -200,9 +205,7 @@ export class StoreRegistry {
200
205
  ),
201
206
  )
202
207
  },
203
- // TODO: Make idleTimeToLive vary for each store when Effect supports per-resource TTL
204
- // See https://github.com/livestorejs/livestore/issues/917
205
- idleTimeToLive: config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
208
+ idleTimeToLive: ({ options }: StoreCacheKey) => options.unusedCacheTime ?? config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
206
209
  }).pipe(Runtime.runSync(this.#runtime))
207
210
  }
208
211
 
@@ -260,29 +263,34 @@ export class StoreRegistry {
260
263
  ): Store<TSchema, TContext> | Promise<Store<TSchema, TContext>> => {
261
264
  const exit = this.getOrLoad(options).pipe(Effect.scoped, Runtime.runSyncExit(this.#runtime))
262
265
 
263
- if (Exit.isSuccess(exit)) return exit.value
266
+ if (Exit.isSuccess(exit) === true) return exit.value
264
267
 
265
268
  // Check if the failure is due to async work
266
269
  const defect = Cause.dieOption(exit.cause)
267
- if (defect._tag === 'Some' && Runtime.isAsyncFiberException(defect.value)) {
268
- const { storeId } = options
270
+ if (defect._tag !== 'Some') {
271
+ // Handle synchronous failure
272
+ throw Cause.squash(exit.cause)
273
+ }
269
274
 
270
- // Return cached promise if one exists (ensures concurrent calls get the same Promise reference)
271
- const cached = this.#loadingPromises.get(storeId)
272
- if (cached) return cached as Promise<Store<TSchema, TContext>>
275
+ if (Runtime.isAsyncFiberException(defect.value) === false) {
276
+ // Handle synchronous failure
277
+ throw Cause.squash(exit.cause)
278
+ }
273
279
 
274
- // Create and cache the promise
275
- const fiber = defect.value.fiber
276
- const promise = Fiber.join(fiber)
277
- .pipe(Runtime.runPromise(this.#runtime))
278
- .finally(() => this.#loadingPromises.delete(storeId)) as Promise<Store<TSchema, TContext>>
280
+ const { storeId } = options
279
281
 
280
- this.#loadingPromises.set(storeId, promise)
281
- return promise
282
- }
282
+ // Return cached promise if one exists (ensures concurrent calls get the same Promise reference)
283
+ const cached = this.#loadingPromises.get(storeId)
284
+ if (cached !== undefined) return cached as Promise<Store<TSchema, TContext>>
283
285
 
284
- // Handle synchronous failure
285
- throw Cause.squash(exit.cause)
286
+ // Create and cache the promise
287
+ const fiber = defect.value.fiber as Fiber.RuntimeFiber<Store<TSchema, TContext>>
288
+ const promise = Fiber.join(fiber)
289
+ .pipe(Runtime.runPromise(this.#runtime))
290
+ .finally(() => this.#loadingPromises.delete(storeId))
291
+
292
+ this.#loadingPromises.set(storeId, promise)
293
+ return promise
286
294
  }
287
295
 
288
296
  /**
@@ -344,6 +352,25 @@ export class StoreRegistry {
344
352
  // Do nothing; preload is best-effort
345
353
  }
346
354
  }
355
+
356
+ /**
357
+ * Disposes the registry and all its managed stores, immediately releasing resources
358
+ * (database connections, WebSocket connections, web workers, etc.).
359
+ *
360
+ * Most applications should use a single `StoreRegistry` and don't need to call
361
+ * this method. It's only necessary when creating multiple short-lived registries to
362
+ * immediately release resources and avoid conflicts with subsequent registries.
363
+ *
364
+ * @returns A promise that resolves when disposal is complete
365
+ *
366
+ * @remarks
367
+ * - No-op if a custom `runtime` was provided to the constructor (caller owns cleanup)
368
+ * - Idempotent: safe to call multiple times
369
+ * - After disposal, the registry should not be used
370
+ */
371
+ dispose = async (): Promise<void> => {
372
+ await this.#disposeOwnedRuntime?.()
373
+ }
347
374
  }
348
375
 
349
376
  /**
@@ -382,12 +409,10 @@ export class StoreRegistry {
382
409
  * });
383
410
  * ```
384
411
  */
385
- export function storeOptions<
412
+ export const storeOptions = <
386
413
  TSchema extends LiveStoreSchema,
387
414
  TContext = {},
388
415
  TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
389
416
  >(
390
417
  options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
391
- ): RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema> {
392
- return options
393
- }
418
+ ): RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema> => options
@@ -1,3 +1,5 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
1
3
  import {
2
4
  type Adapter,
3
5
  type BootStatus,
@@ -5,13 +7,12 @@ import {
5
7
  type ClientSessionDevtoolsChannel,
6
8
  type ClientSessionSyncProcessorSimulationParams,
7
9
  type IntentionalShutdownCause,
8
- type InvalidPullError,
9
- type IsOfflineError,
10
10
  LogConfig,
11
11
  type MaterializeError,
12
+ type BackendIdMismatchError,
12
13
  type MigrationsReport,
13
14
  provideOtel,
14
- type SyncError,
15
+ type ServerAheadError,
15
16
  UnknownError,
16
17
  } from '@livestore/common'
17
18
  import type { LiveStoreSchema } from '@livestore/common/schema'
@@ -32,16 +33,20 @@ import {
32
33
  TaskTracing,
33
34
  } from '@livestore/utils/effect'
34
35
  import { nanoid } from '@livestore/utils/nanoid'
35
- import * as otel from '@opentelemetry/api'
36
36
 
37
37
  import { connectDevtoolsToStore } from './devtools.ts'
38
- import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
39
38
  import type {
40
39
  LiveStoreContextRunning as LiveStoreContextRunning_,
41
40
  OtelOptions,
42
41
  ShutdownDeferred,
43
42
  } from './store-types.ts'
44
43
  import { StoreInternalsSymbol } from './store-types.ts'
44
+ import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
45
+
46
+ declare global {
47
+ /** Store instances for console debugging */
48
+ var __debugLiveStore: Record<string, Store<any, any>> | undefined
49
+ }
45
50
 
46
51
  /**
47
52
  * @deprecated Use `makeStoreContext()` from `@livestore/livestore/effect` instead.
@@ -320,7 +325,7 @@ export const createStore = <
320
325
  const shutdown = (
321
326
  exit: Exit.Exit<
322
327
  IntentionalShutdownCause,
323
- UnknownError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
328
+ UnknownError | MaterializeError | BackendIdMismatchError
324
329
  >,
325
330
  ) =>
326
331
  Effect.gen(function* () {
@@ -332,7 +337,7 @@ export const createStore = <
332
337
  ),
333
338
  )
334
339
 
335
- if (shutdownDeferred) {
340
+ if (shutdownDeferred !== undefined) {
336
341
  yield* Deferred.done(shutdownDeferred, exit)
337
342
  }
338
343
 
@@ -364,7 +369,7 @@ export const createStore = <
364
369
  syncPayloadEncoded,
365
370
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
366
371
 
367
- if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
372
+ if (LS_DEV === true && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
368
373
  yield* Effect.logDebug(
369
374
  '[@livestore/livestore:createStore] migrationsReport',
370
375
  ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
@@ -383,7 +388,7 @@ export const createStore = <
383
388
  effectContext: { lifetimeScope, runtime },
384
389
  // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
385
390
  // But for now this is a good enough approximation with little downsides
386
- __runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
391
+ __runningInDevtools: ! getDevtoolsEnabled(disableDevtools),
387
392
  confirmUnsavedChanges,
388
393
  // NOTE during boot we're not yet executing events in a batched context
389
394
  // but only set the provided `batchUpdates` function after boot
@@ -420,11 +425,21 @@ export const createStore = <
420
425
 
421
426
  yield* Deferred.succeed(storeDeferred, store as any as Store)
422
427
 
428
+ // Expose store on globalThis for console debugging
429
+ globalThis.__debugLiveStore ??= {}
430
+ globalThis.__debugLiveStore[storeId] = store
431
+
432
+ yield* Effect.addFinalizer(() =>
433
+ Effect.sync(() => {
434
+ delete globalThis.__debugLiveStore?.[storeId]
435
+ }),
436
+ )
437
+
423
438
  return store
424
439
  }).pipe(
425
440
  Effect.withSpan('createStore', { attributes: { debugInstanceId, storeId } }),
426
441
  Effect.annotateLogs({ debugInstanceId, storeId }),
427
- LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
442
+ LS_DEV === true ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
428
443
  Scope.extend(lifetimeScope),
429
444
  )
430
445
  })
@@ -433,7 +448,7 @@ const validateStoreId = (storeId: string) =>
433
448
  Effect.gen(function* () {
434
449
  const validChars = /^[a-zA-Z0-9_-]+$/
435
450
 
436
- if (!validChars.test(storeId)) {
451
+ if (validChars.test(storeId) === false) {
437
452
  return yield* UnknownError.make({
438
453
  cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
439
454
  payload: { storeId },
@@ -7,8 +7,8 @@ import { nanoid } from '@livestore/utils/nanoid'
7
7
 
8
8
  import { NOT_REFRESHED_YET } from '../reactive.ts'
9
9
  import { emptyDebugInfo as makeEmptyDebugInfo } from '../SqliteDbWrapper.ts'
10
- import type { Store } from './store.ts'
11
10
  import { StoreInternalsSymbol } from './store-types.ts'
11
+ import type { Store } from './store.ts'
12
12
 
13
13
  type Unsub = () => void
14
14
  type RequestId = string
@@ -23,7 +23,7 @@ const requestNextTick: (cb: () => void) => number =
23
23
  const cancelTick: (id: number) => void =
24
24
  globalThis.cancelAnimationFrame === undefined ? (id: number) => clearTimeout(id) : globalThis.cancelAnimationFrame
25
25
 
26
- export const connectDevtoolsToStore = ({
26
+ export const connectDevtoolsToStore = Effect.fn('LSD.devtools.connectStoreToDevtools')(function* ({
27
27
  storeDevtoolsChannel,
28
28
  store,
29
29
  }: {
@@ -32,8 +32,7 @@ export const connectDevtoolsToStore = ({
32
32
  Devtools.ClientSession.MessageFromApp
33
33
  >
34
34
  store: Store
35
- }) =>
36
- Effect.gen(function* () {
35
+ }) {
37
36
  const reactivityGraphSubcriptions: SubMap = new Map()
38
37
  const liveQueriesSubscriptions: SubMap = new Map()
39
38
  const debugInfoHistorySubscriptions: SubMap = new Map()
@@ -88,7 +87,7 @@ export const connectDevtoolsToStore = ({
88
87
  // So far I could only observe this problem with webmesh proxy channels (e.g. for Expo)
89
88
  // Proof: https://share.cleanshot.com/V9G87B0B
90
89
  // Also see `leader-worker-devtools.ts` for same problem
91
- if (handledRequestIds.has(requestId)) {
90
+ if (handledRequestIds.has(requestId) === true) {
92
91
  return
93
92
  }
94
93
 
@@ -347,4 +346,4 @@ export const connectDevtoolsToStore = ({
347
346
  Stream.runDrain,
348
347
  Effect.withSpan('LSD.devtools.onMessage'),
349
348
  )
350
- }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('LSD.devtools.connectStoreToDevtools'))
349
+ }, UnknownError.mapToUnknownError)
@@ -1,3 +1,5 @@
1
+ import { expect } from 'vitest'
2
+
1
3
  import { makeInMemoryAdapter } from '@livestore/adapter-web'
2
4
  import type { MockSyncBackend } from '@livestore/common'
3
5
  import { type ClientSessionLeaderThreadProxy, makeMockSyncBackend, type UnknownError } from '@livestore/common'
@@ -6,12 +8,12 @@ import { EventFactory } from '@livestore/common/testing'
6
8
  import type { ShutdownDeferred, Store } from '@livestore/livestore'
7
9
  import { createStore, makeShutdownDeferred } from '@livestore/livestore'
8
10
  import { omitUndefineds } from '@livestore/utils'
11
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
9
12
  import type { OtelTracer, Scope } from '@livestore/utils/effect'
10
13
  import { Context, Effect, FetchHttpClient, Layer, Logger, LogLevel, Queue, Stream } from '@livestore/utils/effect'
11
14
  import { nanoid } from '@livestore/utils/nanoid'
12
15
  import { PlatformNode } from '@livestore/utils/node'
13
- import { Vitest } from '@livestore/utils-dev/node-vitest'
14
- import { expect } from 'vitest'
16
+
15
17
  import { events, schema } from '../utils/tests/fixture.ts'
16
18
 
17
19
  const withTestCtx = Vitest.makeWithTestCtx({
@@ -1,7 +1,9 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
1
3
  import type { QueryBuilder } from '@livestore/common'
2
4
  import { QueryBuilderTypeId } from '@livestore/common'
3
5
  import { Schema } from '@livestore/utils/effect'
4
- import { describe, expect, it } from 'vitest'
6
+
5
7
  import { TypeId } from '../live-queries/base-class.ts'
6
8
  import { queryDb, signal } from '../live-queries/mod.ts'
7
9
  import { isQueryable } from './store-types.ts'
@@ -1,22 +1,22 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+
1
3
  import {
2
4
  type ClientSession,
3
5
  type ClientSessionSyncProcessor,
4
6
  type ClientSessionSyncProcessorSimulationParams,
5
7
  type IntentionalShutdownCause,
6
- type InvalidPullError,
7
- type IsOfflineError,
8
8
  isQueryBuilder,
9
9
  type MaterializeError,
10
10
  type QueryBuilder,
11
11
  type StoreInterrupted,
12
- type SyncError,
12
+ type BackendIdMismatchError,
13
13
  type UnknownError,
14
14
  } from '@livestore/common'
15
15
  import type { StreamEventsOptions } from '@livestore/common/leader-thread'
16
16
  import type { LiveStoreEvent, LiveStoreSchema } from '@livestore/common/schema'
17
17
  import type { Effect, Runtime, Schema, Scope } from '@livestore/utils/effect'
18
18
  import { Deferred, Predicate } from '@livestore/utils/effect'
19
- import type * as otel from '@opentelemetry/api'
19
+
20
20
  import type {
21
21
  LiveQuery,
22
22
  LiveQueryDef,
@@ -45,20 +45,20 @@ export type LiveStoreContext<TSchema extends LiveStoreSchema = LiveStoreSchema.A
45
45
  | LiveStoreContextRunning<TSchema>
46
46
  | {
47
47
  stage: 'error'
48
- error: UnknownError | unknown
48
+ error: unknown
49
49
  }
50
50
  | {
51
51
  stage: 'shutdown'
52
- cause: IntentionalShutdownCause | StoreInterrupted | SyncError
52
+ cause: IntentionalShutdownCause | StoreInterrupted | UnknownError
53
53
  }
54
54
 
55
55
  export type ShutdownDeferred = Deferred.Deferred<
56
56
  IntentionalShutdownCause,
57
- UnknownError | SyncError | StoreInterrupted | MaterializeError | InvalidPullError | IsOfflineError
57
+ UnknownError | StoreInterrupted | MaterializeError | BackendIdMismatchError
58
58
  >
59
59
  export const makeShutdownDeferred: Effect.Effect<ShutdownDeferred> = Deferred.make<
60
60
  IntentionalShutdownCause,
61
- UnknownError | SyncError | StoreInterrupted | MaterializeError | InvalidPullError | IsOfflineError
61
+ UnknownError | StoreInterrupted | MaterializeError | BackendIdMismatchError
62
62
  >()
63
63
 
64
64
  /**
@@ -416,3 +416,42 @@ export const isLiveQueryInstance = (value: unknown): value is LiveQuery<any> =>
416
416
  */
417
417
  export const isQueryable = (value: unknown): value is Queryable<unknown> =>
418
418
  isQueryBuilder(value) || isLiveQueryInstance(value) || isLiveQueryDef(value)
419
+
420
+ /**
421
+ * Represents the current synchronization status of the store.
422
+ *
423
+ * This provides visibility into the sync state between the client session
424
+ * and the leader thread, allowing applications to show sync indicators
425
+ * or determine backend health.
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * const status = store.syncStatus()
430
+ * if (status.isSynced) {
431
+ * console.log('All changes synced')
432
+ * } else {
433
+ * console.log(`${status.pendingCount} events pending sync`)
434
+ * }
435
+ * ```
436
+ */
437
+ export type SyncStatus = {
438
+ /**
439
+ * The local head sequence number (most recent event in the client session).
440
+ * Represented as a string in the format "e{global}.{client}" (e.g., "e5.2").
441
+ */
442
+ localHead: string
443
+ /**
444
+ * The upstream head sequence number (what the leader thread has confirmed).
445
+ * Represented as a string in the format "e{global}" (e.g., "e3").
446
+ */
447
+ upstreamHead: string
448
+ /**
449
+ * Number of events pending synchronization to the leader thread.
450
+ */
451
+ pendingCount: number
452
+ /**
453
+ * Whether the client session is fully synced with the leader thread.
454
+ * True when there are no pending events (pendingCount === 0).
455
+ */
456
+ isSynced: boolean
457
+ }