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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/StoreRegistryContext.d.ts +56 -0
  3. package/dist/StoreRegistryContext.d.ts.map +1 -0
  4. package/dist/StoreRegistryContext.js +61 -0
  5. package/dist/StoreRegistryContext.js.map +1 -0
  6. package/dist/__tests__/fixture.d.ts.map +1 -1
  7. package/dist/__tests__/fixture.js +1 -6
  8. package/dist/__tests__/fixture.js.map +1 -1
  9. package/dist/experimental/components/LiveList.d.ts +4 -2
  10. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  11. package/dist/experimental/components/LiveList.js +6 -5
  12. package/dist/experimental/components/LiveList.js.map +1 -1
  13. package/dist/experimental/mod.d.ts +0 -1
  14. package/dist/experimental/mod.d.ts.map +1 -1
  15. package/dist/experimental/mod.js +0 -1
  16. package/dist/experimental/mod.js.map +1 -1
  17. package/dist/mod.d.ts +4 -3
  18. package/dist/mod.d.ts.map +1 -1
  19. package/dist/mod.js +3 -2
  20. package/dist/mod.js.map +1 -1
  21. package/dist/useClientDocument.d.ts.map +1 -1
  22. package/dist/useClientDocument.js +1 -4
  23. package/dist/useClientDocument.js.map +1 -1
  24. package/dist/useQuery.d.ts +1 -1
  25. package/dist/useQuery.d.ts.map +1 -1
  26. package/dist/useQuery.js +2 -5
  27. package/dist/useQuery.js.map +1 -1
  28. package/dist/useStore.d.ts +50 -46
  29. package/dist/useStore.d.ts.map +1 -1
  30. package/dist/useStore.js +66 -59
  31. package/dist/useStore.js.map +1 -1
  32. package/dist/useStore.test.d.ts.map +1 -0
  33. package/dist/{experimental/multi-store/useStore.test.js → useStore.test.js} +20 -22
  34. package/dist/useStore.test.js.map +1 -0
  35. package/package.json +7 -7
  36. package/src/StoreRegistryContext.tsx +69 -0
  37. package/src/__tests__/fixture.tsx +1 -13
  38. package/src/experimental/components/LiveList.tsx +13 -4
  39. package/src/experimental/mod.ts +0 -1
  40. package/src/mod.ts +4 -3
  41. package/src/useClientDocument.ts +1 -5
  42. package/src/useQuery.ts +2 -6
  43. package/src/{experimental/multi-store/useStore.test.tsx → useStore.test.tsx} +32 -30
  44. package/src/useStore.ts +94 -66
  45. package/dist/LiveStoreContext.d.ts +0 -40
  46. package/dist/LiveStoreContext.d.ts.map +0 -1
  47. package/dist/LiveStoreContext.js +0 -21
  48. package/dist/LiveStoreContext.js.map +0 -1
  49. package/dist/LiveStoreProvider.d.ts +0 -73
  50. package/dist/LiveStoreProvider.d.ts.map +0 -1
  51. package/dist/LiveStoreProvider.js +0 -233
  52. package/dist/LiveStoreProvider.js.map +0 -1
  53. package/dist/LiveStoreProvider.test.d.ts +0 -2
  54. package/dist/LiveStoreProvider.test.d.ts.map +0 -1
  55. package/dist/LiveStoreProvider.test.js +0 -117
  56. package/dist/LiveStoreProvider.test.js.map +0 -1
  57. package/dist/experimental/multi-store/StoreRegistry.d.ts +0 -105
  58. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +0 -1
  59. package/dist/experimental/multi-store/StoreRegistry.js +0 -184
  60. package/dist/experimental/multi-store/StoreRegistry.js.map +0 -1
  61. package/dist/experimental/multi-store/StoreRegistry.test.d.ts +0 -2
  62. package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +0 -1
  63. package/dist/experimental/multi-store/StoreRegistry.test.js +0 -381
  64. package/dist/experimental/multi-store/StoreRegistry.test.js.map +0 -1
  65. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +0 -10
  66. package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +0 -1
  67. package/dist/experimental/multi-store/StoreRegistryContext.js +0 -15
  68. package/dist/experimental/multi-store/StoreRegistryContext.js.map +0 -1
  69. package/dist/experimental/multi-store/mod.d.ts +0 -6
  70. package/dist/experimental/multi-store/mod.d.ts.map +0 -1
  71. package/dist/experimental/multi-store/mod.js +0 -6
  72. package/dist/experimental/multi-store/mod.js.map +0 -1
  73. package/dist/experimental/multi-store/storeOptions.d.ts +0 -4
  74. package/dist/experimental/multi-store/storeOptions.d.ts.map +0 -1
  75. package/dist/experimental/multi-store/storeOptions.js +0 -4
  76. package/dist/experimental/multi-store/storeOptions.js.map +0 -1
  77. package/dist/experimental/multi-store/types.d.ts +0 -25
  78. package/dist/experimental/multi-store/types.d.ts.map +0 -1
  79. package/dist/experimental/multi-store/types.js +0 -2
  80. package/dist/experimental/multi-store/types.js.map +0 -1
  81. package/dist/experimental/multi-store/useStore.d.ts +0 -11
  82. package/dist/experimental/multi-store/useStore.d.ts.map +0 -1
  83. package/dist/experimental/multi-store/useStore.js +0 -16
  84. package/dist/experimental/multi-store/useStore.js.map +0 -1
  85. package/dist/experimental/multi-store/useStore.test.d.ts.map +0 -1
  86. package/dist/experimental/multi-store/useStore.test.js.map +0 -1
  87. package/src/LiveStoreContext.ts +0 -41
  88. package/src/LiveStoreProvider.test.tsx +0 -248
  89. package/src/LiveStoreProvider.tsx +0 -430
  90. package/src/experimental/multi-store/StoreRegistry.test.ts +0 -518
  91. package/src/experimental/multi-store/StoreRegistry.ts +0 -253
  92. package/src/experimental/multi-store/StoreRegistryContext.tsx +0 -23
  93. package/src/experimental/multi-store/mod.ts +0 -5
  94. package/src/experimental/multi-store/storeOptions.ts +0 -8
  95. package/src/experimental/multi-store/types.ts +0 -37
  96. package/src/experimental/multi-store/useStore.ts +0 -26
  97. /package/dist/{experimental/multi-store/useStore.test.d.ts → useStore.test.d.ts} +0 -0
