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

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 (75) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/effect/LiveStore.d.ts +123 -2
  3. package/dist/effect/LiveStore.d.ts.map +1 -1
  4. package/dist/effect/LiveStore.js +195 -1
  5. package/dist/effect/LiveStore.js.map +1 -1
  6. package/dist/effect/mod.d.ts +1 -1
  7. package/dist/effect/mod.d.ts.map +1 -1
  8. package/dist/effect/mod.js +3 -1
  9. package/dist/effect/mod.js.map +1 -1
  10. package/dist/mod.d.ts +1 -0
  11. package/dist/mod.d.ts.map +1 -1
  12. package/dist/mod.js +1 -0
  13. package/dist/mod.js.map +1 -1
  14. package/dist/store/StoreRegistry.d.ts +190 -0
  15. package/dist/store/StoreRegistry.d.ts.map +1 -0
  16. package/dist/store/StoreRegistry.js +244 -0
  17. package/dist/store/StoreRegistry.js.map +1 -0
  18. package/dist/store/StoreRegistry.test.d.ts +2 -0
  19. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  20. package/dist/store/StoreRegistry.test.js +380 -0
  21. package/dist/store/StoreRegistry.test.js.map +1 -0
  22. package/dist/store/create-store.d.ts +50 -4
  23. package/dist/store/create-store.d.ts.map +1 -1
  24. package/dist/store/create-store.js +19 -0
  25. package/dist/store/create-store.js.map +1 -1
  26. package/dist/store/devtools.d.ts.map +1 -1
  27. package/dist/store/devtools.js +13 -0
  28. package/dist/store/devtools.js.map +1 -1
  29. package/dist/store/store-types.d.ts +10 -25
  30. package/dist/store/store-types.d.ts.map +1 -1
  31. package/dist/store/store-types.js.map +1 -1
  32. package/dist/store/store.d.ts +23 -6
  33. package/dist/store/store.d.ts.map +1 -1
  34. package/dist/store/store.js +20 -2
  35. package/dist/store/store.js.map +1 -1
  36. package/docs/building-with-livestore/complex-ui-state/index.md +0 -2
  37. package/docs/building-with-livestore/crud/index.md +0 -2
  38. package/docs/building-with-livestore/data-modeling/index.md +29 -0
  39. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -6
  40. package/docs/building-with-livestore/opentelemetry/index.md +25 -6
  41. package/docs/building-with-livestore/rules-for-ai-agents/index.md +2 -2
  42. package/docs/building-with-livestore/state/sql-queries/index.md +22 -0
  43. package/docs/building-with-livestore/state/sqlite-schema/index.md +2 -2
  44. package/docs/building-with-livestore/store/index.md +344 -0
  45. package/docs/framework-integrations/react-integration/index.md +380 -361
  46. package/docs/framework-integrations/vue-integration/index.md +2 -2
  47. package/docs/getting-started/expo/index.md +189 -43
  48. package/docs/getting-started/react-web/index.md +77 -24
  49. package/docs/getting-started/vue/index.md +3 -3
  50. package/docs/index.md +1 -2
  51. package/docs/llms.txt +0 -1
  52. package/docs/misc/troubleshooting/index.md +3 -3
  53. package/docs/overview/how-livestore-works/index.md +1 -1
  54. package/docs/overview/introduction/index.md +409 -1
  55. package/docs/overview/why-livestore/index.md +108 -2
  56. package/docs/patterns/auth/index.md +185 -34
  57. package/docs/patterns/effect/index.md +11 -1
  58. package/docs/patterns/storybook/index.md +43 -26
  59. package/docs/platform-adapters/expo-adapter/index.md +36 -19
  60. package/docs/platform-adapters/web-adapter/index.md +71 -2
  61. package/docs/tutorial/1-setup-starter-project/index.md +5 -5
  62. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +54 -35
  63. package/docs/tutorial/5-expand-business-logic/index.md +1 -1
  64. package/docs/tutorial/6-persist-ui-state/index.md +12 -12
  65. package/package.json +6 -6
  66. package/src/effect/LiveStore.ts +385 -3
  67. package/src/effect/mod.ts +13 -1
  68. package/src/mod.ts +1 -0
  69. package/src/store/StoreRegistry.test.ts +516 -0
  70. package/src/store/StoreRegistry.ts +393 -0
  71. package/src/store/create-store.ts +50 -4
  72. package/src/store/devtools.ts +15 -0
  73. package/src/store/store-types.ts +17 -5
  74. package/src/store/store.ts +25 -5
  75. package/docs/building-with-livestore/examples/index.md +0 -30
