@livestore/react 0.4.0-dev.20 → 0.4.0-dev.21

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 (40) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreContext.d.ts +27 -0
  3. package/dist/LiveStoreContext.d.ts.map +1 -1
  4. package/dist/LiveStoreContext.js +18 -0
  5. package/dist/LiveStoreContext.js.map +1 -1
  6. package/dist/LiveStoreProvider.d.ts +9 -2
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +2 -1
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/experimental/multi-store/StoreRegistry.d.ts +60 -16
  11. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
  12. package/dist/experimental/multi-store/StoreRegistry.js +125 -216
  13. package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
  14. package/dist/experimental/multi-store/StoreRegistry.test.js +224 -307
  15. package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -1
  16. package/dist/experimental/multi-store/types.d.ts +4 -23
  17. package/dist/experimental/multi-store/types.d.ts.map +1 -1
  18. package/dist/experimental/multi-store/useStore.d.ts +1 -1
  19. package/dist/experimental/multi-store/useStore.d.ts.map +1 -1
  20. package/dist/experimental/multi-store/useStore.js +5 -10
  21. package/dist/experimental/multi-store/useStore.js.map +1 -1
  22. package/dist/experimental/multi-store/useStore.test.js +95 -41
  23. package/dist/experimental/multi-store/useStore.test.js.map +1 -1
  24. package/dist/useClientDocument.d.ts +33 -0
  25. package/dist/useClientDocument.d.ts.map +1 -1
  26. package/dist/useClientDocument.js.map +1 -1
  27. package/dist/useStore.d.ts +51 -0
  28. package/dist/useStore.d.ts.map +1 -1
  29. package/dist/useStore.js +51 -0
  30. package/dist/useStore.js.map +1 -1
  31. package/package.json +6 -6
  32. package/src/LiveStoreContext.ts +27 -0
  33. package/src/LiveStoreProvider.tsx +9 -0
  34. package/src/experimental/multi-store/StoreRegistry.test.ts +236 -349
  35. package/src/experimental/multi-store/StoreRegistry.ts +171 -265
  36. package/src/experimental/multi-store/types.ts +31 -49
  37. package/src/experimental/multi-store/useStore.test.tsx +120 -48
  38. package/src/experimental/multi-store/useStore.ts +5 -13
  39. package/src/useClientDocument.ts +35 -0
  40. package/src/useStore.ts +51 -0
@@ -1,13 +1,21 @@
1
+ import { OtelLiveDummy, UnknownError } from '@livestore/common'
1
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
2
- import { createStorePromise, type Store, type Unsubscribe } from '@livestore/livestore'
3
- import type { CachedStoreOptions, StoreId } from './types.ts'
4
-
5
- type StoreEntryState<TSchema extends LiveStoreSchema> =
6
- | { status: 'idle' }
7
- | { status: 'loading'; promise: Promise<Store<TSchema>>; abortController: AbortController }
8
- | { status: 'success'; store: Store<TSchema> }
9
- | { status: 'error'; error: unknown }
10
- | { status: 'shutting_down'; shutdownPromise: Promise<void> }
3
+ import { createStore, type Store } from '@livestore/livestore'
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 Scope,
17
+ } from '@livestore/utils/effect'
18
+ import type { CachedStoreOptions } from './types.ts'
11
19
 
12
20
  /**
13
21
  * Default time to keep unused stores in cache.
@@ -19,310 +27,214 @@ type StoreEntryState<TSchema extends LiveStoreSchema> =
19
27
  */
20
28
  export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000
21
29
 
