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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +2 -2
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +130 -2
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +185 -6
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +42 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +3 -3
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +1 -1
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts.map +1 -1
  32. package/dist/live-queries/computed.js +2 -2
  33. package/dist/live-queries/computed.js.map +1 -1
  34. package/dist/live-queries/db-query.js +14 -14
  35. package/dist/live-queries/db-query.js.map +1 -1
  36. package/dist/live-queries/db-query.test.js +2 -2
  37. package/dist/live-queries/db-query.test.js.map +1 -1
  38. package/dist/live-queries/signal.test.js +2 -2
  39. package/dist/live-queries/signal.test.js.map +1 -1
  40. package/dist/mod.d.ts +2 -1
  41. package/dist/mod.d.ts.map +1 -1
  42. package/dist/mod.js +1 -0
  43. package/dist/mod.js.map +1 -1
  44. package/dist/reactive.d.ts +9 -9
  45. package/dist/reactive.d.ts.map +1 -1
  46. package/dist/reactive.js +9 -26
  47. package/dist/reactive.js.map +1 -1
  48. package/dist/reactive.test.js +2 -2
  49. package/dist/reactive.test.js.map +1 -1
  50. package/dist/store/StoreRegistry.d.ts +215 -0
  51. package/dist/store/StoreRegistry.d.ts.map +1 -0
  52. package/dist/store/StoreRegistry.js +267 -0
  53. package/dist/store/StoreRegistry.js.map +1 -0
  54. package/dist/store/StoreRegistry.test.d.ts +2 -0
  55. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  56. package/dist/store/StoreRegistry.test.js +381 -0
  57. package/dist/store/StoreRegistry.test.js.map +1 -0
  58. package/dist/store/create-store.d.ts +56 -6
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +32 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +1 -1
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +16 -3
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-eventstream.test.js +2 -2
  67. package/dist/store/store-eventstream.test.js.map +1 -1
  68. package/dist/store/store-types.d.ts +59 -9
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store-types.test.js +1 -1
  72. package/dist/store/store-types.test.js.map +1 -1
  73. package/dist/store/store.d.ts +102 -6
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +148 -47
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/dev.js.map +1 -1
  78. package/dist/utils/stack-info.js +2 -2
  79. package/dist/utils/stack-info.js.map +1 -1
  80. package/dist/utils/tests/fixture.d.ts +1 -1
  81. package/dist/utils/tests/fixture.d.ts.map +1 -1
  82. package/dist/utils/tests/fixture.js.map +1 -1
  83. package/dist/utils/tests/otel.d.ts.map +1 -1
  84. package/dist/utils/tests/otel.js +5 -5
  85. package/dist/utils/tests/otel.js.map +1 -1
  86. package/package.json +59 -18
  87. package/src/QueryCache.ts +1 -1
  88. package/src/SqliteDbWrapper.test.ts +4 -2
  89. package/src/SqliteDbWrapper.ts +12 -11
  90. package/src/ambient.d.ts +0 -7
  91. package/src/effect/LiveStore.test.ts +61 -0
  92. package/src/effect/LiveStore.ts +381 -8
  93. package/src/effect/mod.ts +13 -1
  94. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  95. package/src/live-queries/base-class.ts +7 -6
  96. package/src/live-queries/client-document-get-query.ts +4 -2
  97. package/src/live-queries/computed.ts +3 -2
  98. package/src/live-queries/db-query.test.ts +3 -2
  99. package/src/live-queries/db-query.ts +15 -15
  100. package/src/live-queries/signal.test.ts +3 -2
  101. package/src/mod.ts +2 -0
  102. package/src/reactive.test.ts +3 -2
  103. package/src/reactive.ts +22 -23
  104. package/src/store/StoreRegistry.test.ts +540 -0
  105. package/src/store/StoreRegistry.ts +418 -0
  106. package/src/store/create-store.ts +76 -15
  107. package/src/store/devtools.ts +20 -6
  108. package/src/store/store-eventstream.test.ts +4 -2
  109. package/src/store/store-types.test.ts +3 -1
  110. package/src/store/store-types.ts +64 -13
  111. package/src/store/store.ts +197 -60
  112. package/src/utils/dev.ts +2 -2
  113. package/src/utils/stack-info.ts +2 -2
  114. package/src/utils/tests/fixture.ts +2 -1
  115. package/src/utils/tests/otel.ts +8 -7
  116. package/docs/api/index.md +0 -3
  117. package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
  118. package/docs/building-with-livestore/crud/index.md +0 -5
  119. package/docs/building-with-livestore/data-modeling/index.md +0 -1
  120. package/docs/building-with-livestore/debugging/index.md +0 -17
  121. package/docs/building-with-livestore/devtools/index.md +0 -79
  122. package/docs/building-with-livestore/events/index.md +0 -355
  123. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  124. package/docs/building-with-livestore/examples/index.md +0 -30
  125. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
  126. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  127. package/docs/building-with-livestore/opentelemetry/index.md +0 -208
  128. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  129. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  130. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  131. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  132. package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
  133. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  134. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  135. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  136. package/docs/building-with-livestore/store/index.md +0 -281
  137. package/docs/building-with-livestore/syncing/index.md +0 -136
  138. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  139. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  140. package/docs/examples/cloudflare-adapter/index.md +0 -44
  141. package/docs/examples/expo-adapter/index.md +0 -44
  142. package/docs/examples/index.md +0 -55
  143. package/docs/examples/node-adapter/index.md +0 -44
  144. package/docs/examples/web-adapter/index.md +0 -52
  145. package/docs/framework-integrations/custom-elements/index.md +0 -142
  146. package/docs/framework-integrations/react-integration/index.md +0 -918
  147. package/docs/framework-integrations/solid-integration/index.md +0 -293
  148. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  149. package/docs/framework-integrations/vue-integration/index.md +0 -294
  150. package/docs/getting-started/expo/index.md +0 -736
  151. package/docs/getting-started/node/index.md +0 -115
  152. package/docs/getting-started/react-web/index.md +0 -573
  153. package/docs/getting-started/solid/index.md +0 -3
  154. package/docs/getting-started/vue/index.md +0 -471
  155. package/docs/index.md +0 -209
  156. package/docs/llms.txt +0 -147
  157. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  158. package/docs/misc/FAQ/index.md +0 -37
  159. package/docs/misc/community/index.md +0 -88
  160. package/docs/misc/credits/index.md +0 -14
  161. package/docs/misc/design-partners/index.md +0 -13
  162. package/docs/misc/package-management/index.md +0 -21
  163. package/docs/misc/performance/index.md +0 -25
  164. package/docs/misc/resources/index.md +0 -46
  165. package/docs/misc/state-of-the-project/index.md +0 -37
  166. package/docs/misc/troubleshooting/index.md +0 -82
  167. package/docs/overview/concepts/index.md +0 -78
  168. package/docs/overview/how-livestore-works/index.md +0 -56
  169. package/docs/overview/introduction/index.md +0 -5
  170. package/docs/overview/technology-comparison/index.md +0 -40
  171. package/docs/overview/when-livestore/index.md +0 -81
  172. package/docs/overview/why-livestore/index.md +0 -5
  173. package/docs/patterns/ai/index.md +0 -15
  174. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  175. package/docs/patterns/app-evolution/index.md +0 -72
  176. package/docs/patterns/auth/index.md +0 -226
  177. package/docs/patterns/effect/index.md +0 -1495
  178. package/docs/patterns/encryption/index.md +0 -6
  179. package/docs/patterns/external-data/index.md +0 -5
  180. package/docs/patterns/file-management/index.md +0 -11
  181. package/docs/patterns/file-structure/index.md +0 -14
  182. package/docs/patterns/list-ordering/index.md +0 -369
  183. package/docs/patterns/offline/index.md +0 -32
  184. package/docs/patterns/orm/index.md +0 -18
  185. package/docs/patterns/presence/index.md +0 -11
  186. package/docs/patterns/rich-text-editing/index.md +0 -11
  187. package/docs/patterns/server-side-clients/index.md +0 -97
  188. package/docs/patterns/side-effects/index.md +0 -11
  189. package/docs/patterns/state-machines/index.md +0 -11
  190. package/docs/patterns/storybook/index.md +0 -192
  191. package/docs/patterns/undo-redo/index.md +0 -9
  192. package/docs/patterns/version-control/index.md +0 -8
  193. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  194. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  195. package/docs/platform-adapters/expo-adapter/index.md +0 -245
  196. package/docs/platform-adapters/node-adapter/index.md +0 -160
  197. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  198. package/docs/platform-adapters/web-adapter/index.md +0 -218
  199. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  200. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  201. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  202. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  203. package/docs/sync-providers/cloudflare/index.md +0 -773
  204. package/docs/sync-providers/custom/index.md +0 -65
  205. package/docs/sync-providers/electricsql/index.md +0 -159
  206. package/docs/sync-providers/s2/index.md +0 -230
  207. package/docs/tutorial/0-welcome/index.md +0 -48
  208. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  209. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  210. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
  211. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  212. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  213. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  214. package/docs/tutorial/7-next-steps/index.md +0 -22
  215. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  216. package/docs/understanding-livestore/event-sourcing/index.md +0 -40
