@livestore/livestore 0.4.0-dev.9 → 0.5.0-dev.0

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 (132) 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 +15 -13
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +24 -6
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +134 -6
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +190 -11
  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 +41 -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 +129 -9
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +30 -4
  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 +4 -3
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts +56 -0
  32. package/dist/live-queries/computed.d.ts.map +1 -1
  33. package/dist/live-queries/computed.js +62 -6
  34. package/dist/live-queries/computed.js.map +1 -1
  35. package/dist/live-queries/db-query.d.ts +2 -2
  36. package/dist/live-queries/db-query.d.ts.map +1 -1
  37. package/dist/live-queries/db-query.js +41 -30
  38. package/dist/live-queries/db-query.js.map +1 -1
  39. package/dist/live-queries/db-query.test.js +112 -29
  40. package/dist/live-queries/db-query.test.js.map +1 -1
  41. package/dist/live-queries/signal.d.ts +49 -0
  42. package/dist/live-queries/signal.d.ts.map +1 -1
  43. package/dist/live-queries/signal.js +51 -2
  44. package/dist/live-queries/signal.js.map +1 -1
  45. package/dist/live-queries/signal.test.js +4 -4
  46. package/dist/live-queries/signal.test.js.map +1 -1
  47. package/dist/mod.d.ts +3 -3
  48. package/dist/mod.d.ts.map +1 -1
  49. package/dist/mod.js +3 -2
  50. package/dist/mod.js.map +1 -1
  51. package/dist/reactive.d.ts +10 -10
  52. package/dist/reactive.d.ts.map +1 -1
  53. package/dist/reactive.js +29 -27
  54. package/dist/reactive.js.map +1 -1
  55. package/dist/reactive.test.js +2 -2
  56. package/dist/reactive.test.js.map +1 -1
  57. package/dist/store/StoreRegistry.d.ts +215 -0
  58. package/dist/store/StoreRegistry.d.ts.map +1 -0
  59. package/dist/store/StoreRegistry.js +267 -0
  60. package/dist/store/StoreRegistry.js.map +1 -0
  61. package/dist/store/StoreRegistry.test.d.ts +2 -0
  62. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  63. package/dist/store/StoreRegistry.test.js +384 -0
  64. package/dist/store/StoreRegistry.test.js.map +1 -0
  65. package/dist/store/create-store.d.ts +99 -22
  66. package/dist/store/create-store.d.ts.map +1 -1
  67. package/dist/store/create-store.js +85 -37
  68. package/dist/store/create-store.js.map +1 -1
  69. package/dist/store/devtools.d.ts +6 -18
  70. package/dist/store/devtools.d.ts.map +1 -1
  71. package/dist/store/devtools.js +61 -19
  72. package/dist/store/devtools.js.map +1 -1
  73. package/dist/store/store-eventstream.test.d.ts +2 -0
  74. package/dist/store/store-eventstream.test.d.ts.map +1 -0
  75. package/dist/store/store-eventstream.test.js +65 -0
  76. package/dist/store/store-eventstream.test.js.map +1 -0
  77. package/dist/store/store-types.d.ts +271 -31
  78. package/dist/store/store-types.d.ts.map +1 -1
  79. package/dist/store/store-types.js +41 -1
  80. package/dist/store/store-types.js.map +1 -1
  81. package/dist/store/store-types.test.d.ts +2 -0
  82. package/dist/store/store-types.test.d.ts.map +1 -0
  83. package/dist/store/store-types.test.js +39 -0
  84. package/dist/store/store-types.test.js.map +1 -0
  85. package/dist/store/store.d.ts +255 -68
  86. package/dist/store/store.d.ts.map +1 -1
  87. package/dist/store/store.js +533 -186
  88. package/dist/store/store.js.map +1 -1
  89. package/dist/utils/dev.d.ts.map +1 -1
  90. package/dist/utils/dev.js +1 -1
  91. package/dist/utils/dev.js.map +1 -1
  92. package/dist/utils/stack-info.d.ts.map +1 -1
  93. package/dist/utils/stack-info.js +3 -2
  94. package/dist/utils/stack-info.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +46 -39
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -0
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/otel.d.ts.map +1 -1
  100. package/dist/utils/tests/otel.js +5 -5
  101. package/dist/utils/tests/otel.js.map +1 -1
  102. package/package.json +50 -18
  103. package/src/QueryCache.ts +1 -1
  104. package/src/SqliteDbWrapper.test.ts +30 -6
  105. package/src/SqliteDbWrapper.ts +21 -17
  106. package/src/ambient.d.ts +0 -7
  107. package/src/effect/LiveStore.test.ts +61 -0
  108. package/src/effect/LiveStore.ts +426 -22
  109. package/src/effect/mod.ts +13 -1
  110. package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
  111. package/src/live-queries/base-class.ts +162 -32
  112. package/src/live-queries/client-document-get-query.ts +6 -4
  113. package/src/live-queries/computed.ts +65 -8
  114. package/src/live-queries/db-query.test.ts +168 -30
  115. package/src/live-queries/db-query.ts +58 -43
  116. package/src/live-queries/signal.test.ts +5 -4
  117. package/src/live-queries/signal.ts +52 -3
  118. package/src/mod.ts +19 -2
  119. package/src/reactive.test.ts +3 -2
  120. package/src/reactive.ts +58 -49
  121. package/src/store/StoreRegistry.test.ts +543 -0
  122. package/src/store/StoreRegistry.ts +429 -0
  123. package/src/store/create-store.ts +227 -74
  124. package/src/store/devtools.ts +305 -261
  125. package/src/store/store-eventstream.test.ts +123 -0
  126. package/src/store/store-types.test.ts +52 -0
  127. package/src/store/store-types.ts +318 -44
  128. package/src/store/store.ts +695 -283
  129. package/src/utils/dev.ts +3 -4
  130. package/src/utils/stack-info.ts +5 -2
  131. package/src/utils/tests/fixture.ts +9 -1
  132. package/src/utils/tests/otel.ts +8 -7