@@ -0,0 +1,393 @@
1
+ import { LogConfig, OtelLiveDummy, provideOtel, UnknownError } from '@livestore/common'
2
+ import type { LiveStoreSchema } from '@livestore/common/schema'
3
+ import { omitUndefineds } from '@livestore/utils'
4
+ import {
5
+ Cause,
6
+ Effect,
7
+ Equal,
8
+ Exit,
9
+ Fiber,
10
+ Hash,
11
+ Layer,
12
+ ManagedRuntime,
13
+ type OtelTracer,
14
+ RcMap,
15
+ Runtime,
16
+ type Schema,
17
+ type Scope,
18
+ } from '@livestore/utils/effect'
19
+ import { type CreateStoreOptions, createStore } from './create-store.ts'
20
+ import type { Store } from './store.ts'
21
+ import type { OtelOptions } from './store-types.ts'
22
+
23
+ /**
24
+ * Default time to keep unused stores in cache.
25
+ *
26
+ * - Browser: 60 seconds (60,000 ms)
27
+ * - SSR: Infinity (disables disposal to avoid disposing stores before server render completes)
28
+ *
29
+ * @internal Exported primarily for testing purposes.
30
+ */
31
+ export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000
32
+
33
+ /**
34
+ * Configuration options for stores managed by a {@link StoreRegistry}.
35
+ *
36
+ * Extends {@link CreateStoreOptions} with registry-specific settings for caching and observability.
37
+ * Use with {@link storeOptions} helper to get full type inference when defining reusable store configurations.
38
+ *
39
+ * @typeParam TSchema - The LiveStore schema type
40
+ * @typeParam TContext - User-defined context attached to the store
41
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
42
+ *
43
+ * @see {@link storeOptions} for defining reusable store configurations
44
+ * @see {@link StoreRegistry} for managing store lifecycles
45
+ */
46
+ export interface RegistryStoreOptions<
47
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
48
+ TContext = {},
49
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
50
+ > extends CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> {
51
+ /**
52
+ * OpenTelemetry configuration for tracing store operations.
53
+ *
54
+ * When provided, store operations (boot, queries, commits) will be traced
55
+ * under the given root span context using the specified tracer.
56
+ */
57
+ otelOptions?: Partial<OtelOptions>
58
+ /**
59
+ * The time in milliseconds that this store should remain
60
+ * in memory after becoming unused. When this store becomes
61
+ * unused (no active retentions), it will be disposed after this duration.
62
+ *
63
+ * Stores transition to the unused state as soon as they have no
64
+ * active retentions, so when all components which use that store
65
+ * have unmounted.
66
+ *
67
+ * @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.
72
+ * - If set to `Infinity`, will disable automatic disposal
73
+ * - 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
+ * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
76
+ * disposing stores before server render completes.
77
+ */
78
+ unusedCacheTime?: number
79
+ }
80
+
81
+ type StoreRegistryConfig = {
82
+ /**
83
+ * Default options that are applied to all stores when they are loaded.
84
+ *
85
+ * @remarks
86
+ * These are options that typically don't depend on the specific store being loaded:
87
+ * - Framework integration (`batchUpdates`)
88
+ * - Environment settings (`disableDevtools`, `debug`, `otelOptions`)
89
+ * - Behavior defaults (`confirmUnsavedChanges`, `unusedCacheTime`)
90
+ *
91
+ * Store-specific fields like `schema`, `adapter`, `storeId`, and `boot` are intentionally
92
+ * excluded since they vary per store definition.
93
+ */
94
+ defaultOptions?: Partial<
95
+ Pick<
96
+ RegistryStoreOptions,
97
+ 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'debug' | 'otelOptions' | 'unusedCacheTime'
98
+ >
99
+ >
100
+ /**
101
+ * Custom Effect runtime for all registry operations (loading, caching, etc.).
102
+ * When the runtime's scope closes, all managed stores are automatically shut down.
103
+ */
104
+ runtime?: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
105
+ }
106
+
107
+ /**
108
+ * RcMap cache key that uses storeId for equality/hashing but carries full options.
109
+ * This allows RcMap to deduplicate by storeId while the lookup function has access to all options.
110
+ *
111
+ * @remarks
112
+ * Only `storeId` is used for equality and hashing. This means if `getOrLoadPromise` is called
113
+ * with different options (e.g., different `adapter`) but the same `storeId`, the cached store
114
+ * from the first call will be returned. This is intentional - a store's identity is determined
115
+ * solely by its `storeId`, and callers should not expect to get different stores by varying
116
+ * other options while keeping the same `storeId`.
117
+ */
118
+ class StoreCacheKey implements Equal.Equal {
119
+ readonly options: RegistryStoreOptions<any, any, any>
120
+
121
+ constructor(options: RegistryStoreOptions<any, any, any>) {
122
+ this.options = options
123
+ }
124
+
125
+ /**
126
+ * Equality is based solely on `storeId`. Other options in `RegistryStoreOptions` are ignored
127
+ * for cache key comparison. The first options used for a given `storeId` determine the
128
+ * store's configuration.
129
+ */
130
+ [Equal.symbol](that: Equal.Equal): boolean {
131
+ return that instanceof StoreCacheKey && this.options.storeId === that.options.storeId
132
+ }
133
+
134
+ [Hash.symbol](): number {
135
+ return Hash.string(this.options.storeId)
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Store Registry coordinating store loading, caching, and retention
141
+ *
142
+ * @public
143
+ */
144
+ export class StoreRegistry {
145
+ /**
146
+ * Reference-counted cache mapping storeId to Store instances.
147
+ * Stores are created on first access and disposed after `unusedCacheTime` when all references are released.
148
+ */
149
+ readonly #rcMap: RcMap.RcMap<StoreCacheKey, Store<any, any>, UnknownError>
150
+
151
+ /**
152
+ * Effect runtime providing Scope and OtelTracer for all registry operations.
153
+ * When the runtime's scope closes, all managed stores are automatically shut down.
154
+ */
155
+ readonly #runtime: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
156
+
157
+ /**
158
+ * In-flight loading promises keyed by storeId.
159
+ * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
160
+ */
161
+ readonly #loadingPromises: Map<string, Promise<Store<any, any>>> = new Map()
162
+
163
+ /**
164
+ * Default options merged into all store configurations at load time.
165
+ */
166
+ readonly #defaultOptions: StoreRegistryConfig['defaultOptions']
167
+
168
+ /**
169
+ * Creates a new StoreRegistry instance.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const registry = new StoreRegistry({
174
+ * defaultOptions: {
175
+ * batchUpdates,
176
+ * unusedCacheTime: 30_000,
177
+ * }
178
+ * })
179
+ * ```
180
+ */
181
+ 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)
186
+
187
+ this.#rcMap = RcMap.make({
188
+ lookup: ({ options }: StoreCacheKey) => {
189
+ // Merge registry defaults with call-site options (call-site takes precedence)
190
+ const mergedOptions = { ...this.#defaultOptions, ...options }
191
+ return createStore(mergedOptions).pipe(
192
+ Effect.catchAllDefect((cause) => UnknownError.make({ cause })),
193
+ Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`),
194
+ LogConfig.withLoggerConfig(mergedOptions, { threadName: 'window' }),
195
+ provideOtel(
196
+ omitUndefineds({
197
+ parentSpanContext: mergedOptions.otelOptions?.rootSpanContext,
198
+ otelTracer: mergedOptions.otelOptions?.tracer,
199
+ }),
200
+ ),
201
+ )
202
+ },
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,
206
+ }).pipe(Runtime.runSync(this.#runtime))
207
+ }
208
+
209
+ /**
210
+ * Gets a cached store or loads a new one, with the store lifetime scoped to the caller.
211
+ *
212
+ * @typeParam TSchema - The schema type for the store
213
+ * @typeParam TContext - The context type for the store
214
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
215
+ * @returns An Effect that yields the store, scoped to the provided Scope
216
+ *
217
+ * @remarks
218
+ * - Stores are kept in cache and reused while any scope holds them
219
+ * - When the scope closes, the reference is released; the store is disposed after `unusedCacheTime`
220
+ * if no other scopes retain it
221
+ * - Concurrent calls with the same storeId share the same store instance
222
+ */
223
+ getOrLoad = <
224
+ TSchema extends LiveStoreSchema,
225
+ TContext = {},
226
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
227
+ >(
228
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
229
+ ): Effect.Effect<Store<TSchema, TContext>, UnknownError, Scope.Scope> =>
230
+ Effect.gen(this, function* () {
231
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
232
+ const key = new StoreCacheKey(options)
233
+ const store = yield* RcMap.get(this.#rcMap, key)
234
+
235
+ return store as Store<TSchema, TContext>
236
+ }).pipe(Effect.withSpan(`StoreRegistry.getOrLoad:${options.storeId}`))
237
+
238
+ /**
239
+ * Get or load a store, returning it directly if already loaded or a promise if loading.
240
+ *
241
+ * @typeParam TSchema - The schema type for the store
242
+ * @typeParam TContext - The context type for the store
243
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
244
+ * @returns The loaded store if available, or a Promise that resolves to the loaded store
245
+ * @throws unknown - store loading error
246
+ *
247
+ * @remarks
248
+ * - Returns the store instance directly (synchronous) when already loaded
249
+ * - Returns a stable Promise reference when loading is in progress or needs to be initiated
250
+ * - Throws with the same error instance on subsequent calls after failure
251
+ * - Applies default options from registry config, with call-site options taking precedence
252
+ * - Concurrent calls with the same storeId share the same store instance
253
+ */
254
+ getOrLoadPromise = <
255
+ TSchema extends LiveStoreSchema,
256
+ TContext = {},
257
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
258
+ >(
259
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
260
+ ): Store<TSchema, TContext> | Promise<Store<TSchema, TContext>> => {
261
+ const exit = this.getOrLoad(options).pipe(Effect.scoped, Runtime.runSyncExit(this.#runtime))
262
+
263
+ if (Exit.isSuccess(exit)) return exit.value
264
+
265
+ // Check if the failure is due to async work
266
+ const defect = Cause.dieOption(exit.cause)
267
+ if (defect._tag === 'Some' && Runtime.isAsyncFiberException(defect.value)) {
268
+ const { storeId } = options
269
+
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>>
273
+
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>>
279
+
280
+ this.#loadingPromises.set(storeId, promise)
281
+ return promise
282
+ }
283
+
284
+ // Handle synchronous failure
285
+ throw Cause.squash(exit.cause)
286
+ }
287
+
288
+ /**
289
+ * Retains the store in cache.
290
+ *
291
+ * @typeParam TSchema - The schema type for the store
292
+ * @typeParam TContext - The context type for the store
293
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
294
+ * @returns A release function that, when called, removes this retention hold
295
+ *
296
+ * @remarks
297
+ * - Multiple retains on the same store are independent; each must be released separately
298
+ * - If the store isn't cached yet, it will be loaded and then retained
299
+ * - The store will remain in cache until all retains are released and after `unusedCacheTime` expires
300
+ */
301
+ retain = <
302
+ TSchema extends LiveStoreSchema,
303
+ TContext = {},
304
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
305
+ >(
306
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
307
+ ): (() => void) => {
308
+ const release = Effect.gen(this, function* () {
309
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
310
+ const key = new StoreCacheKey(options)
311
+ yield* RcMap.get(this.#rcMap, key)
312
+ // Effect.never suspends indefinitely, keeping the RcMap reference alive.
313
+ // When `release()` is called, the fiber is interrupted, closing the scope
314
+ // and releasing the RcMap entry (which may trigger disposal after idleTimeToLive).
315
+ yield* Effect.never
316
+ }).pipe(Effect.scoped, Runtime.runCallback(this.#runtime))
317
+
318
+ return () => release()
319
+ }
320
+
321
+ /**
322
+ * Loads a store (without suspending) to warm up the cache.
323
+ *
324
+ * @typeParam TSchema - The schema of the store to preload
325
+ * @typeParam TContext - The context type for the store
326
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
327
+ * @returns A promise that resolves when the loading is complete (success or failure)
328
+ *
329
+ * @remarks
330
+ * - We don't return the store or throw as this is a fire-and-forget operation.
331
+ * - If the entry remains unused after preload resolves/rejects, it is scheduled for disposal.
332
+ * - Does not affect the retention of the store in cache.
333
+ */
334
+ preload = async <
335
+ TSchema extends LiveStoreSchema,
336
+ TContext = {},
337
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
338
+ >(
339
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
340
+ ): Promise<void> => {
341
+ try {
342
+ await this.getOrLoadPromise(options)
343
+ } catch {
344
+ // Do nothing; preload is best-effort
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Helper for defining reusable store options with full type inference. Returns
351
+ * options that can be passed to `useStore()` or `storeRegistry.preload()`.
352
+ *
353
+ * @remarks
354
+ * At runtime this is an identity function that returns the input unchanged.
355
+ * Its value lies in enabling TypeScript's excess property checking to catch
356
+ * typos and configuration errors, while allowing options to be shared across
357
+ * `useStore()`, `storeRegistry.preload()`, `storeRegistry.getOrLoad()`, etc.
358
+ *
359
+ * @typeParam TSchema - The LiveStore schema type
360
+ * @typeParam TContext - User-defined context attached to the store
361
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
362
+ * @param options - The store configuration options
363
+ * @returns The same options object, unchanged
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * export const issueStoreOptions = (issueId: string) =>
368
+ * storeOptions({
369
+ * storeId: `issue-${issueId}`,
370
+ * schema,
371
+ * adapter,
372
+ * unusedCacheTime: 30_000,
373
+ * })
374
+ *
375
+ * // In a component
376
+ * const issueStore = useStore(issueStoreOptions(issueId))
377
+ *
378
+ * // In a route loader or event handler
379
+ * storeRegistry.preload({
380
+ * ...issueStoreOptions(issueId),
381
+ * unusedCacheTime: 10_000,
382
+ * });
383
+ * ```
384
+ */
385
+ export function storeOptions<
386
+ TSchema extends LiveStoreSchema,
387
+ TContext = {},
388
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
389
+ >(
390
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
391
+ ): RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema> {
392
+ return options
393
+ }
@@ -43,6 +43,22 @@ import type {
43
43
  } from './store-types.ts'
44
44
  import { StoreInternalsSymbol } from './store-types.ts'
45
45
 
46
+ /**
47
+ * @deprecated Use `makeStoreContext()` from `@livestore/livestore/effect` instead.
48
+ * This service doesn't preserve schema types. See the Effect integration docs for migration.
49
+ *
50
+ * @example Migration
51
+ * ```ts
52
+ * // Before (untyped)
53
+ * import { LiveStoreContextRunning } from '@livestore/livestore/effect'
54
+ * const { store } = yield* LiveStoreContextRunning
55
+ *
56
+ * // After (typed)
57
+ * import { makeStoreContext } from '@livestore/livestore/effect'
58
+ * const AppStore = makeStoreContext<typeof schema>()('app')
59
+ * const { store } = yield* AppStore.Tag
60
+ * ```
61
+ */
46
62
  export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
47
63
  LiveStoreContextRunning,
48
64
  LiveStoreContextRunning_
@@ -54,6 +70,9 @@ export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/e
54
70
  }).pipe(Layer.unwrapScoped)
55
71
  }
56
72
 
73
+ /**
74
+ * @deprecated Use `StoreContext.DeferredTag` from `makeStoreContext()` instead.
75
+ */
57
76
  export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
58
77
  DeferredStoreContext,
59
78
  Deferred.Deferred<LiveStoreContextRunning['Type'], UnknownError>
@@ -97,14 +116,14 @@ export type LiveStoreContextProps<
97
116
  */
98
117
  syncPayloadSchema?: TSyncPayloadSchema
99
118
  /**
100
- * Payload that is sent to the sync backend during connection establishment.
119
+ * Payload that is sent to the sync backend when connecting
101
120
  *
102
121
  * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
103
122
  * - At runtime this value is encoded with `syncPayloadSchema` before being handed to the adapter.
104
123
  *
105
124
  * Example:
106
125
  * const SyncPayload = Schema.Struct({ authToken: Schema.String })
107
- * <LiveStoreProvider syncPayloadSchema={SyncPayload} syncPayload={{ authToken: '...' }} />
126
+ * useStore({ ..., syncPayloadSchema: SyncPayload, syncPayload: { authToken: '...' } })
108
127
  */
109
128
  syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
110
129
  }
@@ -114,9 +133,21 @@ export interface CreateStoreOptions<
114
133
  TContext = {},
115
134
  TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
116
135
  > extends LogConfig.WithLoggerOptions {
136
+ /** The LiveStore schema defining tables, events, and materializers. */
117
137
  schema: TSchema
138
+ /** Adapter used for data storage and synchronization. */
118
139
  adapter: Adapter
140
+ /**
141
+ * Unique identifier for the Store instance, stable for its lifetime.
142
+ *
143
+ * - **Valid characters**: Only alphanumeric characters, underscores (`_`), and hyphens (`-`)
144
+ * are allowed. Must match `/^[a-zA-Z0-9_-]+$/`.
145
+ * - **Globally unique**: Use globally unique IDs (e.g., nanoid) to prevent collisions across stores.
146
+ * - **Use namespaces**: Prefix to avoid collisions and for easier identification when debugging
147
+ * (e.g., `app-root`, `workspace-abc123`, `issue-456`)
148
+ */
119
149
  storeId: string
150
+ /** User-defined context that will be attached to the created Store (e.g. for dependency injection). */
120
151
  context?: TContext
121
152
  boot?: (
122
153
  store: Store<TSchema, TContext>,
@@ -125,6 +156,19 @@ export interface CreateStoreOptions<
125
156
  parentSpan: otel.Span
126
157
  },
127
158
  ) => Effect.SyncOrPromiseOrEffect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
159
+ onBootStatus?: (status: BootStatus) => void
160
+ /**
161
+ * Needed in React so LiveStore can apply multiple events in a single render.
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * // With React DOM
166
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
167
+ *
168
+ * // With React Native
169
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-native'
170
+ * ```
171
+ */
128
172
  batchUpdates?: (run: () => void) => void
129
173
  /**
130
174
  * Whether to disable devtools.
@@ -132,7 +176,6 @@ export interface CreateStoreOptions<
132
176
  * @default 'auto'
133
177
  */
134
178
  disableDevtools?: boolean | 'auto'
135
- onBootStatus?: (status: BootStatus) => void
136
179
  shutdownDeferred?: ShutdownDeferred
137
180
  /**
138
181
  * Currently only used in the web adapter:
@@ -150,7 +193,7 @@ export interface CreateStoreOptions<
150
193
  */
151
194
  syncPayloadSchema?: TSyncPayloadSchema
152
195
  /**
153
- * Payload that is sent to the sync backend during connection establishment.
196
+ * Payload that is sent to the sync backend when connecting
154
197
  *
155
198
  * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
156
199
  * - At runtime this value is encoded with `syncPayloadSchema` and carried through the adapter
@@ -159,8 +202,11 @@ export interface CreateStoreOptions<
159
202
  * @default undefined
160
203
  */
161
204
  syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
205
+ /** Options provided to the Store constructor. */
162
206
  params?: {
207
+ /** Max events pushed to the leader per write batch. */
163
208
  leaderPushBatchSize?: number
209
+ /** Chunk size used when the stream replays confirmed events. */
164
210
  eventQueryBatchSize?: number
165
211
  simulation?: {
166
212
  clientSessionSyncProcessor: typeof ClientSessionSyncProcessorSimulationParams.Type
@@ -235,6 +235,7 @@ export const connectDevtoolsToStore = ({
235
235
  sendToDevtools(
236
236
  Devtools.ClientSession.LiveQueriesRes.make({
237
237
  liveQueries: [...store[StoreInternalsSymbol].activeQueries].map((q) => ({
238
+ /** TODO: include schema metadata for schema-aware rendering in devtools (e.g., schema AST/hash/identifier or table+columns for QueryBuilder-derived queries). */
238
239
  _tag: q._tag,
239
240
  id: q.id,
240
241
  label: q.label,
@@ -316,6 +317,20 @@ export const connectDevtoolsToStore = ({
316
317
  break
317
318
  }
318
319
  case 'LSD.ClientSession.Ping': {
320
+ // Check version mismatch and respond with VersionMismatch if versions don't match
321
+ if (decodedMessage.liveStoreVersion !== liveStoreVersion) {
322
+ sendToDevtools(
323
+ Devtools.ClientSession.VersionMismatch.make({
324
+ requestId,
325
+ clientId,
326
+ sessionId,
327
+ liveStoreVersion,
328
+ appVersion: liveStoreVersion,
329
+ receivedVersion: decodedMessage.liveStoreVersion,
330
+ }),
331
+ )
332
+ break
333
+ }
319
334
  sendToDevtools(Devtools.ClientSession.Pong.make({ requestId, clientId, sessionId, liveStoreVersion }))
320
335
  break
321
336
  }
@@ -38,9 +38,11 @@ import type { Store } from './store.ts'
38
38
  * - `running`: Store is active and ready for queries/commits
39
39
  * - `error`: Store failed during boot or operation
40
40
  * - `shutdown`: Store was intentionally shut down or interrupted
41
+ *
42
+ * @typeParam TSchema - The LiveStore schema type. Defaults to `LiveStoreSchema.Any`.
41
43
  */
42
- export type LiveStoreContext =
43
- | LiveStoreContextRunning
44
+ export type LiveStoreContext<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> =
45
+ | LiveStoreContextRunning<TSchema>
44
46
  | {
45
47
  stage: 'error'
46
48
  error: UnknownError | unknown
@@ -64,10 +66,14 @@ export const makeShutdownDeferred: Effect.Effect<ShutdownDeferred> = Deferred.ma
64
66
  *
65
67
  * This is the normal operating state where you can query data, commit events,
66
68
  * and subscribe to changes.
69
+ *
70
+ * @typeParam TSchema - The LiveStore schema type. Defaults to `LiveStoreSchema.Any`
71
+ * for backwards compatibility, but prefer providing the concrete schema type
72
+ * for full type safety.
67
73
  */
68
- export type LiveStoreContextRunning = {
74
+ export type LiveStoreContextRunning<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> = {
69
75
  stage: 'running'
70
- store: Store
76
+ store: Store<TSchema>
71
77
  }
72
78
 
73
79
  export type OtelOptions = {
@@ -170,7 +176,13 @@ export type StoreInternals = {
170
176
  isShutdown: boolean
171
177
  }
172
178
 
173
- export type StoreOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> = {
179
+ /**
180
+ * Parameters for constructing a Store instance.
181
+ *
182
+ * @internal This type is used by the Store constructor and is not part of the public API.
183
+ * For creating stores, use `createStore()` or `StoreRegistry` instead.
184
+ */
185
+ export type StoreConstructorParams<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> = {
174
186
  clientSession: ClientSession
175
187
  schema: TSchema
176
188
  storeId: string
@@ -15,6 +15,7 @@ import {
15
15
  prepareBindValues,
16
16
  QueryBuilderAstSymbol,
17
17
  replaceSessionIdSymbol,
18
+ type StorageMode,
18
19
  UnknownError,
19
20
  } from '@livestore/common'
20
21
  import type { StreamEventsOptions } from '@livestore/common/leader-thread'
@@ -49,10 +50,10 @@ import {
49
50
  type Queryable,
50
51
  type RefreshReason,
51
52
  type StoreCommitOptions,
53
+ type StoreConstructorParams,
52
54
  type StoreEventsOptions,
53
55
  type StoreInternals,
54
56
  StoreInternalsSymbol,
55
- type StoreOptions,
56
57
  type StoreOtel,
57
58
  type SubscribeOptions,
58
59
  type Unsubscribe,
@@ -90,8 +91,8 @@ export const STORE_DEFAULT_PARAMS = {
90
91
  * ## Creating a Store
91
92
  *
92
93
  * 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.
94
+ * In React applications, use `StoreRegistry` with `<StoreRegistryProvider>` and the `useStore()` hook
95
+ * which manages the Store lifecycle.
95
96
  *
96
97
  * ## Querying Data
97
98
  *
@@ -136,7 +137,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
136
137
  readonly context: TContext
137
138
 
138
139
  /** Options provided to the Store constructor. */
139
- readonly params: StoreOptions<TSchema, TContext>['params']
140
+ readonly params: StoreConstructorParams<TSchema, TContext>['params']
140
141
 
141
142
  /**
142
143
  * Reactive connectivity updates emitted by the backing sync backend.
@@ -157,6 +158,24 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
157
158
  */
158
159
  readonly networkStatus: ClientSession['leaderThread']['networkStatus']
159
160
 
161
+ /**
162
+ * Indicates how data is being stored.
163
+ *
164
+ * - `persisted`: Data is persisted to disk (e.g., via OPFS on web, SQLite file on native)
165
+ * - `in-memory`: Data is only stored in memory and will be lost on page refresh
166
+ *
167
+ * The store operates in `in-memory` mode when persistent storage is unavailable,
168
+ * such as in Safari/Firefox private browsing mode where OPFS is restricted.
169
+ *
170
+ * @example
171
+ * ```tsx
172
+ * if (store.storageMode === 'in-memory') {
173
+ * showWarning('Data will not be persisted in private browsing mode')
174
+ * }
175
+ * ```
176
+ */
177
+ readonly storageMode: StorageMode
178
+
160
179
  /**
161
180
  * Store internals. Not part of the public API — shapes and semantics may change without notice.
162
181
  */
@@ -174,7 +193,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
174
193
  params,
175
194
  confirmUnsavedChanges,
176
195
  __runningInDevtools,
177
- }: StoreOptions<TSchema, TContext>) {
196
+ }: StoreConstructorParams<TSchema, TContext>) {
178
197
  super()
179
198
 
180
199
  this.storeId = storeId
@@ -182,6 +201,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
182
201
  this.context = context
183
202
  this.params = params
184
203
  this.networkStatus = clientSession.leaderThread.networkStatus
204
+ this.storageMode = clientSession.leaderThread.initialState.storageMode
185
205
 
186
206
  const reactivityGraph = makeReactivityGraph()
187
207