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