@@ -0,0 +1,429 @@
1
+ import { OtelLiveDummy, provideOtel, UnknownError } from '@livestore/common'
2
+ import type { LiveStoreSchema } from '@livestore/common/schema'
3
+ import { isDevEnv, omitUndefineds } from '@livestore/utils'
4
+ import {
5
+ Cause,
6
+ type Context,
7
+ Effect,
8
+ Equal,
9
+ Exit,
10
+ Fiber,
11
+ Hash,
12
+ Layer,
13
+ ManagedRuntime,
14
+ Result,
15
+ type OtelTracer,
16
+ RcMap,
17
+ References,
18
+ type Schema,
19
+ Scope,
20
+ } from '@livestore/utils/effect'
21
+
22
+ import { type CreateStoreOptions, createStore } from './create-store.ts'
23
+ import type { OtelOptions } from './store-types.ts'
24
+ import type { Store } from './store.ts'
25
+
26
+ /**
27
+ * Default time to keep unused stores in cache.
28
+ *
29
+ * - Browser: 60 seconds (60,000 ms)
30
+ * - SSR: Infinity (disables disposal to avoid disposing stores before server render completes)
31
+ *
32
+ * @internal Exported primarily for testing purposes.
33
+ */
34
+ export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000
35
+
36
+ /**
37
+ * Configuration options for stores managed by a {@link StoreRegistry}.
38
+ *
39
+ * Extends {@link CreateStoreOptions} with registry-specific settings for caching and observability.
40
+ * Use with {@link storeOptions} helper to get full type inference when defining reusable store configurations.
41
+ *
42
+ * @typeParam TSchema - The LiveStore schema type
43
+ * @typeParam TContext - User-defined context attached to the store
44
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
45
+ *
46
+ * @see {@link storeOptions} for defining reusable store configurations
47
+ * @see {@link StoreRegistry} for managing store lifecycles
48
+ */
49
+ export interface RegistryStoreOptions<
50
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
51
+ TContext = {},
52
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
53
+ > extends CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> {
54
+ /**
55
+ * OpenTelemetry configuration for tracing store operations.
56
+ *
57
+ * When provided, store operations (boot, queries, commits) will be traced
58
+ * under the given root span context using the specified tracer.
59
+ */
60
+ otelOptions?: Partial<OtelOptions>
61
+ /**
62
+ * The time in milliseconds that this store should remain
63
+ * in memory after becoming unused. When this store becomes
64
+ * unused (no active retentions), it will be disposed after this duration.
65
+ *
66
+ * Stores transition to the unused state as soon as they have no
67
+ * active retentions, so when all components which use that store
68
+ * have unmounted.
69
+ *
70
+ * @remarks
71
+ * - Per-store values override the registry-level default (set via `StoreRegistry` constructor's
72
+ * `defaultOptions.unusedCacheTime`)
73
+ * - The value is fixed when the store is first loaded into the registry. If the same `storeId` is
74
+ * requested again with a different `unusedCacheTime`, the original value is kept.
75
+ * - If set to `Infinity`, will disable automatic disposal
76
+ * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
77
+ *
78
+ * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
79
+ * disposing stores before server render completes.
80
+ */
81
+ unusedCacheTime?: number
82
+ }
83
+
84
+ type StoreRegistryConfig = {
85
+ /**
86
+ * Default options that are applied to all stores when they are loaded.
87
+ *
88
+ * @remarks
89
+ * These are options that typically don't depend on the specific store being loaded:
90
+ * - Framework integration (`batchUpdates`)
91
+ * - Environment settings (`disableDevtools`, `debug`, `otelOptions`)
92
+ * - Behavior defaults (`confirmUnsavedChanges`, `unusedCacheTime`)
93
+ *
94
+ * Store-specific fields like `schema`, `adapter`, `storeId`, and `boot` are intentionally
95
+ * excluded since they vary per store definition.
96
+ */
97
+ defaultOptions?: Partial<
98
+ Pick<
99
+ RegistryStoreOptions,
100
+ 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'debug' | 'otelOptions' | 'unusedCacheTime'
101
+ >
102
+ >
103
+ /**
104
+ * Custom Effect context for all registry operations (loading, caching, etc.).
105
+ * When the context's scope closes, all managed stores are automatically shut down.
106
+ */
107
+ context?: Context.Context<Scope.Scope | OtelTracer.OtelTracer>
108
+ }
109
+
110
+ /**
111
+ * RcMap cache key that uses storeId for equality/hashing but carries full options.
112
+ * This allows RcMap to deduplicate by storeId while the lookup function has access to all options.
113
+ *
114
+ * @remarks
115
+ * Only `storeId` is used for equality and hashing. This means if `getOrLoadPromise` is called
116
+ * with different options (e.g., different `adapter`) but the same `storeId`, the cached store
117
+ * from the first call will be returned. This is intentional - a store's identity is determined
118
+ * solely by its `storeId`, and callers should not expect to get different stores by varying
119
+ * other options while keeping the same `storeId`.
120
+ */
121
+ class StoreCacheKey implements Equal.Equal {
122
+ readonly options: RegistryStoreOptions<any, any, any>
123
+
124
+ constructor(options: RegistryStoreOptions<any, any, any>) {
125
+ this.options = options
126
+ }
127
+
128
+ /**
129
+ * Equality is based solely on `storeId`. Other options in `RegistryStoreOptions` are ignored
130
+ * for cache key comparison. The first options used for a given `storeId` determine the
131
+ * store's configuration.
132
+ */
133
+ [Equal.symbol](that: Equal.Equal): boolean {
134
+ return that instanceof StoreCacheKey && this.options.storeId === that.options.storeId
135
+ }
136
+
137
+ [Hash.symbol](): number {
138
+ return Hash.string(this.options.storeId)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Store Registry coordinating store loading, caching, and retention
144
+ *
145
+ * @public
146
+ */
147
+ export class StoreRegistry {
148
+ /**
149
+ * Reference-counted cache mapping storeId to Store instances.
150
+ * Stores are created on first access and disposed after `unusedCacheTime` when all references are released.
151
+ */
152
+ readonly #rcMap: RcMap.RcMap<StoreCacheKey, Store<any, any>, UnknownError>
153
+
154
+ /**
155
+ * Effect context providing Scope and OtelTracer for all registry operations.
156
+ * When the context's scope closes, all managed stores are automatically shut down.
157
+ */
158
+ readonly #context: Context.Context<Scope.Scope | OtelTracer.OtelTracer>
159
+
160
+ /**
161
+ * Disposal callback for the runtime created by the registry.
162
+ * Undefined when caller provided their own services (caller owns cleanup in that case).
163
+ */
164
+ readonly #disposeOwnedRuntime: (() => Promise<void>) | undefined
165
+
166
+ /**
167
+ * In-flight loading promises keyed by storeId.
168
+ * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
169
+ */
170
+ readonly #loadingPromises: Map<string, Promise<Store<any, any>>> = new Map()
171
+
172
+ /**
173
+ * Creates a new StoreRegistry instance.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * const registry = new StoreRegistry({
178
+ * defaultOptions: {
179
+ * batchUpdates,
180
+ * unusedCacheTime: 30_000,
181
+ * }
182
+ * })
183
+ * ```
184
+ */
185
+ constructor(config: StoreRegistryConfig = {}) {
186
+ if (config.context !== undefined) {
187
+ this.#context = config.context
188
+ } else {
189
+ const ownedRuntime = ManagedRuntime.make(Layer.mergeAll(Layer.effect(Scope.Scope, Effect.scope), OtelLiveDummy))
190
+ this.#context = ownedRuntime.contextEffect.pipe(Effect.runSync)
191
+ this.#disposeOwnedRuntime = () => ownedRuntime.dispose()
192
+ }
193
+
194
+ this.#rcMap = RcMap.make({
195
+ lookup: ({ options }: StoreCacheKey) => {
196
+ // Merge registry defaults with call-site options (call-site takes precedence)
197
+ const mergedOptions = { ...config.defaultOptions, ...options }
198
+ return createStore(mergedOptions).pipe(
199
+ Effect.catchDefect((cause) => UnknownError.make({ cause })),
200
+ Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`),
201
+ Effect.provide(
202
+ Layer.mergeAll(
203
+ mergedOptions.logger ?? Layer.empty,
204
+ Layer.succeed(
205
+ References.MinimumLogLevel,
206
+ mergedOptions.logLevel ?? (isDevEnv() === true ? 'Debug' : 'Info'),
207
+ ),
208
+ ),
209
+ ),
210
+ provideOtel(
211
+ omitUndefineds({
212
+ parentSpanContext: mergedOptions.otelOptions?.rootSpanContext,
213
+ otelTracer: mergedOptions.otelOptions?.tracer,
214
+ }),
215
+ ),
216
+ )
217
+ },
218
+ idleTimeToLive: ({ options }: StoreCacheKey) =>
219
+ options.unusedCacheTime ?? config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
220
+ }).pipe(Effect.runSyncWith(this.#context))
221
+ }
222
+
223
+ /**
224
+ * Gets a cached store or loads a new one, with the store lifetime scoped to the caller.
225
+ *
226
+ * @typeParam TSchema - The schema type for the store
227
+ * @typeParam TContext - The context type for the store
228
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
229
+ * @returns An Effect that yields the store, scoped to the provided Scope
230
+ *
231
+ * @remarks
232
+ * - Stores are kept in cache and reused while any scope holds them
233
+ * - When the scope closes, the reference is released; the store is disposed after `unusedCacheTime`
234
+ * if no other scopes retain it
235
+ * - Concurrent calls with the same storeId share the same store instance
236
+ */
237
+ getOrLoad = <
238
+ TSchema extends LiveStoreSchema,
239
+ TContext = {},
240
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
241
+ >(
242
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
243
+ ): Effect.Effect<Store<TSchema, TContext>, UnknownError, Scope.Scope> =>
244
+ Effect.gen({ self: this }, function* () {
245
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
246
+ const key = new StoreCacheKey(options)
247
+ const store = yield* RcMap.get(this.#rcMap, key)
248
+
249
+ return store as Store<TSchema, TContext>
250
+ }).pipe(Effect.withSpan(`StoreRegistry.getOrLoad:${options.storeId}`))
251
+
252
+ /**
253
+ * Get or load a store, returning it directly if already loaded or a promise if loading.
254
+ *
255
+ * @typeParam TSchema - The schema type for the store
256
+ * @typeParam TContext - The context type for the store
257
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
258
+ * @returns The loaded store if available, or a Promise that resolves to the loaded store
259
+ * @throws unknown - store loading error
260
+ *
261
+ * @remarks
262
+ * - Returns the store instance directly (synchronous) when already loaded
263
+ * - Returns a stable Promise reference when loading is in progress or needs to be initiated
264
+ * - Throws with the same error instance on subsequent calls after failure
265
+ * - Applies default options from registry config, with call-site options taking precedence
266
+ * - Concurrent calls with the same storeId share the same store instance
267
+ */
268
+ getOrLoadPromise = <
269
+ TSchema extends LiveStoreSchema,
270
+ TContext = {},
271
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
272
+ >(
273
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
274
+ ): Store<TSchema, TContext> | Promise<Store<TSchema, TContext>> => {
275
+ const exit = this.getOrLoad(options).pipe(Effect.scoped, Effect.runSyncExitWith(this.#context))
276
+
277
+ if (Exit.isSuccess(exit) === true) return exit.value
278
+
279
+ // Check if the failure is due to async work
280
+ const defect = Cause.findDefect(exit.cause)
281
+ if (Result.isFailure(defect) === true) {
282
+ // Handle synchronous failure
283
+ throw Cause.squash(exit.cause)
284
+ }
285
+
286
+ if (Cause.isAsyncFiberError(defect.success) === false) {
287
+ // Handle synchronous failure
288
+ throw Cause.squash(exit.cause)
289
+ }
290
+
291
+ const { storeId } = options
292
+
293
+ // Return cached promise if one exists (ensures concurrent calls get the same Promise reference)
294
+ const cached = this.#loadingPromises.get(storeId)
295
+ if (cached !== undefined) return cached as Promise<Store<TSchema, TContext>>
296
+
297
+ // Create and cache the promise
298
+ const fiber = defect.success.fiber as Fiber.Fiber<Store<TSchema, TContext>>
299
+ const promise = Fiber.join(fiber)
300
+ .pipe(Effect.runPromiseWith(this.#context))
301
+ .finally(() => this.#loadingPromises.delete(storeId))
302
+
303
+ this.#loadingPromises.set(storeId, promise)
304
+ return promise
305
+ }
306
+
307
+ /**
308
+ * Retains the store in cache.
309
+ *
310
+ * @typeParam TSchema - The schema type for the store
311
+ * @typeParam TContext - The context type for the store
312
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
313
+ * @returns A release function that, when called, removes this retention hold
314
+ *
315
+ * @remarks
316
+ * - Multiple retains on the same store are independent; each must be released separately
317
+ * - If the store isn't cached yet, it will be loaded and then retained
318
+ * - The store will remain in cache until all retains are released and after `unusedCacheTime` expires
319
+ */
320
+ retain = <
321
+ TSchema extends LiveStoreSchema,
322
+ TContext = {},
323
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
324
+ >(
325
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
326
+ ): (() => void) => {
327
+ const release = Effect.gen({ self: this }, function* () {
328
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
329
+ const key = new StoreCacheKey(options)
330
+ yield* RcMap.get(this.#rcMap, key)
331
+ // Effect.never suspends indefinitely, keeping the RcMap reference alive.
332
+ // When `release()` is called, the fiber is interrupted, closing the scope
333
+ // and releasing the RcMap entry (which may trigger disposal after idleTimeToLive).
334
+ return yield* Effect.never
335
+ }).pipe(Effect.scoped, Effect.runCallbackWith(this.#context))
336
+
337
+ return () => release()
338
+ }
339
+
340
+ /**
341
+ * Loads a store (without suspending) to warm up the cache.
342
+ *
343
+ * @typeParam TSchema - The schema of the store to preload
344
+ * @typeParam TContext - The context type for the store
345
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
346
+ * @returns A promise that resolves when the loading is complete (success or failure)
347
+ *
348
+ * @remarks
349
+ * - We don't return the store or throw as this is a fire-and-forget operation.
350
+ * - If the entry remains unused after preload resolves/rejects, it is scheduled for disposal.
351
+ * - Does not affect the retention of the store in cache.
352
+ */
353
+ preload = async <
354
+ TSchema extends LiveStoreSchema,
355
+ TContext = {},
356
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
357
+ >(
358
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
359
+ ): Promise<void> => {
360
+ try {
361
+ await this.getOrLoadPromise(options)
362
+ } catch {
363
+ // Do nothing; preload is best-effort
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Disposes the registry and all its managed stores, immediately releasing resources
369
+ * (database connections, WebSocket connections, web workers, etc.).
370
+ *
371
+ * Most applications should use a single `StoreRegistry` and don't need to call
372
+ * this method. It's only necessary when creating multiple short-lived registries to
373
+ * immediately release resources and avoid conflicts with subsequent registries.
374
+ *
375
+ * @returns A promise that resolves when disposal is complete
376
+ *
377
+ * @remarks
378
+ * - No-op if a custom `context` was provided to the constructor (caller owns cleanup)
379
+ * - Idempotent: safe to call multiple times
380
+ * - After disposal, the registry should not be used
381
+ */
382
+ dispose = async (): Promise<void> => {
383
+ await this.#disposeOwnedRuntime?.()
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Helper for defining reusable store options with full type inference. Returns
389
+ * options that can be passed to `useStore()` or `storeRegistry.preload()`.
390
+ *
391
+ * @remarks
392
+ * At runtime this is an identity function that returns the input unchanged.
393
+ * Its value lies in enabling TypeScript's excess property checking to catch
394
+ * typos and configuration errors, while allowing options to be shared across
395
+ * `useStore()`, `storeRegistry.preload()`, `storeRegistry.getOrLoad()`, etc.
396
+ *
397
+ * @typeParam TSchema - The LiveStore schema type
398
+ * @typeParam TContext - User-defined context attached to the store
399
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
400
+ * @param options - The store configuration options
401
+ * @returns The same options object, unchanged
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * export const issueStoreOptions = (issueId: string) =>
406
+ * storeOptions({
407
+ * storeId: `issue-${issueId}`,
408
+ * schema,
409
+ * adapter,
410
+ * unusedCacheTime: 30_000,
411
+ * })
412
+ *
413
+ * // In a component
414
+ * const issueStore = useStore(issueStoreOptions(issueId))
415
+ *
416
+ * // In a route loader or event handler
417
+ * storeRegistry.preload({
418
+ * ...issueStoreOptions(issueId),
419
+ * unusedCacheTime: 10_000,
420
+ * });
421
+ * ```
422
+ */
423
+ export const storeOptions = <
424
+ TSchema extends LiveStoreSchema,
425
+ TContext = {},
426
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
427
+ >(
428
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
429
+ ): RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema> => options