@livestore/react 0.4.0-dev.14 → 0.4.0-dev.15
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/LiveStoreProvider.d.ts +4 -3
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +10 -3
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/LiveStoreProvider.test.js +3 -3
- package/dist/LiveStoreProvider.test.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +1 -1
- package/dist/__tests__/fixture.js +1 -1
- package/dist/experimental/components/LiveList.js +1 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +4 -17
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.js +118 -165
- package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts +1 -1
- package/dist/experimental/multi-store/useStore.d.ts.map +1 -1
- package/dist/experimental/multi-store/useStore.js +2 -6
- package/dist/experimental/multi-store/useStore.js.map +1 -1
- package/dist/useClientDocument.test.js +2 -2
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.test.js +2 -2
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.test.js +1 -1
- package/package.json +6 -6
- package/src/LiveStoreProvider.test.tsx +3 -3
- package/src/LiveStoreProvider.tsx +15 -5
- package/src/__tests__/fixture.tsx +1 -1
- package/src/experimental/components/LiveList.tsx +1 -1
- package/src/experimental/multi-store/StoreRegistry.ts +121 -173
- package/src/experimental/multi-store/StoreRegistryContext.tsx +1 -1
- package/src/experimental/multi-store/useStore.ts +2 -11
- package/src/useClientDocument.test.tsx +3 -3
- package/src/useQuery.test.tsx +2 -2
- package/src/useRcResource.test.tsx +1 -1
|
@@ -7,9 +7,9 @@ import React from 'react'
|
|
|
7
7
|
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
|
|
8
8
|
import { describe, expect, it } from 'vitest'
|
|
9
9
|
|
|
10
|
-
import { events, schema, tables } from './__tests__/fixture.
|
|
11
|
-
import { LiveStoreProvider } from './LiveStoreProvider.
|
|
12
|
-
import * as LiveStoreReact from './mod.
|
|
10
|
+
import { events, schema, tables } from './__tests__/fixture.tsx'
|
|
11
|
+
import { LiveStoreProvider } from './LiveStoreProvider.tsx'
|
|
12
|
+
import * as LiveStoreReact from './mod.ts'
|
|
13
13
|
|
|
14
14
|
describe.each([true, false])('LiveStoreProvider (strictMode: %s)', (strictMode) => {
|
|
15
15
|
const WithStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
@@ -28,7 +28,7 @@ import React from 'react'
|
|
|
28
28
|
|
|
29
29
|
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
30
30
|
|
|
31
|
-
export interface LiveStoreProviderProps {
|
|
31
|
+
export interface LiveStoreProviderProps<TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue> {
|
|
32
32
|
schema: LiveStoreSchema
|
|
33
33
|
/**
|
|
34
34
|
* The `storeId` can be used to isolate multiple stores from each other.
|
|
@@ -77,7 +77,8 @@ export interface LiveStoreProviderProps {
|
|
|
77
77
|
*
|
|
78
78
|
* @default undefined
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
syncPayloadSchema?: TSyncPayloadSchema
|
|
81
|
+
syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
|
|
81
82
|
debug?: {
|
|
82
83
|
instanceId?: string
|
|
83
84
|
}
|
|
@@ -108,7 +109,7 @@ const defaultRenderShutdown = (cause: IntentionalShutdownCause | StoreInterrupte
|
|
|
108
109
|
const defaultRenderLoading = (status: BootStatus) =>
|
|
109
110
|
IS_REACT_NATIVE ? null : <>LiveStore is loading ({status.stage})...</>
|
|
110
111
|
|
|
111
|
-
export const LiveStoreProvider = ({
|
|
112
|
+
export const LiveStoreProvider = <TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue>({
|
|
112
113
|
renderLoading = defaultRenderLoading,
|
|
113
114
|
renderError = defaultRenderError,
|
|
114
115
|
renderShutdown = defaultRenderShutdown,
|
|
@@ -123,8 +124,9 @@ export const LiveStoreProvider = ({
|
|
|
123
124
|
signal,
|
|
124
125
|
confirmUnsavedChanges = true,
|
|
125
126
|
syncPayload,
|
|
127
|
+
syncPayloadSchema,
|
|
126
128
|
debug,
|
|
127
|
-
}: LiveStoreProviderProps & React.PropsWithChildren): React.ReactNode => {
|
|
129
|
+
}: LiveStoreProviderProps<TSyncPayloadSchema> & React.PropsWithChildren): React.ReactNode => {
|
|
128
130
|
const storeCtx = useCreateStore({
|
|
129
131
|
storeId,
|
|
130
132
|
schema,
|
|
@@ -137,6 +139,7 @@ export const LiveStoreProvider = ({
|
|
|
137
139
|
disableDevtools,
|
|
138
140
|
signal,
|
|
139
141
|
syncPayload,
|
|
142
|
+
syncPayloadSchema,
|
|
140
143
|
debug,
|
|
141
144
|
}),
|
|
142
145
|
})
|
|
@@ -175,6 +178,7 @@ const useCreateStore = ({
|
|
|
175
178
|
params,
|
|
176
179
|
confirmUnsavedChanges,
|
|
177
180
|
syncPayload,
|
|
181
|
+
syncPayloadSchema,
|
|
178
182
|
debug,
|
|
179
183
|
}: CreateStoreOptions<LiveStoreSchema> & {
|
|
180
184
|
signal?: AbortSignal
|
|
@@ -211,6 +215,7 @@ const useCreateStore = ({
|
|
|
211
215
|
params,
|
|
212
216
|
confirmUnsavedChanges,
|
|
213
217
|
syncPayload,
|
|
218
|
+
syncPayloadSchema,
|
|
214
219
|
debugInstanceId,
|
|
215
220
|
})
|
|
216
221
|
|
|
@@ -239,6 +244,7 @@ const useCreateStore = ({
|
|
|
239
244
|
params: inputPropsCacheRef.current.params !== params,
|
|
240
245
|
confirmUnsavedChanges: inputPropsCacheRef.current.confirmUnsavedChanges !== confirmUnsavedChanges,
|
|
241
246
|
syncPayload: inputPropsCacheRef.current.syncPayload !== syncPayload,
|
|
247
|
+
syncPayloadSchema: inputPropsCacheRef.current.syncPayloadSchema !== syncPayloadSchema,
|
|
242
248
|
debugInstanceId: inputPropsCacheRef.current.debugInstanceId !== debugInstanceId,
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -253,7 +259,8 @@ const useCreateStore = ({
|
|
|
253
259
|
inputPropChanges.context ||
|
|
254
260
|
inputPropChanges.params ||
|
|
255
261
|
inputPropChanges.confirmUnsavedChanges ||
|
|
256
|
-
inputPropChanges.syncPayload
|
|
262
|
+
inputPropChanges.syncPayload ||
|
|
263
|
+
inputPropChanges.syncPayloadSchema
|
|
257
264
|
) {
|
|
258
265
|
inputPropsCacheRef.current = {
|
|
259
266
|
schema,
|
|
@@ -267,6 +274,7 @@ const useCreateStore = ({
|
|
|
267
274
|
params,
|
|
268
275
|
confirmUnsavedChanges,
|
|
269
276
|
syncPayload,
|
|
277
|
+
syncPayloadSchema,
|
|
270
278
|
debugInstanceId,
|
|
271
279
|
}
|
|
272
280
|
if (ctxValueRef.current.componentScope !== undefined && ctxValueRef.current.shutdownDeferred !== undefined) {
|
|
@@ -342,6 +350,7 @@ const useCreateStore = ({
|
|
|
342
350
|
params,
|
|
343
351
|
confirmUnsavedChanges,
|
|
344
352
|
syncPayload,
|
|
353
|
+
syncPayloadSchema,
|
|
345
354
|
}),
|
|
346
355
|
onBootStatus: (status) => {
|
|
347
356
|
if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
|
|
@@ -405,6 +414,7 @@ const useCreateStore = ({
|
|
|
405
414
|
params,
|
|
406
415
|
confirmUnsavedChanges,
|
|
407
416
|
syncPayload,
|
|
417
|
+
syncPayloadSchema,
|
|
408
418
|
debugInstanceId,
|
|
409
419
|
interrupt,
|
|
410
420
|
])
|
|
@@ -8,7 +8,7 @@ import { Effect, Schema, type Scope } from '@livestore/utils/effect'
|
|
|
8
8
|
import type * as otel from '@opentelemetry/api'
|
|
9
9
|
import React from 'react'
|
|
10
10
|
|
|
11
|
-
import * as LiveStoreReact from '../mod.
|
|
11
|
+
import * as LiveStoreReact from '../mod.ts'
|
|
12
12
|
|
|
13
13
|
export type Todo = {
|
|
14
14
|
id: string
|
|
@@ -1,48 +1,78 @@
|
|
|
1
1
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
2
2
|
import { createStorePromise, type Store, type Unsubscribe } from '@livestore/livestore'
|
|
3
|
-
import { noop } from '@livestore/utils'
|
|
4
3
|
import type { CachedStoreOptions, StoreId } from './types.ts'
|
|
5
4
|
|
|
5
|
+
type StoreEntryState<TSchema extends LiveStoreSchema> =
|
|
6
|
+
| { status: 'idle' }
|
|
7
|
+
| { status: 'loading'; promise: Promise<Store<TSchema>> }
|
|
8
|
+
| { status: 'success'; store: Store<TSchema> }
|
|
9
|
+
| { status: 'error'; error: unknown }
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
|
-
* Minimal cache entry that tracks store
|
|
12
|
+
* Minimal cache entry that tracks store state and subscribers.
|
|
8
13
|
*
|
|
9
14
|
* @typeParam TSchema - The schema for this entry's store.
|
|
10
15
|
* @internal
|
|
11
16
|
*/
|
|
12
17
|
class StoreEntry<TSchema extends LiveStoreSchema = LiveStoreSchema> {
|
|
18
|
+
#state: StoreEntryState<TSchema> = { status: 'idle' }
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @remarks
|
|
17
|
-
* A value of `undefined` indicates "not loaded yet".
|
|
21
|
+
* Set of subscriber callbacks to notify on state changes.
|
|
18
22
|
*/
|
|
19
|
-
|
|
23
|
+
#subscribers = new Set<() => void>()
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
|
-
* The
|
|
26
|
+
* The number of active subscribers for this entry.
|
|
23
27
|
*/
|
|
24
|
-
|
|
28
|
+
get subscriberCount() {
|
|
29
|
+
return this.#subscribers.size
|
|
30
|
+
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
|
-
*
|
|
33
|
+
* Transitions to the loading state.
|
|
28
34
|
*/
|
|
29
|
-
promise: Promise<Store<TSchema>>
|
|
35
|
+
#setPromise(promise: Promise<Store<TSchema>>): void {
|
|
36
|
+
if (this.#state.status === 'success' || this.#state.status === 'loading') return
|
|
37
|
+
this.#state = { status: 'loading', promise }
|
|
38
|
+
this.#notify()
|
|
39
|
+
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
|
-
*
|
|
42
|
+
* Transitions to the success state.
|
|
33
43
|
*/
|
|
34
|
-
#
|
|
44
|
+
#setStore = (store: Store<TSchema>): void => {
|
|
45
|
+
this.#state = { status: 'success', store }
|
|
46
|
+
this.#notify()
|
|
47
|
+
}
|
|
35
48
|
|
|
36
49
|
/**
|
|
37
|
-
*
|
|
50
|
+
* Transitions to the error state.
|
|
38
51
|
*/
|
|
39
|
-
|
|
52
|
+
#setError = (error: unknown): void => {
|
|
53
|
+
this.#state = { status: 'error', error }
|
|
54
|
+
this.#notify()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#reset = (): void => {
|
|
58
|
+
this.#state = { status: 'idle' }
|
|
59
|
+
this.#notify()
|
|
60
|
+
}
|
|
40
61
|
|
|
41
62
|
/**
|
|
42
|
-
*
|
|
63
|
+
* Notifies all subscribers of state changes.
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* This should be called after any meaningful state change.
|
|
43
67
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
#notify = (): void => {
|
|
69
|
+
for (const sub of this.#subscribers) {
|
|
70
|
+
try {
|
|
71
|
+
sub()
|
|
72
|
+
} catch {
|
|
73
|
+
// Swallow to protect other listeners
|
|
74
|
+
}
|
|
75
|
+
}
|
|
46
76
|
}
|
|
47
77
|
|
|
48
78
|
/**
|
|
@@ -59,33 +89,42 @@ class StoreEntry<TSchema extends LiveStoreSchema = LiveStoreSchema> {
|
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
/**
|
|
62
|
-
*
|
|
92
|
+
* Initiates loading of the store if not already in progress.
|
|
93
|
+
*
|
|
94
|
+
* @param options - Store creation options
|
|
95
|
+
* @returns Promise that resolves to the loaded store or rejects with an error
|
|
63
96
|
*
|
|
64
97
|
* @remarks
|
|
65
|
-
* This
|
|
98
|
+
* This method handles the complete lifecycle of loading a store:
|
|
99
|
+
* - Creates the store promise via createStorePromise
|
|
100
|
+
* - Transitions through loading → success/error states
|
|
101
|
+
* - Invokes onSettle callback for GC scheduling when needed
|
|
66
102
|
*/
|
|
67
|
-
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
getOrLoad = (options: CachedStoreOptions<TSchema>): Store<TSchema> | Promise<Store<TSchema>> => {
|
|
104
|
+
if (this.#state.status === 'success') return this.#state.store
|
|
105
|
+
if (this.#state.status === 'loading') return this.#state.promise
|
|
106
|
+
if (this.#state.status === 'error') throw this.#state.error
|
|
107
|
+
|
|
108
|
+
const promise = createStorePromise(options)
|
|
109
|
+
.then((store) => {
|
|
110
|
+
this.#setStore(store)
|
|
111
|
+
return store
|
|
112
|
+
})
|
|
113
|
+
.catch((error) => {
|
|
114
|
+
this.#setError(error)
|
|
115
|
+
throw error
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
this.#setPromise(promise)
|
|
77
119
|
|
|
78
|
-
|
|
79
|
-
this.store = store
|
|
80
|
-
this.error = undefined
|
|
81
|
-
this.promise = undefined
|
|
82
|
-
this.notify()
|
|
120
|
+
return promise
|
|
83
121
|
}
|
|
84
122
|
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
this.
|
|
88
|
-
|
|
123
|
+
shutdown = async (): Promise<void> => {
|
|
124
|
+
if (this.#state.status !== 'success') return
|
|
125
|
+
await this.#state.store.shutdownPromise()
|
|
126
|
+
|
|
127
|
+
this.#reset()
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
130
|
|
|
@@ -104,7 +143,7 @@ class StoreCache {
|
|
|
104
143
|
return this.#entries.get(storeId) as StoreEntry<TSchema> | undefined
|
|
105
144
|
}
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
ensure = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> => {
|
|
108
147
|
let entry = this.#entries.get(storeId) as StoreEntry<TSchema> | undefined
|
|
109
148
|
|
|
110
149
|
if (!entry) {
|
|
@@ -116,29 +155,18 @@ class StoreCache {
|
|
|
116
155
|
}
|
|
117
156
|
|
|
118
157
|
/**
|
|
119
|
-
* Removes an entry from the cache
|
|
158
|
+
* Removes an entry from the cache.
|
|
120
159
|
*
|
|
121
160
|
* @param storeId - The ID of the store to remove
|
|
161
|
+
*
|
|
122
162
|
* @remarks
|
|
123
|
-
*
|
|
163
|
+
* - Invokes shutdown on the store before removal.
|
|
124
164
|
*/
|
|
125
165
|
remove = (storeId: StoreId): void => {
|
|
126
166
|
const entry = this.#entries.get(storeId)
|
|
127
167
|
if (!entry) return
|
|
168
|
+
void entry.shutdown()
|
|
128
169
|
this.#entries.delete(storeId)
|
|
129
|
-
// Notify any subscribers of the removal to force re-render;
|
|
130
|
-
// components will resubscribe to a new entry and re-read.
|
|
131
|
-
try {
|
|
132
|
-
entry.notify()
|
|
133
|
-
} catch {
|
|
134
|
-
// Best-effort notify; swallowing to avoid crashing removal flows.
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
clear = (): void => {
|
|
139
|
-
for (const storeId of Array.from(this.#entries.keys())) {
|
|
140
|
-
this.remove(storeId)
|
|
141
|
-
}
|
|
142
170
|
}
|
|
143
171
|
}
|
|
144
172
|
|
|
@@ -187,112 +215,59 @@ export class StoreRegistry {
|
|
|
187
215
|
this.#defaultOptions = defaultOptions
|
|
188
216
|
}
|
|
189
217
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
* @internal
|
|
197
|
-
*/
|
|
198
|
-
ensureStoreEntry = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> => {
|
|
199
|
-
return this.#cache.getOrCreate<TSchema>(storeId)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Resolves a store instance for imperative code paths.
|
|
204
|
-
*
|
|
205
|
-
* @typeParam TSchema - Schema associated with the requested store.
|
|
206
|
-
* @returns A promise that resolves with the ready store or rejects with the loading error.
|
|
207
|
-
*
|
|
208
|
-
* @remarks
|
|
209
|
-
* - If the store is already cached, the returned promise resolves immediately with that instance.
|
|
210
|
-
* - Concurrent callers share the same in-flight request to avoid duplicate store creation.
|
|
211
|
-
*/
|
|
212
|
-
load = async <TSchema extends LiveStoreSchema>(options: CachedStoreOptions<TSchema>): Promise<Store<TSchema>> => {
|
|
213
|
-
const optionsWithDefaults = this.#applyDefaultOptions(options)
|
|
214
|
-
const entry = this.ensureStoreEntry<TSchema>(optionsWithDefaults.storeId)
|
|
215
|
-
|
|
216
|
-
// If already loaded, return it
|
|
217
|
-
if (entry.store) return entry.store
|
|
218
|
-
|
|
219
|
-
// If a load is already in flight, return its promise
|
|
220
|
-
if (entry.promise) return entry.promise
|
|
221
|
-
|
|
222
|
-
// If a previous error exists, throw it
|
|
223
|
-
if (entry.error !== undefined) throw entry.error
|
|
224
|
-
|
|
225
|
-
// Load store if none is in flight
|
|
226
|
-
entry.promise = createStorePromise(optionsWithDefaults)
|
|
227
|
-
.then((store) => {
|
|
228
|
-
entry.setStore(store)
|
|
229
|
-
|
|
230
|
-
// If no one subscribed (e.g., initial render aborted), schedule GC.
|
|
231
|
-
if (entry.subscriberCount === 0) this.#scheduleGC(optionsWithDefaults.storeId)
|
|
232
|
-
|
|
233
|
-
return store
|
|
234
|
-
})
|
|
235
|
-
.catch((error) => {
|
|
236
|
-
entry.setError(error)
|
|
237
|
-
|
|
238
|
-
// Likewise, ensure unused entries are eventually collected.
|
|
239
|
-
if (entry.subscriberCount === 0) this.#scheduleGC(optionsWithDefaults.storeId)
|
|
218
|
+
#applyDefaultOptions = <TSchema extends LiveStoreSchema>(
|
|
219
|
+
options: CachedStoreOptions<TSchema>,
|
|
220
|
+
): CachedStoreOptions<TSchema> => ({
|
|
221
|
+
...this.#defaultOptions,
|
|
222
|
+
...options,
|
|
223
|
+
})
|
|
240
224
|
|
|
241
|
-
|
|
242
|
-
|
|
225
|
+
#scheduleGC = (id: StoreId): void => {
|
|
226
|
+
this.#cancelGC(id)
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
this.#gcTimeouts.delete(id)
|
|
229
|
+
this.#cache.remove(id)
|
|
230
|
+
}, DEFAULT_GC_TIME)
|
|
231
|
+
this.#gcTimeouts.set(id, timer)
|
|
232
|
+
}
|
|
243
233
|
|
|
244
|
-
|
|
234
|
+
#cancelGC = (id: StoreId): void => {
|
|
235
|
+
const t = this.#gcTimeouts.get(id)
|
|
236
|
+
if (t) {
|
|
237
|
+
clearTimeout(t)
|
|
238
|
+
this.#gcTimeouts.delete(id)
|
|
239
|
+
}
|
|
245
240
|
}
|
|
246
241
|
|
|
247
242
|
/**
|
|
248
|
-
*
|
|
249
|
-
* Designed to work with React.use() for Suspense integration.
|
|
243
|
+
* Get or load a store, returning it directly if loaded or a promise if loading.
|
|
250
244
|
*
|
|
251
245
|
* @typeParam TSchema - The schema of the store to load
|
|
252
246
|
* @returns The loaded store if available, or a Promise that resolves to the store if loading
|
|
253
|
-
* @throws unknown loading error
|
|
247
|
+
* @throws unknown loading error
|
|
254
248
|
*
|
|
255
249
|
* @remarks
|
|
250
|
+
* - Designed to work with React.use() for Suspense integration.
|
|
256
251
|
* - When the store is already loaded, returns the store instance directly (not wrapped in a Promise)
|
|
257
252
|
* - When loading, returns a stable Promise reference that can be used with React.use()
|
|
258
253
|
* - This prevents re-suspension on subsequent renders when the store is already loaded
|
|
259
|
-
* - If the initial render that triggered the fetch never commits, we still schedule GC on settle.
|
|
260
254
|
*/
|
|
261
|
-
|
|
255
|
+
getOrLoad = <TSchema extends LiveStoreSchema>(
|
|
262
256
|
options: CachedStoreOptions<TSchema>,
|
|
263
257
|
): Store<TSchema> | Promise<Store<TSchema>> => {
|
|
264
258
|
const optionsWithDefaults = this.#applyDefaultOptions(options)
|
|
265
|
-
const entry = this.
|
|
266
|
-
|
|
267
|
-
// If already loaded, return it directly (not wrapped in Promise)
|
|
268
|
-
if (entry.store) return entry.store
|
|
259
|
+
const entry = this.#cache.ensure<TSchema>(optionsWithDefaults.storeId)
|
|
269
260
|
|
|
270
|
-
|
|
271
|
-
if (entry.error !== undefined) throw entry.error
|
|
261
|
+
const storeOrPromise = entry.getOrLoad(optionsWithDefaults)
|
|
272
262
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Load store if none is in flight
|
|
277
|
-
entry.promise = createStorePromise(optionsWithDefaults)
|
|
278
|
-
.then((store) => {
|
|
279
|
-
entry.setStore(store)
|
|
280
|
-
|
|
281
|
-
// If no one subscribed (e.g., initial render aborted), schedule GC.
|
|
263
|
+
if (storeOrPromise instanceof Promise) {
|
|
264
|
+
return storeOrPromise.finally(() => {
|
|
265
|
+
// If no subscribers remain after load settles, schedule GC
|
|
282
266
|
if (entry.subscriberCount === 0) this.#scheduleGC(optionsWithDefaults.storeId)
|
|
283
|
-
|
|
284
|
-
return store
|
|
285
|
-
})
|
|
286
|
-
.catch((error) => {
|
|
287
|
-
entry.setError(error)
|
|
288
|
-
|
|
289
|
-
// Likewise, ensure unused entries are eventually collected.
|
|
290
|
-
if (entry.subscriberCount === 0) this.#scheduleGC(optionsWithDefaults.storeId)
|
|
291
|
-
|
|
292
|
-
throw error
|
|
293
267
|
})
|
|
268
|
+
}
|
|
294
269
|
|
|
295
|
-
return
|
|
270
|
+
return storeOrPromise
|
|
296
271
|
}
|
|
297
272
|
|
|
298
273
|
/**
|
|
@@ -306,11 +281,15 @@ export class StoreRegistry {
|
|
|
306
281
|
* - If the entry remains unused after preload resolves/rejects, it is scheduled for GC.
|
|
307
282
|
*/
|
|
308
283
|
preload = async <TSchema extends LiveStoreSchema>(options: CachedStoreOptions<TSchema>): Promise<void> => {
|
|
309
|
-
|
|
284
|
+
try {
|
|
285
|
+
await this.getOrLoad(options)
|
|
286
|
+
} catch {
|
|
287
|
+
// Do nothing; preload is best-effort
|
|
288
|
+
}
|
|
310
289
|
}
|
|
311
290
|
|
|
312
291
|
subscribe = <TSchema extends LiveStoreSchema>(storeId: StoreId, listener: () => void): Unsubscribe => {
|
|
313
|
-
const entry = this.
|
|
292
|
+
const entry = this.#cache.ensure<TSchema>(storeId)
|
|
314
293
|
// Active subscriber: cancel any scheduled GC
|
|
315
294
|
this.#cancelGC(storeId)
|
|
316
295
|
|
|
@@ -319,38 +298,7 @@ export class StoreRegistry {
|
|
|
319
298
|
return () => {
|
|
320
299
|
unsubscribe()
|
|
321
300
|
// If no more subscribers remain, schedule GC
|
|
322
|
-
if (entry.subscriberCount === 0)
|
|
323
|
-
this.#scheduleGC(storeId)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
getVersion = <TSchema extends LiveStoreSchema>(storeId: StoreId): number => {
|
|
329
|
-
const entry = this.ensureStoreEntry<TSchema>(storeId)
|
|
330
|
-
return entry.version
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
#applyDefaultOptions = <TSchema extends LiveStoreSchema>(
|
|
334
|
-
options: CachedStoreOptions<TSchema>,
|
|
335
|
-
): CachedStoreOptions<TSchema> => ({
|
|
336
|
-
...this.#defaultOptions,
|
|
337
|
-
...options,
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
#scheduleGC = (id: StoreId): void => {
|
|
341
|
-
this.#cancelGC(id)
|
|
342
|
-
const timer = setTimeout(() => {
|
|
343
|
-
this.#gcTimeouts.delete(id)
|
|
344
|
-
this.#cache.remove(id)
|
|
345
|
-
}, DEFAULT_GC_TIME)
|
|
346
|
-
this.#gcTimeouts.set(id, timer)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#cancelGC = (id: StoreId): void => {
|
|
350
|
-
const t = this.#gcTimeouts.get(id)
|
|
351
|
-
if (t) {
|
|
352
|
-
clearTimeout(t)
|
|
353
|
-
this.#gcTimeouts.delete(id)
|
|
301
|
+
if (entry.subscriberCount === 0) this.#scheduleGC(storeId)
|
|
354
302
|
}
|
|
355
303
|
}
|
|
356
304
|
}
|
|
@@ -16,23 +16,14 @@ export const useStore = <TSchema extends LiveStoreSchema>(
|
|
|
16
16
|
): Store<TSchema> & ReactApi => {
|
|
17
17
|
const storeRegistry = useStoreRegistry()
|
|
18
18
|
|
|
19
|
-
storeRegistry.ensureStoreEntry(options.storeId)
|
|
20
|
-
|
|
21
19
|
const subscribe = React.useCallback(
|
|
22
20
|
(onChange: () => void) => storeRegistry.subscribe(options.storeId, onChange),
|
|
23
21
|
[storeRegistry, options.storeId],
|
|
24
22
|
)
|
|
25
|
-
const getSnapshot = React.useCallback(
|
|
26
|
-
() => storeRegistry.getVersion(options.storeId),
|
|
27
|
-
[storeRegistry, options.storeId],
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
23
|
+
const getSnapshot = React.useCallback(() => storeRegistry.getOrLoad(options), [storeRegistry, options])
|
|
31
24
|
|
|
32
|
-
const storeOrPromise =
|
|
25
|
+
const storeOrPromise = React.useSyncExternalStore(subscribe, getSnapshot)
|
|
33
26
|
|
|
34
|
-
// If read() returns a Promise, use React.use() to suspend
|
|
35
|
-
// If it returns a Store directly, use it immediately
|
|
36
27
|
const loadedStore = storeOrPromise instanceof Promise ? React.use(storeOrPromise) : storeOrPromise
|
|
37
28
|
|
|
38
29
|
return withReactApi(loadedStore)
|
|
@@ -10,9 +10,9 @@ import * as ReactTesting from '@testing-library/react'
|
|
|
10
10
|
import type React from 'react'
|
|
11
11
|
import { beforeEach, expect, it } from 'vitest'
|
|
12
12
|
|
|
13
|
-
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.
|
|
14
|
-
import type * as LiveStoreReact from './mod.
|
|
15
|
-
import { __resetUseRcResourceCache } from './useRcResource.
|
|
13
|
+
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
14
|
+
import type * as LiveStoreReact from './mod.ts'
|
|
15
|
+
import { __resetUseRcResourceCache } from './useRcResource.ts'
|
|
16
16
|
|
|
17
17
|
// const strictMode = process.env.REACT_STRICT_MODE !== undefined
|
|
18
18
|
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -10,8 +10,8 @@ import React from 'react'
|
|
|
10
10
|
import * as ReactWindow from 'react-window'
|
|
11
11
|
import { expect } from 'vitest'
|
|
12
12
|
|
|
13
|
-
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.
|
|
14
|
-
import { __resetUseRcResourceCache } from './useRcResource.
|
|
13
|
+
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
14
|
+
import { __resetUseRcResourceCache } from './useRcResource.ts'
|
|
15
15
|
|
|
16
16
|
Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
17
17
|
'useQuery (strictMode=%s)',
|
|
@@ -2,7 +2,7 @@ import * as ReactTesting from '@testing-library/react'
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { __resetUseRcResourceCache, useRcResource } from './useRcResource.
|
|
5
|
+
import { __resetUseRcResourceCache, useRcResource } from './useRcResource.ts'
|
|
6
6
|
|
|
7
7
|
describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (strictMode=%s)', ({ strictMode }) => {
|
|
8
8
|
beforeEach(() => {
|