@@ -0,0 +1,267 @@
1
+ import { LogConfig, OtelLiveDummy, provideOtel, UnknownError } from '@livestore/common';
2
+ import { omitUndefineds } from '@livestore/utils';
3
+ import { Cause, Effect, Equal, Exit, Fiber, Hash, Layer, ManagedRuntime, RcMap, Runtime, } from '@livestore/utils/effect';
4
+ import { createStore } from "./create-store.js";
5
+ /**
6
+ * Default time to keep unused stores in cache.
7
+ *
8
+ * - Browser: 60 seconds (60,000 ms)
9
+ * - SSR: Infinity (disables disposal to avoid disposing stores before server render completes)
10
+ *
11
+ * @internal Exported primarily for testing purposes.
12
+ */
13
+ export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000;
14
+ /**
15
+ * RcMap cache key that uses storeId for equality/hashing but carries full options.
16
+ * This allows RcMap to deduplicate by storeId while the lookup function has access to all options.
17
+ *
18
+ * @remarks
19
+ * Only `storeId` is used for equality and hashing. This means if `getOrLoadPromise` is called
20
+ * with different options (e.g., different `adapter`) but the same `storeId`, the cached store
21
+ * from the first call will be returned. This is intentional - a store's identity is determined
22
+ * solely by its `storeId`, and callers should not expect to get different stores by varying
23
+ * other options while keeping the same `storeId`.
24
+ */
25
+ class StoreCacheKey {
26
+ options;
27
+ constructor(options) {
28
+ this.options = options;
29
+ }
30
+ /**
31
+ * Equality is based solely on `storeId`. Other options in `RegistryStoreOptions` are ignored
32
+ * for cache key comparison. The first options used for a given `storeId` determine the
33
+ * store's configuration.
34
+ */
35
+ [Equal.symbol](that) {
36
+ return that instanceof StoreCacheKey && this.options.storeId === that.options.storeId;
37
+ }
38
+ [Hash.symbol]() {
39
+ return Hash.string(this.options.storeId);
40
+ }
41
+ }
42
+ /**
43
+ * Store Registry coordinating store loading, caching, and retention
44
+ *
45
+ * @public
46
+ */
47
+ export class StoreRegistry {
48
+ /**
49
+ * Reference-counted cache mapping storeId to Store instances.
50
+ * Stores are created on first access and disposed after `unusedCacheTime` when all references are released.
51
+ */
52
+ #rcMap;
53
+ /**
54
+ * Effect runtime providing Scope and OtelTracer for all registry operations.
55
+ * When the runtime's scope closes, all managed stores are automatically shut down.
56
+ */
57
+ #runtime;
58
+ /**
59
+ * Disposal callback for the runtime created by the registry.
60
+ * Undefined when caller provided their own runtime (caller owns cleanup in that case).
61
+ */
62
+ #disposeOwnedRuntime;
63
+ /**
64
+ * In-flight loading promises keyed by storeId.
65
+ * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
66
+ */
67
+ #loadingPromises = new Map();
68
+ /**
69
+ * Creates a new StoreRegistry instance.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const registry = new StoreRegistry({
74
+ * defaultOptions: {
75
+ * batchUpdates,
76
+ * unusedCacheTime: 30_000,
77
+ * }
78
+ * })
79
+ * ```
80
+ */
81
+ constructor(config = {}) {
82
+ if (config.runtime !== undefined) {
83
+ this.#runtime = config.runtime;
84
+ }
85
+ else {
86
+ const ownedRuntime = ManagedRuntime.make(Layer.mergeAll(Layer.scope, OtelLiveDummy));
87
+ this.#runtime = ownedRuntime.runtimeEffect.pipe(Effect.runSync);
88
+ this.#disposeOwnedRuntime = () => ownedRuntime.dispose();
89
+ }
90
+ this.#rcMap = RcMap.make({
91
+ lookup: ({ options }) => {
92
+ // Merge registry defaults with call-site options (call-site takes precedence)
93
+ const mergedOptions = { ...config.defaultOptions, ...options };
94
+ return createStore(mergedOptions).pipe(Effect.catchAllDefect((cause) => UnknownError.make({ cause })), Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`), LogConfig.withLoggerConfig(mergedOptions, { threadName: 'window' }), provideOtel(omitUndefineds({
95
+ parentSpanContext: mergedOptions.otelOptions?.rootSpanContext,
96
+ otelTracer: mergedOptions.otelOptions?.tracer,
97
+ })));
98
+ },
99
+ idleTimeToLive: ({ options }) => options.unusedCacheTime ?? config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
100
+ }).pipe(Runtime.runSync(this.#runtime));
101
+ }
102
+ /**
103
+ * Gets a cached store or loads a new one, with the store lifetime scoped to the caller.
104
+ *
105
+ * @typeParam TSchema - The schema type for the store
106
+ * @typeParam TContext - The context type for the store
107
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
108
+ * @returns An Effect that yields the store, scoped to the provided Scope
109
+ *
110
+ * @remarks
111
+ * - Stores are kept in cache and reused while any scope holds them
112
+ * - When the scope closes, the reference is released; the store is disposed after `unusedCacheTime`
113
+ * if no other scopes retain it
114
+ * - Concurrent calls with the same storeId share the same store instance
115
+ */
116
+ getOrLoad = (options) => Effect.gen(this, function* () {
117
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
118
+ const key = new StoreCacheKey(options);
119
+ const store = yield* RcMap.get(this.#rcMap, key);
120
+ return store;
121
+ }).pipe(Effect.withSpan(`StoreRegistry.getOrLoad:${options.storeId}`));
122
+ /**
123
+ * Get or load a store, returning it directly if already loaded or a promise if loading.
124
+ *
125
+ * @typeParam TSchema - The schema type for the store
126
+ * @typeParam TContext - The context type for the store
127
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
128
+ * @returns The loaded store if available, or a Promise that resolves to the loaded store
129
+ * @throws unknown - store loading error
130
+ *
131
+ * @remarks
132
+ * - Returns the store instance directly (synchronous) when already loaded
133
+ * - Returns a stable Promise reference when loading is in progress or needs to be initiated
134
+ * - Throws with the same error instance on subsequent calls after failure
135
+ * - Applies default options from registry config, with call-site options taking precedence
136
+ * - Concurrent calls with the same storeId share the same store instance
137
+ */
138
+ getOrLoadPromise = (options) => {
139
+ const exit = this.getOrLoad(options).pipe(Effect.scoped, Runtime.runSyncExit(this.#runtime));
140
+ if (Exit.isSuccess(exit) === true)
141
+ return exit.value;
142
+ // Check if the failure is due to async work
143
+ const defect = Cause.dieOption(exit.cause);
144
+ if (defect._tag !== 'Some') {
145
+ // Handle synchronous failure
146
+ throw Cause.squash(exit.cause);
147
+ }
148
+ if (Runtime.isAsyncFiberException(defect.value) === false) {
149
+ // Handle synchronous failure
150
+ throw Cause.squash(exit.cause);
151
+ }
152
+ const { storeId } = options;
153
+ // Return cached promise if one exists (ensures concurrent calls get the same Promise reference)
154
+ const cached = this.#loadingPromises.get(storeId);
155
+ if (cached !== undefined)
156
+ return cached;
157
+ // Create and cache the promise
158
+ const fiber = defect.value.fiber;
159
+ const promise = Fiber.join(fiber)
160
+ .pipe(Runtime.runPromise(this.#runtime))
161
+ .finally(() => this.#loadingPromises.delete(storeId));
162
+ this.#loadingPromises.set(storeId, promise);
163
+ return promise;
164
+ };
165
+ /**
166
+ * Retains the store in cache.
167
+ *
168
+ * @typeParam TSchema - The schema type for the store
169
+ * @typeParam TContext - The context type for the store
170
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
171
+ * @returns A release function that, when called, removes this retention hold
172
+ *
173
+ * @remarks
174
+ * - Multiple retains on the same store are independent; each must be released separately
175
+ * - If the store isn't cached yet, it will be loaded and then retained
176
+ * - The store will remain in cache until all retains are released and after `unusedCacheTime` expires
177
+ */
178
+ retain = (options) => {
179
+ const release = Effect.gen(this, function* () {
180
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
181
+ const key = new StoreCacheKey(options);
182
+ yield* RcMap.get(this.#rcMap, key);
183
+ // Effect.never suspends indefinitely, keeping the RcMap reference alive.
184
+ // When `release()` is called, the fiber is interrupted, closing the scope
185
+ // and releasing the RcMap entry (which may trigger disposal after idleTimeToLive).
186
+ yield* Effect.never;
187
+ }).pipe(Effect.scoped, Runtime.runCallback(this.#runtime));
188
+ return () => release();
189
+ };
190
+ /**
191
+ * Loads a store (without suspending) to warm up the cache.
192
+ *
193
+ * @typeParam TSchema - The schema of the store to preload
194
+ * @typeParam TContext - The context type for the store
195
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
196
+ * @returns A promise that resolves when the loading is complete (success or failure)
197
+ *
198
+ * @remarks
199
+ * - We don't return the store or throw as this is a fire-and-forget operation.
200
+ * - If the entry remains unused after preload resolves/rejects, it is scheduled for disposal.
201
+ * - Does not affect the retention of the store in cache.
202
+ */
203
+ preload = async (options) => {
204
+ try {
205
+ await this.getOrLoadPromise(options);
206
+ }
207
+ catch {
208
+ // Do nothing; preload is best-effort
209
+ }
210
+ };
211
+ /**
212
+ * Disposes the registry and all its managed stores, immediately releasing resources
213
+ * (database connections, WebSocket connections, web workers, etc.).
214
+ *
215
+ * Most applications should use a single `StoreRegistry` and don't need to call
216
+ * this method. It's only necessary when creating multiple short-lived registries to
217
+ * immediately release resources and avoid conflicts with subsequent registries.
218
+ *
219
+ * @returns A promise that resolves when disposal is complete
220
+ *
221
+ * @remarks
222
+ * - No-op if a custom `runtime` was provided to the constructor (caller owns cleanup)
223
+ * - Idempotent: safe to call multiple times
224
+ * - After disposal, the registry should not be used
225
+ */
226
+ dispose = async () => {
227
+ await this.#disposeOwnedRuntime?.();
228
+ };
229
+ }
230
+ /**
231
+ * Helper for defining reusable store options with full type inference. Returns
232
+ * options that can be passed to `useStore()` or `storeRegistry.preload()`.
233
+ *
234
+ * @remarks
235
+ * At runtime this is an identity function that returns the input unchanged.
236
+ * Its value lies in enabling TypeScript's excess property checking to catch
237
+ * typos and configuration errors, while allowing options to be shared across
238
+ * `useStore()`, `storeRegistry.preload()`, `storeRegistry.getOrLoad()`, etc.
239
+ *
240
+ * @typeParam TSchema - The LiveStore schema type
241
+ * @typeParam TContext - User-defined context attached to the store
242
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
243
+ * @param options - The store configuration options
244
+ * @returns The same options object, unchanged
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * export const issueStoreOptions = (issueId: string) =>
249
+ * storeOptions({
250
+ * storeId: `issue-${issueId}`,
251
+ * schema,
252
+ * adapter,
253
+ * unusedCacheTime: 30_000,
254
+ * })
255
+ *
256
+ * // In a component
257
+ * const issueStore = useStore(issueStoreOptions(issueId))
258
+ *
259
+ * // In a route loader or event handler
260
+ * storeRegistry.preload({
261
+ * ...issueStoreOptions(issueId),
262
+ * unusedCacheTime: 10_000,
263
+ * });
264
+ * ```
265
+ */
266
+ export const storeOptions = (options) => options;
267
+ //# sourceMappingURL=StoreRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoreRegistry.js","sourceRoot":"","sources":["../../src/store/StoreRegistry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=StoreRegistry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoreRegistry.test.d.ts","sourceRoot":"","sources":["../../src/store/StoreRegistry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,381 @@
1
+ import { describe, expect, it } from '@effect/vitest';
2
+ import { makeInMemoryAdapter } from '@livestore/adapter-web';
3
+ import { OtelLiveDummy, UnknownError } from '@livestore/common';
4
+ import { Effect, Fiber, TestClock } from '@livestore/utils/effect';
5
+ import { schema } from "../utils/tests/fixture.js";
6
+ import { StoreInternalsSymbol } from "./store-types.js";
7
+ import { StoreRegistry, storeOptions } from "./StoreRegistry.js";
8
+ describe('StoreRegistry', () => {
9
+ it('returns a promise when the store is loading', async () => {
10
+ const storeRegistry = new StoreRegistry();
11
+ const result = storeRegistry.getOrLoadPromise(testStoreOptions());
12
+ expect(result).toBeInstanceOf(Promise);
13
+ // Clean up
14
+ const store = await result;
15
+ await store.shutdownPromise();
16
+ });
17
+ it('returns cached store synchronously after first load resolves', async () => {
18
+ const storeRegistry = new StoreRegistry();
19
+ const initial = storeRegistry.getOrLoadPromise(testStoreOptions());
20
+ expect(initial).toBeInstanceOf(Promise);
21
+ const store = await initial;
22
+ const cached = storeRegistry.getOrLoadPromise(testStoreOptions());
23
+ expect(cached).toBe(store);
24
+ expect(cached).not.toBeInstanceOf(Promise);
25
+ // Clean up
26
+ await store.shutdownPromise();
27
+ });
28
+ it('reuses the same promise for concurrent getOrLoadPromise calls while loading', async () => {
29
+ const storeRegistry = new StoreRegistry();
30
+ const options = testStoreOptions();
31
+ const first = storeRegistry.getOrLoadPromise(options);
32
+ const second = storeRegistry.getOrLoadPromise(options);
33
+ // Both should be the same promise
34
+ expect(first).toBe(second);
35
+ expect(first).toBeInstanceOf(Promise);
36
+ const store = await first;
37
+ // Both promises should resolve to the same store
38
+ expect(await second).toBe(store);
39
+ // Clean up
40
+ await store.shutdownPromise();
41
+ });
42
+ it('throws synchronously and rethrows on subsequent calls for sync failures', () => {
43
+ const storeRegistry = new StoreRegistry();
44
+ const badOptions = testStoreOptions({
45
+ // @ts-expect-error - intentionally passing invalid adapter to trigger error
46
+ adapter: null,
47
+ });
48
+ // First call throws synchronously
49
+ expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow();
50
+ // Subsequent call should also throw synchronously (cached error)
51
+ expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow();
52
+ });
53
+ it('caches and rethrows rejection on subsequent calls for async failures', async () => {
54
+ const storeRegistry = new StoreRegistry();
55
+ // Create an adapter that fails asynchronously (after yielding to the event loop)
56
+ const failingAdapter = () => Effect.gen(function* () {
57
+ yield* Effect.sleep(0); // Force async execution
58
+ return yield* UnknownError.make({ cause: new Error('Async failure') });
59
+ });
60
+ const badOptions = testStoreOptions({
61
+ adapter: failingAdapter,
62
+ });
63
+ // First call returns a promise that rejects
64
+ await expect(storeRegistry.getOrLoadPromise(badOptions)).rejects.toThrow();
65
+ // Subsequent call should throw the cached error synchronously (RcMap caches failures)
66
+ expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow();
67
+ });
68
+ it('throws the same error instance on multiple calls after failure', async () => {
69
+ const storeRegistry = new StoreRegistry();
70
+ // Create an adapter that fails asynchronously
71
+ const failingAdapter = () => Effect.gen(function* () {
72
+ yield* Effect.sleep(0); // Force async execution
73
+ return yield* UnknownError.make({ cause: new Error('Async failure') });
74
+ });
75
+ const badOptions = testStoreOptions({
76
+ adapter: failingAdapter,
77
+ });
78
+ // Wait for the first failure
79
+ await expect(storeRegistry.getOrLoadPromise(badOptions)).rejects.toThrow();
80
+ // Capture the errors from subsequent calls
81
+ let error1;
82
+ let error2;
83
+ try {
84
+ storeRegistry.getOrLoadPromise(badOptions);
85
+ }
86
+ catch (err) {
87
+ error1 = err;
88
+ }
89
+ try {
90
+ storeRegistry.getOrLoadPromise(badOptions);
91
+ }
92
+ catch (err) {
93
+ error2 = err;
94
+ }
95
+ // Both should be the exact same error instance (cached)
96
+ expect(error1).toBeDefined();
97
+ expect(error1).toBe(error2);
98
+ });
99
+ it('preload does not throw', async () => {
100
+ const storeRegistry = new StoreRegistry();
101
+ // Create invalid options that would cause an error
102
+ const badOptions = testStoreOptions({
103
+ // @ts-expect-error - intentionally passing invalid adapter to trigger error
104
+ adapter: null,
105
+ });
106
+ // preload should not throw
107
+ await expect(storeRegistry.preload(badOptions)).resolves.toBeUndefined();
108
+ // But subsequent getOrLoadStore should throw the cached error
109
+ expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow();
110
+ });
111
+ it('warms the cache so subsequent getOrLoadStore is synchronous after preload', async () => {
112
+ const storeRegistry = new StoreRegistry();
113
+ const options = testStoreOptions();
114
+ // Preload the store
115
+ await storeRegistry.preload(options);
116
+ // Subsequent getOrLoadStore should return synchronously (not a Promise)
117
+ const store = storeRegistry.getOrLoadPromise(options);
118
+ expect(store).not.toBeInstanceOf(Promise);
119
+ // TypeScript doesn't narrow the type, so we need to assert
120
+ if (store instanceof Promise) {
121
+ throw new Error('Expected store, got Promise');
122
+ }
123
+ // Clean up
124
+ await store.shutdownPromise();
125
+ });
126
+ it.layer(OtelLiveDummy)('time-dependent (using TestClock)', (it) => {
127
+ it.scoped('disposes store after unusedCacheTime expires', () => Effect.gen(function* () {
128
+ const unusedCacheTime = 25;
129
+ const runtime = yield* Effect.runtime();
130
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
131
+ const options = testStoreOptions();
132
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
133
+ // Store should still be in cache
134
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
135
+ expect(cached).toBe(store);
136
+ // Let the idle timer fiber register its sleep with TestClock
137
+ yield* Effect.yieldNow();
138
+ // Advance time past unusedCacheTime → idle timer fires → entry evicted
139
+ yield* TestClock.adjust(unusedCacheTime);
140
+ // After eviction, a new load should produce a different store
141
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
142
+ expect(nextStore).not.toBe(store);
143
+ expect(nextStore[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined();
144
+ }));
145
+ it.scoped('does not dispose when unusedCacheTime is Infinity', () => Effect.gen(function* () {
146
+ const runtime = yield* Effect.runtime();
147
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: Number.POSITIVE_INFINITY } });
148
+ const options = testStoreOptions();
149
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
150
+ // Advance a large amount of time — no idle timer was started for Infinity
151
+ yield* TestClock.adjust(100_000);
152
+ // Store should still be cached
153
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
154
+ expect(cached).toBe(store);
155
+ }));
156
+ it.scoped('schedules disposal if store becomes unused during loading', () => Effect.gen(function* () {
157
+ const unusedCacheTime = 50;
158
+ const runtime = yield* Effect.runtime();
159
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
160
+ const options = testStoreOptions();
161
+ // Load without retaining — disposal is scheduled when scope closes
162
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
163
+ yield* Effect.yieldNow();
164
+ yield* TestClock.adjust(unusedCacheTime);
165
+ // Store should be disposed
166
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
167
+ expect(nextStore).not.toBe(store);
168
+ }));
169
+ it.scoped('allows call-site options to override default options', () => Effect.gen(function* () {
170
+ const runtime = yield* Effect.runtime();
171
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 10_000 } }); // Long default
172
+ const unusedCacheTimeOverride = 25;
173
+ const options = testStoreOptions({ unusedCacheTime: unusedCacheTimeOverride });
174
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
175
+ yield* Effect.yieldNow();
176
+ yield* TestClock.adjust(unusedCacheTimeOverride);
177
+ // Should be disposed according to the override time, not default
178
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
179
+ expect(nextStore).not.toBe(store);
180
+ }));
181
+ it.scoped('disposes different stores according to their own unusedCacheTime', () => Effect.gen(function* () {
182
+ const runtime = yield* Effect.runtime();
183
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 1000 } });
184
+ const shortOptions = testStoreOptions({ storeId: 'short-lived', unusedCacheTime: 25 });
185
+ const longOptions = testStoreOptions({ storeId: 'long-lived', unusedCacheTime: 10_000 });
186
+ const shortStore = yield* registry.getOrLoad(shortOptions).pipe(Effect.scoped);
187
+ const longStore = yield* registry.getOrLoad(longOptions).pipe(Effect.scoped);
188
+ yield* Effect.yieldNow();
189
+ // Advance past short store's unusedCacheTime only
190
+ yield* TestClock.adjust(25);
191
+ // Short store should be disposed, long store should still be cached
192
+ const nextShortStore = yield* registry.getOrLoad(shortOptions).pipe(Effect.scoped);
193
+ expect(nextShortStore).not.toBe(shortStore);
194
+ const cachedLongStore = yield* registry.getOrLoad(longOptions).pipe(Effect.scoped);
195
+ expect(cachedLongStore).toBe(longStore);
196
+ }));
197
+ // This test is skipped because we don't yet support dynamic `unusedCacheTime` updates for cached stores.
198
+ // See https://github.com/livestorejs/livestore/issues/918
199
+ it.scoped.skip('keeps the longest unusedCacheTime seen for a store when options vary across calls', () => Effect.gen(function* () {
200
+ const runtime = yield* Effect.runtime();
201
+ const registry = new StoreRegistry({ runtime });
202
+ const options = testStoreOptions({ unusedCacheTime: 10 });
203
+ const release = registry.retain(options);
204
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
205
+ // Call with longer unusedCacheTime
206
+ yield* registry.getOrLoad(testStoreOptions({ unusedCacheTime: 100 })).pipe(Effect.scoped);
207
+ release();
208
+ yield* Effect.yieldNow();
209
+ // After 99ms, store should still be alive (100ms unusedCacheTime used)
210
+ yield* TestClock.adjust(99);
211
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
212
+ expect(cached).toBe(store);
213
+ // After 1 more ms, store should be disposed
214
+ yield* TestClock.adjust(1);
215
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
216
+ expect(nextStore).not.toBe(store);
217
+ }));
218
+ it.scoped('handles rapid retain/release cycles without errors', () => Effect.gen(function* () {
219
+ const unusedCacheTime = 50;
220
+ const runtime = yield* Effect.runtime();
221
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
222
+ const options = testStoreOptions();
223
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
224
+ // Rapidly retain and release multiple times
225
+ for (let i = 0; i < 10; i++) {
226
+ const release = registry.retain(options);
227
+ release();
228
+ }
229
+ yield* Effect.yieldNow();
230
+ yield* TestClock.adjust(unusedCacheTime);
231
+ // Store should be disposed after the last release
232
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
233
+ expect(nextStore).not.toBe(store);
234
+ }));
235
+ it.scoped('cancels disposal when new retain', () => Effect.gen(function* () {
236
+ const unusedCacheTime = 50;
237
+ const runtime = yield* Effect.runtime();
238
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
239
+ const options = testStoreOptions();
240
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
241
+ yield* Effect.yieldNow();
242
+ // Advance almost to disposal threshold
243
+ yield* TestClock.adjust(unusedCacheTime - 5);
244
+ // Add a new retain before disposal triggers
245
+ const release = registry.retain(options);
246
+ // Complete the original unusedCacheTime
247
+ yield* TestClock.adjust(5);
248
+ // Store should not have been disposed because retain keeps it alive
249
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
250
+ expect(cached).toBe(store);
251
+ // Release retain — new idle timer starts
252
+ release();
253
+ yield* Effect.yieldNow();
254
+ yield* TestClock.adjust(unusedCacheTime);
255
+ // Now it should be disposed
256
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
257
+ expect(nextStore).not.toBe(store);
258
+ }));
259
+ it.scoped('aborts loading when disposal fires while store is still loading', () => Effect.gen(function* () {
260
+ const unusedCacheTime = 10;
261
+ const loadDelay = 1000;
262
+ const runtime = yield* Effect.runtime();
263
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
264
+ // Adapter that takes time to load (controlled by TestClock)
265
+ const baseAdapter = makeInMemoryAdapter();
266
+ const options = testStoreOptions({
267
+ adapter: ((args) => Effect.gen(function* () {
268
+ yield* Effect.sleep(loadDelay);
269
+ return yield* baseAdapter(args);
270
+ })),
271
+ });
272
+ // Retain triggers loading (won't complete until clock advances past loadDelay)
273
+ const release = registry.retain(options);
274
+ yield* Effect.yieldNow();
275
+ // Release immediately — schedules disposal after unusedCacheTime
276
+ release();
277
+ yield* Effect.yieldNow();
278
+ // Advance past unusedCacheTime but NOT past loadDelay → disposal fires, interrupts loading
279
+ yield* TestClock.adjust(unusedCacheTime);
280
+ // Start a fresh load — since the first was aborted, this should be a new entry
281
+ const freshLoadFiber = yield* Effect.fork(registry.getOrLoad(options).pipe(Effect.scoped));
282
+ yield* Effect.yieldNow();
283
+ // Advance enough for the fresh load to complete
284
+ yield* TestClock.adjust(loadDelay);
285
+ const store = yield* Fiber.join(freshLoadFiber);
286
+ expect(store).toBeDefined();
287
+ }));
288
+ it.scoped('retain keeps store alive past unusedCacheTime', () => Effect.gen(function* () {
289
+ const unusedCacheTime = 50;
290
+ const runtime = yield* Effect.runtime();
291
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
292
+ const options = testStoreOptions();
293
+ // Load the store
294
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
295
+ // Retain the store before disposal could fire
296
+ const release = registry.retain(options);
297
+ yield* Effect.yieldNow();
298
+ // Advance past unusedCacheTime — idle timer fires but refCount > 0, so no eviction
299
+ yield* TestClock.adjust(unusedCacheTime + 50);
300
+ // Store should still be cached because retain keeps it alive
301
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
302
+ expect(cached).toBe(store);
303
+ release();
304
+ }));
305
+ it.scoped('manages multiple stores with different IDs independently', () => Effect.gen(function* () {
306
+ const unusedCacheTime = 50;
307
+ const runtime = yield* Effect.runtime();
308
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
309
+ const options1 = testStoreOptions({ storeId: 'store-1' });
310
+ const options2 = testStoreOptions({ storeId: 'store-2' });
311
+ const store1 = yield* registry.getOrLoad(options1).pipe(Effect.scoped);
312
+ const store2 = yield* registry.getOrLoad(options2).pipe(Effect.scoped);
313
+ // Should be different store instances
314
+ expect(store1).not.toBe(store2);
315
+ // Both should be cached independently
316
+ const cached1 = yield* registry.getOrLoad(options1).pipe(Effect.scoped);
317
+ const cached2 = yield* registry.getOrLoad(options2).pipe(Effect.scoped);
318
+ expect(cached1).toBe(store1);
319
+ expect(cached2).toBe(store2);
320
+ yield* Effect.yieldNow();
321
+ yield* TestClock.adjust(unusedCacheTime);
322
+ // Both stores should be disposed
323
+ const newStore1 = yield* registry.getOrLoad(options1).pipe(Effect.scoped);
324
+ const newStore2 = yield* registry.getOrLoad(options2).pipe(Effect.scoped);
325
+ expect(newStore1).not.toBe(store1);
326
+ expect(newStore2).not.toBe(store2);
327
+ }));
328
+ it.scoped('applies default options from constructor', () => Effect.gen(function* () {
329
+ const runtime = yield* Effect.runtime();
330
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 100 } });
331
+ const options = testStoreOptions();
332
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
333
+ // Verify the store loads successfully
334
+ expect(store).toBeDefined();
335
+ expect(store[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined();
336
+ yield* Effect.yieldNow();
337
+ // After 50ms, store should still be cached (default is 100ms)
338
+ yield* TestClock.adjust(50);
339
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
340
+ expect(cached).toBe(store);
341
+ }));
342
+ it.scoped('does not serve a disposed store from cache', () => Effect.gen(function* () {
343
+ const unusedCacheTime = 25;
344
+ const runtime = yield* Effect.runtime();
345
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
346
+ const options = testStoreOptions();
347
+ const originalStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
348
+ // Verify store is cached
349
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
350
+ expect(cached).toBe(originalStore);
351
+ yield* Effect.yieldNow();
352
+ yield* TestClock.adjust(unusedCacheTime);
353
+ // After disposal, calling getOrLoad should produce a fresh store
354
+ const freshStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
355
+ expect(freshStore).not.toBe(originalStore);
356
+ }));
357
+ it.scoped('schedules disposal after preload if no retainers are added', () => Effect.gen(function* () {
358
+ const unusedCacheTime = 50;
359
+ const runtime = yield* Effect.runtime();
360
+ const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } });
361
+ const options = testStoreOptions();
362
+ // Preload without retaining (load + immediate release)
363
+ const store = yield* registry.getOrLoad(options).pipe(Effect.scoped);
364
+ // Verify it's cached
365
+ const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped);
366
+ expect(cached).toBe(store);
367
+ yield* Effect.yieldNow();
368
+ yield* TestClock.adjust(unusedCacheTime);
369
+ // Store should be disposed since no retainers were added
370
+ const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped);
371
+ expect(nextStore).not.toBe(store);
372
+ }));
373
+ });
374
+ });
375
+ const testStoreOptions = (overrides = {}) => storeOptions({
376
+ storeId: 'test-store',
377
+ schema,
378
+ adapter: makeInMemoryAdapter(),
379
+ ...overrides,
380
+ });
381
+ //# sourceMappingURL=StoreRegistry.test.js.map