@@ -1,253 +0,0 @@
1
- import { OtelLiveDummy, UnknownError } from '@livestore/common'
2
- import type { LiveStoreSchema } from '@livestore/common/schema'
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'
19
-
20
- /**
21
- * Default time to keep unused stores in cache.
22
- *
23
- * - Browser: 60 seconds (60,000ms)
24
- * - SSR: Infinity (disables disposal to avoid disposing stores before server render completes)
25
- *
26
- * @internal Exported primarily for testing purposes.
27
- */
28
- export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000
29
-
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
53
- /**
54
- * Optionally, pass a custom runtime that will be used to run all operations (loading, caching, etc.).
55
- */
56
- runtime?: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
57
- }
58
-
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
-
73
- constructor(options: CachedStoreOptions<TSchema>) {
74
- this.options = options
75
- }
76
-
77
- /**
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
- */
82
- [Equal.symbol](that: Equal.Equal): boolean {
83
- return that instanceof StoreCacheKey && this.options.storeId === that.options.storeId
84
- }
85
-
86
- [Hash.symbol](): number {
87
- return Hash.string(this.options.storeId)
88
- }
89
- }
90
-
91
- /**
92
- * Store Registry coordinating store loading, caching, and retention
93
- *
94
- * @public
95
- */
96
- export class StoreRegistry {
97
- /**
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.
100
- */
101
- #rcMap: RcMap.RcMap<StoreCacheKey<any>, Store, UnknownError>
102
-
103
- /**
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
- */
107
- #runtime: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
108
-
109
- /**
110
- * In-flight loading promises keyed by storeId.
111
- * Ensures concurrent `getOrLoadPromise` calls receive the same Promise reference.
112
- */
113
- #loadingPromises: Map<string, Promise<Store<any>>> = new Map()
114
-
115
- /**
116
- * Creates a new StoreRegistry instance.
117
- *
118
- * @param params.defaultOptions - Default options applied to all stores managed by this registry when they are loaded.
119
- *
120
- * @example
121
- * ```ts
122
- * const registry = new StoreRegistry({
123
- * defaultOptions: {
124
- * batchUpdates,
125
- * unusedCacheTime: 30_000,
126
- * }
127
- * })
128
- * ```
129
- */
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))
146
- }
147
-
148
- /**
149
- * Gets a cached store or loads a new one, with the store lifetime scoped to the caller.
150
- *
151
- * @typeParam TSchema - The schema type for the store
152
- * @returns An Effect that yields the store, scoped to the provided Scope
153
- *
154
- * @remarks
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
159
- */
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)
166
-
167
- return store as unknown as Store<TSchema>
168
- }).pipe(Effect.withSpan(`StoreRegistry.getOrLoad:${options.storeId}`))
169
-
170
- /**
171
- * Get or load a store, returning it directly if already loaded or a promise if loading.
172
- *
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
183
- */
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))
188
-
189
- if (Exit.isSuccess(exit)) return exit.value as Store<TSchema>
190
-
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
195
-
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>>
199
-
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>>
205
-
206
- this.#loadingPromises.set(storeId, promise)
207
- return promise
208
- }
209
-
210
- // Handle synchronous failure
211
- throw Cause.squash(exit.cause)
212
- }
213
-
214
- /**
215
- * Retains the store in cache until the returned release function is called.
216
- *
217
- * @returns A release function that, when called, removes this retention hold
218
- *
219
- * @remarks
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
222
- */
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))
232
-
233
- return () => release()
234
- }
235
-
236
- /**
237
- * Warms the cache for a store without adding a retention.
238
- *
239
- * @typeParam TSchema - The schema of the store to preload
240
- * @returns A promise that resolves when the loading is complete (success or failure)
241
- *
242
- * @remarks
243
- * - We don't return the store or throw as this is a fire-and-forget operation.
244
- * - If the entry remains unused after preload resolves/rejects, it is scheduled for disposal.
245
- */
246
- preload = async <TSchema extends LiveStoreSchema>(options: CachedStoreOptions<TSchema>): Promise<void> => {
247
- try {
248
- await this.getOrLoadPromise(options)
249
- } catch {
250
- // Do nothing; preload is best-effort
251
- }
252
- }
253
- }
@@ -1,23 +0,0 @@
1
- import * as React from 'react'
2
- import type { StoreRegistry } from './StoreRegistry.ts'
3
-
4
- export const StoreRegistryContext = React.createContext<StoreRegistry | undefined>(undefined)
5
-
6
- export type StoreRegistryProviderProps = {
7
- storeRegistry: StoreRegistry
8
- children: React.ReactNode
9
- }
10
-
11
- export const StoreRegistryProvider = ({ storeRegistry, children }: StoreRegistryProviderProps): React.JSX.Element => {
12
- return <StoreRegistryContext value={storeRegistry}>{children}</StoreRegistryContext>
13
- }
14
-
15
- export const useStoreRegistry = (override?: StoreRegistry) => {
16
- if (override) return override
17
-
18
- const storeRegistry = React.use(StoreRegistryContext)
19
-
20
- if (!storeRegistry) throw new Error('useStoreRegistry() must be used within <StoreRegistryProvider>')
21
-
22
- return storeRegistry
23
- }
@@ -1,5 +0,0 @@
1
- export * from './StoreRegistry.ts'
2
- export * from './StoreRegistryContext.tsx'
3
- export * from './storeOptions.ts'
4
- export * from './types.ts'
5
- export * from './useStore.ts'
@@ -1,8 +0,0 @@
1
- import type { LiveStoreSchema } from '@livestore/common/schema'
2
- import type { CachedStoreOptions } from './types.ts'
3
-
4
- export function storeOptions<TSchema extends LiveStoreSchema>(
5
- options: CachedStoreOptions<TSchema>,
6
- ): CachedStoreOptions<TSchema> {
7
- return options
8
- }
@@ -1,37 +0,0 @@
1
- import type { LiveStoreSchema } from '@livestore/common/schema'
2
- import type { CreateStoreOptions, OtelOptions } from '@livestore/livestore'
3
-
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>
19
- /**
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.
35
- */
36
- unusedCacheTime?: number
37
- }
@@ -1,26 +0,0 @@
1
- import type { LiveStoreSchema } from '@livestore/common/schema'
2
- import type { Store } from '@livestore/livestore'
3
- import * as React from 'react'
4
- import type { ReactApi } from '../../LiveStoreContext.ts'
5
- import { withReactApi } from '../../useStore.ts'
6
- import { useStoreRegistry } from './StoreRegistryContext.tsx'
7
- import type { CachedStoreOptions } from './types.ts'
8
-
9
- /**
10
- * Suspense and Error Boundary friendly hook.
11
- * - Returns data or throws (Promise|Error).
12
- * - No loading or error states are returned.
13
- */
14
- export const useStore = <TSchema extends LiveStoreSchema>(
15
- options: CachedStoreOptions<TSchema>,
16
- ): Store<TSchema> & ReactApi => {
17
- const storeRegistry = useStoreRegistry()
18
-
19
- React.useEffect(() => storeRegistry.retain(options), [storeRegistry, options])
20
-
21
- const storeOrPromise = React.useMemo(() => storeRegistry.getOrLoadPromise(options), [storeRegistry, options])
22
-
23
- const store = storeOrPromise instanceof Promise ? React.use(storeOrPromise) : storeOrPromise
24
-
25
- return withReactApi(store)
26
- }