@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,418 @@
1
+ import { LogConfig, OtelLiveDummy, provideOtel, UnknownError } from '@livestore/common'
2
+ import type { LiveStoreSchema } from '@livestore/common/schema'
3
+ import { omitUndefineds } from '@livestore/utils'
4
+ import {
5
+ Cause,
6
+ Effect,
7
+ Equal,
8
+ Exit,
9
+ Fiber,
10
+ Hash,
11
+ Layer,
12
+ ManagedRuntime,
13
+ type OtelTracer,
14
+ RcMap,
15
+ Runtime,
16
+ type Schema,
17
+ type Scope,
18
+ } from '@livestore/utils/effect'
19
+
20
+ import { createStore, type CreateStoreOptions } from './create-store.ts'
21
+ import type { Store } from './store.ts'
22
+ import type { OtelOptions } from './store-types.ts'
23
+
24
+ /**
25
+ * Default time to keep unused stores in cache.
26
+ *
27
+ * - Browser: 60 seconds (60,000 ms)
28
+ * - SSR: Infinity (disables disposal to avoid disposing stores before server render completes)
29
+ *
30
+ * @internal Exported primarily for testing purposes.
31
+ */
32
+ export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000
33
+
34
+ /**
35
+ * Configuration options for stores managed by a {@link StoreRegistry}.
36
+ *
37
+ * Extends {@link CreateStoreOptions} with registry-specific settings for caching and observability.
38
+ * Use with {@link storeOptions} helper to get full type inference when defining reusable store configurations.
39
+ *
40
+ * @typeParam TSchema - The LiveStore schema type
41
+ * @typeParam TContext - User-defined context attached to the store
42
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
43
+ *
44
+ * @see {@link storeOptions} for defining reusable store configurations
45
+ * @see {@link StoreRegistry} for managing store lifecycles
46
+ */
47
+ export interface RegistryStoreOptions<
48
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
49
+ TContext = {},
50
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
51
+ > extends CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> {
52
+ /**
53
+ * OpenTelemetry configuration for tracing store operations.
54
+ *
55
+ * When provided, store operations (boot, queries, commits) will be traced
56
+ * under the given root span context using the specified tracer.
57
+ */
58
+ otelOptions?: Partial<OtelOptions>
59
+ /**
60
+ * The time in milliseconds that this store should remain
61
+ * in memory after becoming unused. When this store becomes
62
+ * unused (no active retentions), it will be disposed after this duration.
63
+ *
64
+ * Stores transition to the unused state as soon as they have no
65
+ * active retentions, so when all components which use that store
66
+ * have unmounted.
67
+ *
68
+ * @remarks
69
+ * - Per-store values override the registry-level default (set via `StoreRegistry` constructor's
70
+ * `defaultOptions.unusedCacheTime`)
71
+ * - The value is fixed when the store is first loaded into the registry. If the same `storeId` is
72
+ * requested again with a different `unusedCacheTime`, the original value is kept.
73
+ * - If set to `Infinity`, will disable automatic disposal
74
+ * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
75
+ *
76
+ * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
77
+ * disposing stores before server render completes.
78
+ */
79
+ unusedCacheTime?: number
80
+ }
81
+
82
+ type StoreRegistryConfig = {
83
+ /**
84
+ * Default options that are applied to all stores when they are loaded.
85
+ *
86
+ * @remarks
87
+ * These are options that typically don't depend on the specific store being loaded:
88
+ * - Framework integration (`batchUpdates`)
89
+ * - Environment settings (`disableDevtools`, `debug`, `otelOptions`)
90
+ * - Behavior defaults (`confirmUnsavedChanges`, `unusedCacheTime`)
91
+ *
92
+ * Store-specific fields like `schema`, `adapter`, `storeId`, and `boot` are intentionally
93
+ * excluded since they vary per store definition.
94
+ */
95
+ defaultOptions?: Partial<
96
+ Pick<
97
+ RegistryStoreOptions,
98
+ 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'debug' | 'otelOptions' | 'unusedCacheTime'
99
+ >
100
+ >
101
+ /**
102
+ * Custom Effect runtime for all registry operations (loading, caching, etc.).
103
+ * When the runtime's scope closes, all managed stores are automatically shut down.
104
+ */
105
+ runtime?: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
106
+ }
107
+
108
+ /**
109
+ * RcMap cache key that uses storeId for equality/hashing but carries full options.
110
+ * This allows RcMap to deduplicate by storeId while the lookup function has access to all options.
111
+ *
112
+ * @remarks
113
+ * Only `storeId` is used for equality and hashing. This means if `getOrLoadPromise` is called
114
+ * with different options (e.g., different `adapter`) but the same `storeId`, the cached store
115
+ * from the first call will be returned. This is intentional - a store's identity is determined
116
+ * solely by its `storeId`, and callers should not expect to get different stores by varying
117
+ * other options while keeping the same `storeId`.
118
+ */
119
+ class StoreCacheKey implements Equal.Equal {
120
+ readonly options: RegistryStoreOptions<any, any, any>
121
+
122
+ constructor(options: RegistryStoreOptions<any, any, any>) {
123
+ this.options = options
124
+ }
125
+
126
+ /**
127
+ * Equality is based solely on `storeId`. Other options in `RegistryStoreOptions` are ignored
128
+ * for cache key comparison. The first options used for a given `storeId` determine the
129
+ * store's configuration.
130
+ */
131
+ [Equal.symbol](that: Equal.Equal): boolean {
132
+ return that instanceof StoreCacheKey && this.options.storeId === that.options.storeId
133
+ }
134
+
135
+ [Hash.symbol](): number {
136
+ return Hash.string(this.options.storeId)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Store Registry coordinating store loading, caching, and retention
142
+ *
143
+ * @public
144
+ */
145
+ export class StoreRegistry {
146
+ /**
147
+ * Reference-counted cache mapping storeId to Store instances.
148
+ * Stores are created on first access and disposed after `unusedCacheTime` when all references are released.
149
+ */
150
+ readonly #rcMap: RcMap.RcMap<StoreCacheKey, Store<any, any>, UnknownError>
151
+
152
+ /**
153
+ * Effect runtime providing Scope and OtelTracer for all registry operations.
154
+ * When the runtime's scope closes, all managed stores are automatically shut down.
155
+ */
156
+ readonly #runtime: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
157
+
158
+ /**
159
+ * Disposal callback for the runtime created by the registry.
160
+ * Undefined when caller provided their own runtime (caller owns cleanup in that case).
161
+ */
162
+ readonly #disposeOwnedRuntime: (() => Promise<void>) | undefined
163
+
164
+ /**
165
+ * In-flight loading promises keyed by storeId.
166
+ * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
167
+ */
168
+ readonly #loadingPromises: Map<string, Promise<Store<any, any>>> = new Map()
169
+
170
+ /**
171
+ * Creates a new StoreRegistry instance.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const registry = new StoreRegistry({
176
+ * defaultOptions: {
177
+ * batchUpdates,
178
+ * unusedCacheTime: 30_000,
179
+ * }
180
+ * })
181
+ * ```
182
+ */
183
+ constructor(config: StoreRegistryConfig = {}) {
184
+ if (config.runtime !== undefined) {
185
+ this.#runtime = config.runtime
186
+ } else {
187
+ const ownedRuntime = ManagedRuntime.make(Layer.mergeAll(Layer.scope, OtelLiveDummy))
188
+ this.#runtime = ownedRuntime.runtimeEffect.pipe(Effect.runSync)
189
+ this.#disposeOwnedRuntime = () => ownedRuntime.dispose()
190
+ }
191
+
192
+ this.#rcMap = RcMap.make({
193
+ lookup: ({ options }: StoreCacheKey) => {
194
+ // Merge registry defaults with call-site options (call-site takes precedence)
195
+ const mergedOptions = { ...config.defaultOptions, ...options }
196
+ return createStore(mergedOptions).pipe(
197
+ Effect.catchAllDefect((cause) => UnknownError.make({ cause })),
198
+ Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`),
199
+ LogConfig.withLoggerConfig(mergedOptions, { threadName: 'window' }),
200
+ provideOtel(
201
+ omitUndefineds({
202
+ parentSpanContext: mergedOptions.otelOptions?.rootSpanContext,
203
+ otelTracer: mergedOptions.otelOptions?.tracer,
204
+ }),
205
+ ),
206
+ )
207
+ },
208
+ idleTimeToLive: ({ options }: StoreCacheKey) => options.unusedCacheTime ?? config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
209
+ }).pipe(Runtime.runSync(this.#runtime))
210
+ }
211
+
212
+ /**
213
+ * Gets a cached store or loads a new one, with the store lifetime scoped to the caller.
214
+ *
215
+ * @typeParam TSchema - The schema type for the store
216
+ * @typeParam TContext - The context type for the store
217
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
218
+ * @returns An Effect that yields the store, scoped to the provided Scope
219
+ *
220
+ * @remarks
221
+ * - Stores are kept in cache and reused while any scope holds them
222
+ * - When the scope closes, the reference is released; the store is disposed after `unusedCacheTime`
223
+ * if no other scopes retain it
224
+ * - Concurrent calls with the same storeId share the same store instance
225
+ */
226
+ getOrLoad = <
227
+ TSchema extends LiveStoreSchema,
228
+ TContext = {},
229
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
230
+ >(
231
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
232
+ ): Effect.Effect<Store<TSchema, TContext>, UnknownError, Scope.Scope> =>
233
+ Effect.gen(this, function* () {
234
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
235
+ const key = new StoreCacheKey(options)
236
+ const store = yield* RcMap.get(this.#rcMap, key)
237
+
238
+ return store as Store<TSchema, TContext>
239
+ }).pipe(Effect.withSpan(`StoreRegistry.getOrLoad:${options.storeId}`))
240
+
241
+ /**
242
+ * Get or load a store, returning it directly if already loaded or a promise if loading.
243
+ *
244
+ * @typeParam TSchema - The schema type for the store
245
+ * @typeParam TContext - The context type for the store
246
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
247
+ * @returns The loaded store if available, or a Promise that resolves to the loaded store
248
+ * @throws unknown - store loading error
249
+ *
250
+ * @remarks
251
+ * - Returns the store instance directly (synchronous) when already loaded
252
+ * - Returns a stable Promise reference when loading is in progress or needs to be initiated
253
+ * - Throws with the same error instance on subsequent calls after failure
254
+ * - Applies default options from registry config, with call-site options taking precedence
255
+ * - Concurrent calls with the same storeId share the same store instance
256
+ */
257
+ getOrLoadPromise = <
258
+ TSchema extends LiveStoreSchema,
259
+ TContext = {},
260
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
261
+ >(
262
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
263
+ ): Store<TSchema, TContext> | Promise<Store<TSchema, TContext>> => {
264
+ const exit = this.getOrLoad(options).pipe(Effect.scoped, Runtime.runSyncExit(this.#runtime))
265
+
266
+ if (Exit.isSuccess(exit) === true) return exit.value
267
+
268
+ // Check if the failure is due to async work
269
+ const defect = Cause.dieOption(exit.cause)
270
+ if (defect._tag !== 'Some') {
271
+ // Handle synchronous failure
272
+ throw Cause.squash(exit.cause)
273
+ }
274
+
275
+ if (Runtime.isAsyncFiberException(defect.value) === false) {
276
+ // Handle synchronous failure
277
+ throw Cause.squash(exit.cause)
278
+ }
279
+
280
+ const { storeId } = options
281
+
282
+ // Return cached promise if one exists (ensures concurrent calls get the same Promise reference)
283
+ const cached = this.#loadingPromises.get(storeId)
284
+ if (cached !== undefined) return cached as Promise<Store<TSchema, TContext>>
285
+
286
+ // Create and cache the promise
287
+ const fiber = defect.value.fiber as Fiber.RuntimeFiber<Store<TSchema, TContext>>
288
+ const promise = Fiber.join(fiber)
289
+ .pipe(Runtime.runPromise(this.#runtime))
290
+ .finally(() => this.#loadingPromises.delete(storeId))
291
+
292
+ this.#loadingPromises.set(storeId, promise)
293
+ return promise
294
+ }
295
+
296
+ /**
297
+ * Retains the store in cache.
298
+ *
299
+ * @typeParam TSchema - The schema type for the store
300
+ * @typeParam TContext - The context type for the store
301
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
302
+ * @returns A release function that, when called, removes this retention hold
303
+ *
304
+ * @remarks
305
+ * - Multiple retains on the same store are independent; each must be released separately
306
+ * - If the store isn't cached yet, it will be loaded and then retained
307
+ * - The store will remain in cache until all retains are released and after `unusedCacheTime` expires
308
+ */
309
+ retain = <
310
+ TSchema extends LiveStoreSchema,
311
+ TContext = {},
312
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
313
+ >(
314
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
315
+ ): (() => void) => {
316
+ const release = Effect.gen(this, function* () {
317
+ // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
318
+ const key = new StoreCacheKey(options)
319
+ yield* RcMap.get(this.#rcMap, key)
320
+ // Effect.never suspends indefinitely, keeping the RcMap reference alive.
321
+ // When `release()` is called, the fiber is interrupted, closing the scope
322
+ // and releasing the RcMap entry (which may trigger disposal after idleTimeToLive).
323
+ yield* Effect.never
324
+ }).pipe(Effect.scoped, Runtime.runCallback(this.#runtime))
325
+
326
+ return () => release()
327
+ }
328
+
329
+ /**
330
+ * Loads a store (without suspending) to warm up the cache.
331
+ *
332
+ * @typeParam TSchema - The schema of the store to preload
333
+ * @typeParam TContext - The context type for the store
334
+ * @typeParam TSyncPayloadSchema - The sync payload schema type
335
+ * @returns A promise that resolves when the loading is complete (success or failure)
336
+ *
337
+ * @remarks
338
+ * - We don't return the store or throw as this is a fire-and-forget operation.
339
+ * - If the entry remains unused after preload resolves/rejects, it is scheduled for disposal.
340
+ * - Does not affect the retention of the store in cache.
341
+ */
342
+ preload = async <
343
+ TSchema extends LiveStoreSchema,
344
+ TContext = {},
345
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
346
+ >(
347
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
348
+ ): Promise<void> => {
349
+ try {
350
+ await this.getOrLoadPromise(options)
351
+ } catch {
352
+ // Do nothing; preload is best-effort
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Disposes the registry and all its managed stores, immediately releasing resources
358
+ * (database connections, WebSocket connections, web workers, etc.).
359
+ *
360
+ * Most applications should use a single `StoreRegistry` and don't need to call
361
+ * this method. It's only necessary when creating multiple short-lived registries to
362
+ * immediately release resources and avoid conflicts with subsequent registries.
363
+ *
364
+ * @returns A promise that resolves when disposal is complete
365
+ *
366
+ * @remarks
367
+ * - No-op if a custom `runtime` was provided to the constructor (caller owns cleanup)
368
+ * - Idempotent: safe to call multiple times
369
+ * - After disposal, the registry should not be used
370
+ */
371
+ dispose = async (): Promise<void> => {
372
+ await this.#disposeOwnedRuntime?.()
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Helper for defining reusable store options with full type inference. Returns
378
+ * options that can be passed to `useStore()` or `storeRegistry.preload()`.
379
+ *
380
+ * @remarks
381
+ * At runtime this is an identity function that returns the input unchanged.
382
+ * Its value lies in enabling TypeScript's excess property checking to catch
383
+ * typos and configuration errors, while allowing options to be shared across
384
+ * `useStore()`, `storeRegistry.preload()`, `storeRegistry.getOrLoad()`, etc.
385
+ *
386
+ * @typeParam TSchema - The LiveStore schema type
387
+ * @typeParam TContext - User-defined context attached to the store
388
+ * @typeParam TSyncPayloadSchema - Schema for the sync payload sent to the backend
389
+ * @param options - The store configuration options
390
+ * @returns The same options object, unchanged
391
+ *
392
+ * @example
393
+ * ```ts
394
+ * export const issueStoreOptions = (issueId: string) =>
395
+ * storeOptions({
396
+ * storeId: `issue-${issueId}`,
397
+ * schema,
398
+ * adapter,
399
+ * unusedCacheTime: 30_000,
400
+ * })
401
+ *
402
+ * // In a component
403
+ * const issueStore = useStore(issueStoreOptions(issueId))
404
+ *
405
+ * // In a route loader or event handler
406
+ * storeRegistry.preload({
407
+ * ...issueStoreOptions(issueId),
408
+ * unusedCacheTime: 10_000,
409
+ * });
410
+ * ```
411
+ */
412
+ export const storeOptions = <
413
+ TSchema extends LiveStoreSchema,
414
+ TContext = {},
415
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
416
+ >(
417
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
418
+ ): RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema> => options
@@ -1,3 +1,5 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
1
3
  import {
2
4
  type Adapter,
3
5
  type BootStatus,
@@ -5,13 +7,12 @@ import {
5
7
  type ClientSessionDevtoolsChannel,
6
8
  type ClientSessionSyncProcessorSimulationParams,
7
9
  type IntentionalShutdownCause,
8
- type InvalidPullError,
9
- type IsOfflineError,
10
10
  LogConfig,
11
11
  type MaterializeError,
12
+ type BackendIdMismatchError,
12
13
  type MigrationsReport,
13
14
  provideOtel,
14
- type SyncError,
15
+ type ServerAheadError,
15
16
  UnknownError,
16
17
  } from '@livestore/common'
17
18
  import type { LiveStoreSchema } from '@livestore/common/schema'
@@ -32,17 +33,37 @@ import {
32
33
  TaskTracing,
33
34
  } from '@livestore/utils/effect'
34
35
  import { nanoid } from '@livestore/utils/nanoid'
35
- import * as otel from '@opentelemetry/api'
36
36
 
37
37
  import { connectDevtoolsToStore } from './devtools.ts'
38
- import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
39
38
  import type {
40
39
  LiveStoreContextRunning as LiveStoreContextRunning_,
41
40
  OtelOptions,
42
41
  ShutdownDeferred,
43
42
  } from './store-types.ts'
44
43
  import { StoreInternalsSymbol } from './store-types.ts'
44
+ import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
45
45
 
46
+ declare global {
47
+ /** Store instances for console debugging */
48
+ var __debugLiveStore: Record<string, Store<any, any>> | undefined
49
+ }
50
+
51
+ /**
52
+ * @deprecated Use `makeStoreContext()` from `@livestore/livestore/effect` instead.
53
+ * This service doesn't preserve schema types. See the Effect integration docs for migration.
54
+ *
55
+ * @example Migration
56
+ * ```ts
57
+ * // Before (untyped)
58
+ * import { LiveStoreContextRunning } from '@livestore/livestore/effect'
59
+ * const { store } = yield* LiveStoreContextRunning
60
+ *
61
+ * // After (typed)
62
+ * import { makeStoreContext } from '@livestore/livestore/effect'
63
+ * const AppStore = makeStoreContext<typeof schema>()('app')
64
+ * const { store } = yield* AppStore.Tag
65
+ * ```
66
+ */
46
67
  export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
47
68
  LiveStoreContextRunning,
48
69
  LiveStoreContextRunning_
@@ -54,6 +75,9 @@ export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/e
54
75
  }).pipe(Layer.unwrapScoped)
55
76
  }
56
77
 
78
+ /**
79
+ * @deprecated Use `StoreContext.DeferredTag` from `makeStoreContext()` instead.
80
+ */
57
81
  export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
58
82
  DeferredStoreContext,
59
83
  Deferred.Deferred<LiveStoreContextRunning['Type'], UnknownError>
@@ -97,14 +121,14 @@ export type LiveStoreContextProps<
97
121
  */
98
122
  syncPayloadSchema?: TSyncPayloadSchema
99
123
  /**
100
- * Payload that is sent to the sync backend during connection establishment.
124
+ * Payload that is sent to the sync backend when connecting
101
125
  *
102
126
  * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
103
127
  * - At runtime this value is encoded with `syncPayloadSchema` before being handed to the adapter.
104
128
  *
105
129
  * Example:
106
130
  * const SyncPayload = Schema.Struct({ authToken: Schema.String })
107
- * <LiveStoreProvider syncPayloadSchema={SyncPayload} syncPayload={{ authToken: '...' }} />
131
+ * useStore({ ..., syncPayloadSchema: SyncPayload, syncPayload: { authToken: '...' } })
108
132
  */
109
133
  syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
110
134
  }
@@ -114,9 +138,21 @@ export interface CreateStoreOptions<
114
138
  TContext = {},
115
139
  TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
116
140
  > extends LogConfig.WithLoggerOptions {
141
+ /** The LiveStore schema defining tables, events, and materializers. */
117
142
  schema: TSchema
143
+ /** Adapter used for data storage and synchronization. */
118
144
  adapter: Adapter
145
+ /**
146
+ * Unique identifier for the Store instance, stable for its lifetime.
147
+ *
148
+ * - **Valid characters**: Only alphanumeric characters, underscores (`_`), and hyphens (`-`)
149
+ * are allowed. Must match `/^[a-zA-Z0-9_-]+$/`.
150
+ * - **Globally unique**: Use globally unique IDs (e.g., nanoid) to prevent collisions across stores.
151
+ * - **Use namespaces**: Prefix to avoid collisions and for easier identification when debugging
152
+ * (e.g., `app-root`, `workspace-abc123`, `issue-456`)
153
+ */
119
154
  storeId: string
155
+ /** User-defined context that will be attached to the created Store (e.g. for dependency injection). */
120
156
  context?: TContext
121
157
  boot?: (
122
158
  store: Store<TSchema, TContext>,
@@ -125,6 +161,19 @@ export interface CreateStoreOptions<
125
161
  parentSpan: otel.Span
126
162
  },
127
163
  ) => Effect.SyncOrPromiseOrEffect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
164
+ onBootStatus?: (status: BootStatus) => void
165
+ /**
166
+ * Needed in React so LiveStore can apply multiple events in a single render.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * // With React DOM
171
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
172
+ *
173
+ * // With React Native
174
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-native'
175
+ * ```
176
+ */
128
177
  batchUpdates?: (run: () => void) => void
129
178
  /**
130
179
  * Whether to disable devtools.
@@ -132,7 +181,6 @@ export interface CreateStoreOptions<
132
181
  * @default 'auto'
133
182
  */
134
183
  disableDevtools?: boolean | 'auto'
135
- onBootStatus?: (status: BootStatus) => void
136
184
  shutdownDeferred?: ShutdownDeferred
137
185
  /**
138
186
  * Currently only used in the web adapter:
@@ -150,7 +198,7 @@ export interface CreateStoreOptions<
150
198
  */
151
199
  syncPayloadSchema?: TSyncPayloadSchema
152
200
  /**
153
- * Payload that is sent to the sync backend during connection establishment.
201
+ * Payload that is sent to the sync backend when connecting
154
202
  *
155
203
  * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
156
204
  * - At runtime this value is encoded with `syncPayloadSchema` and carried through the adapter
@@ -159,8 +207,11 @@ export interface CreateStoreOptions<
159
207
  * @default undefined
160
208
  */
161
209
  syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
210
+ /** Options provided to the Store constructor. */
162
211
  params?: {
212
+ /** Max events pushed to the leader per write batch. */
163
213
  leaderPushBatchSize?: number
214
+ /** Chunk size used when the stream replays confirmed events. */
164
215
  eventQueryBatchSize?: number
165
216
  simulation?: {
166
217
  clientSessionSyncProcessor: typeof ClientSessionSyncProcessorSimulationParams.Type
@@ -274,7 +325,7 @@ export const createStore = <
274
325
  const shutdown = (
275
326
  exit: Exit.Exit<
276
327
  IntentionalShutdownCause,
277
- UnknownError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
328
+ UnknownError | MaterializeError | BackendIdMismatchError
278
329
  >,
279
330
  ) =>
280
331
  Effect.gen(function* () {
@@ -286,7 +337,7 @@ export const createStore = <
286
337
  ),
287
338
  )
288
339
 
289
- if (shutdownDeferred) {
340
+ if (shutdownDeferred !== undefined) {
290
341
  yield* Deferred.done(shutdownDeferred, exit)
291
342
  }
292
343
 
@@ -318,7 +369,7 @@ export const createStore = <
318
369
  syncPayloadEncoded,
319
370
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
320
371
 
321
- if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
372
+ if (LS_DEV === true && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
322
373
  yield* Effect.logDebug(
323
374
  '[@livestore/livestore:createStore] migrationsReport',
324
375
  ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
@@ -337,7 +388,7 @@ export const createStore = <
337
388
  effectContext: { lifetimeScope, runtime },
338
389
  // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
339
390
  // But for now this is a good enough approximation with little downsides
340
- __runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
391
+ __runningInDevtools: ! getDevtoolsEnabled(disableDevtools),
341
392
  confirmUnsavedChanges,
342
393
  // NOTE during boot we're not yet executing events in a batched context
343
394
  // but only set the provided `batchUpdates` function after boot
@@ -374,11 +425,21 @@ export const createStore = <
374
425
 
375
426
  yield* Deferred.succeed(storeDeferred, store as any as Store)
376
427
 
428
+ // Expose store on globalThis for console debugging
429
+ globalThis.__debugLiveStore ??= {}
430
+ globalThis.__debugLiveStore[storeId] = store
431
+
432
+ yield* Effect.addFinalizer(() =>
433
+ Effect.sync(() => {
434
+ delete globalThis.__debugLiveStore?.[storeId]
435
+ }),
436
+ )
437
+
377
438
  return store
378
439
  }).pipe(
379
440
  Effect.withSpan('createStore', { attributes: { debugInstanceId, storeId } }),
380
441
  Effect.annotateLogs({ debugInstanceId, storeId }),
381
- LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
442
+ LS_DEV === true ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
382
443
  Scope.extend(lifetimeScope),
383
444
  )
384
445
  })
@@ -387,7 +448,7 @@ const validateStoreId = (storeId: string) =>
387
448
  Effect.gen(function* () {
388
449
  const validChars = /^[a-zA-Z0-9_-]+$/
389
450
 
390
- if (!validChars.test(storeId)) {
451
+ if (validChars.test(storeId) === false) {
391
452
  return yield* UnknownError.make({
392
453
  cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
393
454
  payload: { storeId },