22
- /**
23
- * @typeParam TSchema - The schema for this entry's store.
24
- * @internal
25
- */
26
- class StoreEntry<TSchema extends LiveStoreSchema = LiveStoreSchema> {
27
- readonly #storeId: StoreId
28
- readonly #cache: StoreCache
29
-
30
- #state: StoreEntryState<TSchema> = { status: 'idle' }
31
-
32
- #unusedCacheTime?: number
33
- #disposalTimeout?: ReturnType<typeof setTimeout> | null
34
-
30
+ type DefaultStoreOptions = Partial<
31
+ Pick<
32
+ CachedStoreOptions,
33
+ 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'syncPayload' | 'debug' | 'otelOptions'
34
+ >
35
+ > & {
36
+ /**
37
+ * The time in milliseconds that unused stores remain in memory.
38
+ * When a store becomes unused (no active retentions), it will be disposed
39
+ * after this duration.
40
+ *
41
+ * Stores transition to the unused state as soon as they have no
42
+ * active retentions, so when all components which use that store
43
+ * have unmounted.
44
+ *
45
+ * @remarks
46
+ * - If set to `Infinity`, will disable disposal
47
+ * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
48
+ *
49
+ * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
50
+ * disposing stores before server render completes.
51
+ */
52
+ unusedCacheTime?: number
35
53
  /**
36
- * Set of subscriber callbacks to notify on state changes.
54
+ * Optionally, pass a custom runtime that will be used to run all operations (loading, caching, etc.).
37
55
  */
38
- readonly #subscribers = new Set<() => void>()
39
-
40
- constructor(storeId: StoreId, cache: StoreCache) {
41
- this.#storeId = storeId
42
- this.#cache = cache
43
- }
44
-
45
- #scheduleDisposal = (): void => {
46
- this.#cancelDisposal()
47
-
48
- const effectiveTime = this.#unusedCacheTime === undefined ? DEFAULT_UNUSED_CACHE_TIME : this.#unusedCacheTime
49
-
50
- if (effectiveTime === Number.POSITIVE_INFINITY) return // Infinity disables disposal
51
-
52
- this.#disposalTimeout = setTimeout(() => {
53
- this.#disposalTimeout = null
54
-
55
- // Re-check to avoid racing with a new subscription
56
- if (this.#subscribers.size > 0) return
57
-
58
- // Abort any in-progress loading to release resources early
59
- this.#abortLoading()
60
-
61
- // Transition to shutting_down state BEFORE starting async shutdown.
62
- // This prevents new subscribers from receiving a store that's about to be disposed.
63
- const shutdownPromise = this.#shutdown().finally(() => {
64
- // Reset to idle so fresh loads can proceed, then remove from cache if still inactive
65
- this.#setIdle()
66
- if (this.#subscribers.size === 0) this.#cache.delete(this.#storeId)
67
- })
56
+ runtime?: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
57
+ }
68
58
 
69
- this.#setShuttingDown(shutdownPromise)
70
- }, effectiveTime)
71
- }
59
+ /**
60
+ * RcMap cache key that uses storeId for equality/hashing but carries full options.
61
+ * This allows RcMap to deduplicate by storeId while the lookup function has access to all options.
62
+ *
63
+ * @remarks
64
+ * Only `storeId` is used for equality and hashing. This means if `getOrLoadPromise` is called
65
+ * with different options (e.g., different `adapter`) but the same `storeId`, the cached store
66
+ * from the first call will be returned. This is intentional - a store's identity is determined
67
+ * solely by its `storeId`, and callers should not expect to get different stores by varying
68
+ * other options while keeping the same `storeId`.
69
+ */
70
+ class StoreCacheKey<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> implements Equal.Equal {
71
+ readonly options: CachedStoreOptions<TSchema>
72
72
 
73
- #cancelDisposal = (): void => {
74
- if (!this.#disposalTimeout) return
75
- clearTimeout(this.#disposalTimeout)
76
- this.#disposalTimeout = null
73
+ constructor(options: CachedStoreOptions<TSchema>) {
74
+ this.options = options
77
75
  }
78
76
 
79
77
  /**
80
- * Transitions to the loading state.
78
+ * Equality is based solely on `storeId`. Other options in `CachedStoreOptions` are ignored
79
+ * for cache key comparison. The first options used for a given `storeId` determine the
80
+ * store's configuration.
81
81
  */
82
- #setLoading(promise: Promise<Store<TSchema>>, abortController: AbortController): void {
83
- if (this.#state.status === 'success' || this.#state.status === 'loading') return
84
- this.#state = { status: 'loading', promise, abortController }
85
- this.#notify()
82
+ [Equal.symbol](that: Equal.Equal): boolean {
83
+ return that instanceof StoreCacheKey && this.options.storeId === that.options.storeId
86
84
  }
87
85
 
88
- /**
89
- * Transitions to the success state.
90
- */
91
- #setStore = (store: Store<TSchema>): void => {
92
- this.#state = { status: 'success', store }
93
- this.#notify()
86
+ [Hash.symbol](): number {
87
+ return Hash.string(this.options.storeId)
94
88
  }
89
+ }
95
90
 
91
+ /**
92
+ * Store Registry coordinating store loading, caching, and retention
93
+ *
94
+ * @public
95
+ */
96
+ export class StoreRegistry {
96
97
  /**
97
- * Transitions to the error state.
98
+ * Reference-counted cache mapping storeId to Store instances.
99
+ * Stores are created on first access and disposed after `unusedCacheTime` when all references are released.
98
100
  */
99
- #setError = (error: unknown): void => {
100
- this.#state = { status: 'error', error }
101
- this.#notify()
102
- }
101
+ #rcMap: RcMap.RcMap<StoreCacheKey<any>, Store, UnknownError>
103
102
 
104
103
  /**
105
- * Transitions to the shutting_down state.
104
+ * Effect runtime providing Scope and OtelTracer for all registry operations.
105
+ * When the runtime's scope closes, all managed stores are automatically shut down.
106
106
  */
107
- #setShuttingDown = (shutdownPromise: Promise<void>): void => {
108
- this.#state = { status: 'shutting_down', shutdownPromise }
109
- this.#notify()
110
- }
107
+ #runtime: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
111
108
 
112
109
  /**
113
- * Transitions to the idle state.
110
+ * In-flight loading promises keyed by storeId.
111
+ * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
114
112
  */
115
- #setIdle = (): void => {
116
- this.#state = { status: 'idle' }
117
- // No notify needed - getOrLoad will handle the fresh load
118
- }
113
+ #loadingPromises: Map<string, Promise<Store<any>>> = new Map()
119
114
 
120
115
  /**
121
- * Notifies all subscribers of state changes.
116
+ * Creates a new StoreRegistry instance.
122
117
  *
123
- * @remarks
124
- * This should be called after any meaningful state change.
125
- */
126
- #notify = (): void => {
127
- for (const sub of this.#subscribers) {
128
- try {
129
- sub()
130
- } catch {
131
- // Swallow to protect other listeners
132
- }
133
- }
134
- }
135
-
136
- /**
137
- * Subscribes to this entry's updates.
118
+ * @param params.defaultOptions - Default options applied to all stores managed by this registry when they are loaded.
138
119
  *
139
- * @param listener - Callback invoked when the entry changes
140
- * @returns Unsubscribe function
120
+ * @example
121
+ * ```ts
122
+ * const registry = new StoreRegistry({
123
+ * defaultOptions: {
124
+ * batchUpdates,
125
+ * unusedCacheTime: 30_000,
126
+ * }
127
+ * })
128
+ * ```
141
129
  */
142
- subscribe = (listener: () => void): Unsubscribe => {
143
- this.#cancelDisposal()
144
- this.#subscribers.add(listener)
145
- return () => {
146
- this.#subscribers.delete(listener)
147
- // If no more subscribers remain, schedule disposal
148
- if (this.#subscribers.size === 0) this.#scheduleDisposal()
149
- }
130
+ constructor(params: { defaultOptions?: DefaultStoreOptions } = {}) {
131
+ this.#runtime =
132
+ params.defaultOptions?.runtime ??
133
+ ManagedRuntime.make(Layer.mergeAll(Layer.scope, OtelLiveDummy)).runtimeEffect.pipe(Effect.runSync)
134
+
135
+ this.#rcMap = RcMap.make({
136
+ lookup: (key: StoreCacheKey) =>
137
+ Effect.gen(this, function* () {
138
+ const { options } = key
139
+
140
+ return yield* createStore(options).pipe(Effect.catchAllDefect((cause) => UnknownError.make({ cause })))
141
+ }).pipe(Effect.withSpan(`StoreRegistry.lookup:${key.options.storeId}`)),
142
+ // TODO: Make idleTimeToLive vary for each store when Effect supports per-resource TTL
143
+ // See https://github.com/livestorejs/livestore/issues/917
144
+ idleTimeToLive: params.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
145
+ }).pipe(Runtime.runSync(this.#runtime))
150
146
  }
151
147
 
152
148
  /**
153
- * Gets the loaded store or initiates loading if not already in progress.
149
+ * Gets a cached store or loads a new one, with the store lifetime scoped to the caller.
154
150
  *
155
- * @param options - Store creation options
156
- * @returns The loaded store if available, or a Promise that resolves to the loaded store
151
+ * @typeParam TSchema - The schema type for the store
152
+ * @returns An Effect that yields the store, scoped to the provided Scope
157
153
  *
158
154
  * @remarks
159
- * This method handles the complete lifecycle of loading a store:
160
- * - Returns the store directly if already loaded (synchronous)
161
- * - Returns a Promise if loading is in progress or needs to be initiated
162
- * - Transitions through loading success/error states
163
- * - Schedules disposal when loading completes without active subscribers
155
+ * - Stores are kept in cache and reused while any scope holds them
156
+ * - When the scope closes, the reference is released; the store is disposed after `unusedCacheTime`
157
+ * if no other scopes retain it
158
+ * - Concurrent calls with the same storeId share the same store instance
164
159
  */
165
- getOrLoad = (options: CachedStoreOptions<TSchema>): Store<TSchema> | Promise<Store<TSchema>> => {
166
- if (options.unusedCacheTime !== undefined)
167
- this.#unusedCacheTime = Math.max(this.#unusedCacheTime ?? 0, options.unusedCacheTime)
168
-
169
- if (this.#state.status === 'success') return this.#state.store
170
- if (this.#state.status === 'loading') return this.#state.promise
171
- if (this.#state.status === 'error') throw this.#state.error
172
-
173
- // Wait for shutdown to complete, then recursively call to load a fresh store
174
- if (this.#state.status === 'shutting_down') {
175
- return this.#state.shutdownPromise.then(() => this.getOrLoad(options))
176
- }
177
-
178
- const abortController = new AbortController()
179
-
180
- const promise = createStorePromise({ ...options, signal: abortController.signal })
181
- .then((store) => {
182
- this.#setStore(store)
183
- return store
184
- })
185
- .catch((error) => {
186
- this.#setError(error)
187
- throw error
188
- })
189
- .finally(() => {
190
- // The store entry may have become unused (no subscribers) while loading the store
191
- if (this.#subscribers.size === 0) this.#scheduleDisposal()
192
- })
193
-
194
- this.#setLoading(promise, abortController)
160
+ getOrLoad = <TSchema extends LiveStoreSchema>(
161
+ options: CachedStoreOptions<TSchema>,
162
+ ): Effect.Effect<Store<TSchema>, UnknownError, Scope.Scope> =>
163
+ Effect.gen(this, function* () {
164
+ const key = new StoreCacheKey(options)
165
+ const store = yield* RcMap.get(this.#rcMap, key)
195
166
 
196
- return promise
197
- }
167
+ return store as unknown as Store<TSchema>
168
+ }).pipe(Effect.withSpan(`StoreRegistry.getOrLoad:${options.storeId}`))
198
169
 
199
170
  /**
200
- * Aborts an in-progress store load.
171
+ * Get or load a store, returning it directly if already loaded or a promise if loading.
201
172
  *
202
- * This signals the createStorePromise to cancel, releasing resources like
203
- * worker threads, SQLite connections, and network requests.
173
+ * @typeParam TSchema - The schema type for the store
174
+ * @returns The loaded store if available, or a Promise that resolves to the loaded store
175
+ * @throws unknown loading error
176
+ *
177
+ * @remarks
178
+ * - Returns the store instance directly (synchronous) when already loaded
179
+ * - Returns a stable Promise reference when loading is in progress or needs to be initiated
180
+ * - Throws with the same error instance on subsequent calls after failure
181
+ * - Applies default options from registry config, with call-site options taking precedence
182
+ * - Concurrent calls with the same storeId share the same store instance
204
183
  */
205
- #abortLoading = (): void => {
206
- if (this.#state.status !== 'loading') return
207
- this.#state.abortController.abort()
208
- }
184
+ getOrLoadPromise = <TSchema extends LiveStoreSchema>(
185
+ options: CachedStoreOptions<TSchema>,
186
+ ): Store<TSchema> | Promise<Store<TSchema>> => {
187
+ const exit = this.getOrLoad<TSchema>(options).pipe(Effect.scoped, Runtime.runSyncExit(this.#runtime))
209
188
 
210
- #shutdown = async (): Promise<void> => {
211
- if (this.#state.status !== 'success') return
212
- await this.#state.store.shutdownPromise().catch((reason) => {
213
- console.warn(`Store ${this.#storeId} failed to shutdown cleanly during disposal:`, reason)
214
- })
215
- }
216
- }
189
+ if (Exit.isSuccess(exit)) return exit.value as Store<TSchema>
217
190
 
218
- /**
219
- * In-memory map of {@link StoreEntry} instances keyed by {@link StoreId}.
220
- *
221
- * @privateRemarks
222
- * The cache is intentionally small; eviction and disposal timers are coordinated by the client.
223
- *
224
- * @internal
225
- */
226
- class StoreCache {
227
- readonly #entries = new Map<StoreId, StoreEntry>()
191
+ // Check if the failure is due to async work
192
+ const defect = Cause.dieOption(exit.cause)
193
+ if (defect._tag === 'Some' && Runtime.isAsyncFiberException(defect.value)) {
194
+ const { storeId } = options
228
195
 
229
- get = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> | undefined => {
230
- return this.#entries.get(storeId) as StoreEntry<TSchema> | undefined
231
- }
196
+ // Return cached promise if one exists (ensures concurrent calls get the same Promise reference)
197
+ const cached = this.#loadingPromises.get(storeId)
198
+ if (cached) return cached as Promise<Store<TSchema>>
232
199
 
233
- ensure = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> => {
234
- let entry = this.#entries.get(storeId) as StoreEntry<TSchema> | undefined
200
+ // Create and cache the promise
201
+ const fiber = defect.value.fiber
202
+ const promise = Fiber.join(fiber)
203
+ .pipe(Runtime.runPromise(this.#runtime))
204
+ .finally(() => this.#loadingPromises.delete(storeId)) as Promise<Store<TSchema>>
235
205
 
236
- if (!entry) {
237
- entry = new StoreEntry<TSchema>(storeId, this)
238
- this.#entries.set(storeId, entry as unknown as StoreEntry)
206
+ this.#loadingPromises.set(storeId, promise)
207
+ return promise
239
208
  }
240
209
 
241
- return entry
242
- }
243
-
244
- /**
245
- * Removes an entry from the cache.
246
- *
247
- * @param storeId - The ID of the store to remove
248
- */
249
- delete = (storeId: StoreId): void => {
250
- this.#entries.delete(storeId)
251
- }
252
- }
253
-
254
- type DefaultStoreOptions = Partial<
255
- Pick<
256
- CachedStoreOptions<any>,
257
- 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'syncPayload' | 'debug' | 'otelOptions'
258
- >
259
- > & {
260
- /**
261
- * The time in milliseconds that unused stores remain in memory.
262
- * When a store becomes unused (no subscribers), it will be disposed
263
- * after this duration.
264
- *
265
- * Stores transition to the unused state as soon as they have no
266
- * subscriptions registered, so when all components which use that
267
- * store have unmounted.
268
- *
269
- * @remarks
270
- * - If set to `Infinity`, will disable disposal
271
- * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
272
- *
273
- * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
274
- * disposing stores before server render completes.
275
- */
276
- unusedCacheTime?: number
277
- }
278
-
279
- type StoreRegistryConfig = {
280
- defaultOptions?: DefaultStoreOptions
281
- }
282
-
283
- /**
284
- * Store Registry coordinating store loading, caching, and subscription
285
- *
286
- * @public
287
- */
288
- export class StoreRegistry {
289
- readonly #cache = new StoreCache()
290
- readonly #defaultOptions: DefaultStoreOptions
291
-
292
- constructor({ defaultOptions = {} }: StoreRegistryConfig = {}) {
293
- this.#defaultOptions = defaultOptions
210
+ // Handle synchronous failure
211
+ throw Cause.squash(exit.cause)
294
212
  }
295
213
 
296
- #applyDefaultOptions = <TSchema extends LiveStoreSchema>(
297
- options: CachedStoreOptions<TSchema>,
298
- ): CachedStoreOptions<TSchema> => ({
299
- ...this.#defaultOptions,
300
- ...options,
301
- })
302
-
303
214
  /**
304
- * Get or load a store, returning it directly if loaded or a promise if loading.
215
+ * Retains the store in cache until the returned release function is called.
305
216
  *
306
- * @typeParam TSchema - The schema of the store to load
307
- * @returns The loaded store if available, or a Promise that resolves to the loaded store
308
- * @throws unknown loading error
217
+ * @returns A release function that, when called, removes this retention hold
309
218
  *
310
219
  * @remarks
311
- * - Returns the store instance directly (synchronous) when already loaded
312
- * - Returns a stable Promise reference when loading is in progress or needs to be initiated
313
- * - Applies default options from registry config, with call-site options taking precedence
220
+ * - Multiple retains on the same store are independent; each must be released separately
221
+ * - If the store isn't cached yet, it will be loaded and then retained
314
222
  */
315
- getOrLoad = <TSchema extends LiveStoreSchema>(
316
- options: CachedStoreOptions<TSchema>,
317
- ): Store<TSchema> | Promise<Store<TSchema>> => {
318
- const optionsWithDefaults = this.#applyDefaultOptions(options)
319
- const storeEntry = this.#cache.ensure<TSchema>(optionsWithDefaults.storeId)
223
+ retain = (options: CachedStoreOptions<any>): (() => void) => {
224
+ const release = Effect.gen(this, function* () {
225
+ const key = new StoreCacheKey(options)
226
+ yield* RcMap.get(this.#rcMap, key)
227
+ // Effect.never suspends indefinitely, keeping the RcMap reference alive.
228
+ // When `release()` is called, the fiber is interrupted, closing the scope
229
+ // and releasing the RcMap entry (which may trigger disposal after idleTimeToLive).
230
+ yield* Effect.never
231
+ }).pipe(Effect.scoped, Runtime.runCallback(this.#runtime))
320
232
 
321
- return storeEntry.getOrLoad(optionsWithDefaults)
233
+ return () => release()
322
234
  }
323
235
 
324
236
  /**
325
- * Warms the cache for a store without mounting a subscriber.
237
+ * Warms the cache for a store without adding a retention.
326
238
  *
327
239
  * @typeParam TSchema - The schema of the store to preload
328
240
  * @returns A promise that resolves when the loading is complete (success or failure)
@@ -333,15 +245,9 @@ export class StoreRegistry {
333
245
  */
334
246
  preload = async <TSchema extends LiveStoreSchema>(options: CachedStoreOptions<TSchema>): Promise<void> => {
335
247
  try {
336
- await this.getOrLoad(options)
248
+ await this.getOrLoadPromise(options)
337
249
  } catch {
338
250
  // Do nothing; preload is best-effort
339
251
  }
340
252
  }
341
-
342
- subscribe = <TSchema extends LiveStoreSchema>(storeId: StoreId, listener: () => void): Unsubscribe => {
343
- const entry = this.#cache.ensure<TSchema>(storeId)
344
-
345
- return entry.subscribe(listener)
346
- }
347
253
  }
@@ -1,55 +1,37 @@
1
- import type { Adapter } from '@livestore/common'
2
1
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
2
  import type { CreateStoreOptions, OtelOptions } from '@livestore/livestore'
4
3
 
5
- export type StoreId = string
6
-
7
- /**
8
- * Minimum information required to create a store
9
- */
10
- export type StoreDescriptor<TSchema extends LiveStoreSchema> = {
11
- /**
12
- * Schema describing the data structure.
13
- */
14
- readonly schema: TSchema
15
-
4
+ export type CachedStoreOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> = Pick<
5
+ CreateStoreOptions<TSchema, TContext>,
6
+ | 'storeId'
7
+ | 'schema'
8
+ | 'adapter'
9
+ | 'boot'
10
+ | 'batchUpdates'
11
+ | 'disableDevtools'
12
+ | 'confirmUnsavedChanges'
13
+ | 'syncPayload'
14
+ | 'debug'
15
+ | 'shutdownDeferred'
16
+ > & {
17
+ signal?: AbortSignal
18
+ otelOptions?: Partial<OtelOptions>
16
19
  /**
17
- * Adapter for persistence and synchronization.
20
+ * The time in milliseconds that this store should remain
21
+ * in memory after becoming unused. When this store becomes
22
+ * unused (no active retentions), it will be disposed after this duration.
23
+ *
24
+ * Stores transition to the unused state as soon as they have no
25
+ * active retentions, so when all components which use that store
26
+ * have unmounted.
27
+ *
28
+ * @remarks
29
+ * - When different `unusedCacheTime` values are used for the same store, the longest one will be used.
30
+ * - If set to `Infinity`, will disable automatic disposal
31
+ * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
32
+ *
33
+ * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
34
+ * disposing stores before server render completes.
18
35
  */
19
- readonly adapter: Adapter
20
-
21
- /**
22
- * The ID of the store.
23
- */
24
- readonly storeId: StoreId
36
+ unusedCacheTime?: number
25
37
  }
26
-
27
- export type CachedStoreOptions<
28
- TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
29
- TContext = {},
30
- > = StoreDescriptor<TSchema> &
31
- Pick<
32
- CreateStoreOptions<TSchema, TContext>,
33
- 'boot' | 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'syncPayload' | 'debug'
34
- > & {
35
- signal?: AbortSignal
36
- otelOptions?: Partial<OtelOptions>
37
- /**
38
- * The time in milliseconds that this store should remain
39
- * in memory after becoming unused. When this store becomes
40
- * unused (no subscribers), it will be disposed after this duration.
41
- *
42
- * Stores transition to the unused state as soon as they have no
43
- * subscriptions registered, so when all components which use that
44
- * store have unmounted.
45
- *
46
- * @remarks
47
- * - When different `unusedCacheTime` values are used for the same store, the longest one will be used.
48
- * - If set to `Infinity`, will disable automatic disposal
49
- * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
50
- *
51
- * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
52
- * disposing stores before server render completes.
53
- */
54
- unusedCacheTime?: number
55
- }