@livestore/react 0.4.0-dev.14 → 0.4.0-dev.16
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 +5 -4
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +17 -6
- 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 +32 -24
- 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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Adapter, BootStatus, IntentionalShutdownCause, MigrationsReport, SyncError } from '@livestore/common'
|
|
2
|
-
import { provideOtel, UnexpectedError } from '@livestore/common'
|
|
2
|
+
import { LogConfig, provideOtel, UnexpectedError } from '@livestore/common'
|
|
3
3
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
4
4
|
import type {
|
|
5
5
|
CreateStoreOptions,
|
|
@@ -11,24 +11,14 @@ import type {
|
|
|
11
11
|
import { createStore, makeShutdownDeferred, StoreInterrupted } from '@livestore/livestore'
|
|
12
12
|
import { errorToString, IS_REACT_NATIVE, LS_DEV, omitUndefineds } from '@livestore/utils'
|
|
13
13
|
import type { OtelTracer } from '@livestore/utils/effect'
|
|
14
|
-
import {
|
|
15
|
-
Cause,
|
|
16
|
-
Deferred,
|
|
17
|
-
Effect,
|
|
18
|
-
Exit,
|
|
19
|
-
identity,
|
|
20
|
-
Logger,
|
|
21
|
-
LogLevel,
|
|
22
|
-
Schema,
|
|
23
|
-
Scope,
|
|
24
|
-
TaskTracing,
|
|
25
|
-
} from '@livestore/utils/effect'
|
|
14
|
+
import { Cause, Deferred, Effect, Exit, identity, Schema, Scope, TaskTracing } from '@livestore/utils/effect'
|
|
26
15
|
import type * as otel from '@opentelemetry/api'
|
|
27
16
|
import React from 'react'
|
|
28
17
|
|
|
29
18
|
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
30
19
|
|
|
31
|
-
export interface LiveStoreProviderProps
|
|
20
|
+
export interface LiveStoreProviderProps<TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue>
|
|
21
|
+
extends LogConfig.WithLoggerOptions {
|
|
32
22
|
schema: LiveStoreSchema
|
|
33
23
|
/**
|
|
34
24
|
* The `storeId` can be used to isolate multiple stores from each other.
|
|
@@ -77,7 +67,8 @@ export interface LiveStoreProviderProps {
|
|
|
77
67
|
*
|
|
78
68
|
* @default undefined
|
|
79
69
|
*/
|
|
80
|
-
|
|
70
|
+
syncPayloadSchema?: TSyncPayloadSchema
|
|
71
|
+
syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
|
|
81
72
|
debug?: {
|
|
82
73
|
instanceId?: string
|
|
83
74
|
}
|
|
@@ -108,7 +99,7 @@ const defaultRenderShutdown = (cause: IntentionalShutdownCause | StoreInterrupte
|
|
|
108
99
|
const defaultRenderLoading = (status: BootStatus) =>
|
|
109
100
|
IS_REACT_NATIVE ? null : <>LiveStore is loading ({status.stage})...</>
|
|
110
101
|
|
|
111
|
-
export const LiveStoreProvider = ({
|
|
102
|
+
export const LiveStoreProvider = <TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue>({
|
|
112
103
|
renderLoading = defaultRenderLoading,
|
|
113
104
|
renderError = defaultRenderError,
|
|
114
105
|
renderShutdown = defaultRenderShutdown,
|
|
@@ -123,8 +114,11 @@ export const LiveStoreProvider = ({
|
|
|
123
114
|
signal,
|
|
124
115
|
confirmUnsavedChanges = true,
|
|
125
116
|
syncPayload,
|
|
117
|
+
syncPayloadSchema,
|
|
126
118
|
debug,
|
|
127
|
-
|
|
119
|
+
logger,
|
|
120
|
+
logLevel,
|
|
121
|
+
}: LiveStoreProviderProps<TSyncPayloadSchema> & React.PropsWithChildren): React.ReactNode => {
|
|
128
122
|
const storeCtx = useCreateStore({
|
|
129
123
|
storeId,
|
|
130
124
|
schema,
|
|
@@ -137,8 +131,11 @@ export const LiveStoreProvider = ({
|
|
|
137
131
|
disableDevtools,
|
|
138
132
|
signal,
|
|
139
133
|
syncPayload,
|
|
134
|
+
syncPayloadSchema,
|
|
140
135
|
debug,
|
|
141
136
|
}),
|
|
137
|
+
logger,
|
|
138
|
+
logLevel,
|
|
142
139
|
})
|
|
143
140
|
|
|
144
141
|
if (storeCtx.stage === 'error') {
|
|
@@ -175,11 +172,15 @@ const useCreateStore = ({
|
|
|
175
172
|
params,
|
|
176
173
|
confirmUnsavedChanges,
|
|
177
174
|
syncPayload,
|
|
175
|
+
syncPayloadSchema,
|
|
178
176
|
debug,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
logger,
|
|
178
|
+
logLevel,
|
|
179
|
+
}: CreateStoreOptions<LiveStoreSchema> &
|
|
180
|
+
LogConfig.WithLoggerOptions & {
|
|
181
|
+
signal?: AbortSignal
|
|
182
|
+
otelOptions?: Partial<OtelOptions>
|
|
183
|
+
}) => {
|
|
183
184
|
const [_, rerender] = React.useState(0)
|
|
184
185
|
const ctxValueRef = React.useRef<{
|
|
185
186
|
value: StoreContext_ | BootStatus
|
|
@@ -211,6 +212,7 @@ const useCreateStore = ({
|
|
|
211
212
|
params,
|
|
212
213
|
confirmUnsavedChanges,
|
|
213
214
|
syncPayload,
|
|
215
|
+
syncPayloadSchema,
|
|
214
216
|
debugInstanceId,
|
|
215
217
|
})
|
|
216
218
|
|
|
@@ -239,6 +241,7 @@ const useCreateStore = ({
|
|
|
239
241
|
params: inputPropsCacheRef.current.params !== params,
|
|
240
242
|
confirmUnsavedChanges: inputPropsCacheRef.current.confirmUnsavedChanges !== confirmUnsavedChanges,
|
|
241
243
|
syncPayload: inputPropsCacheRef.current.syncPayload !== syncPayload,
|
|
244
|
+
syncPayloadSchema: inputPropsCacheRef.current.syncPayloadSchema !== syncPayloadSchema,
|
|
242
245
|
debugInstanceId: inputPropsCacheRef.current.debugInstanceId !== debugInstanceId,
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -253,7 +256,8 @@ const useCreateStore = ({
|
|
|
253
256
|
inputPropChanges.context ||
|
|
254
257
|
inputPropChanges.params ||
|
|
255
258
|
inputPropChanges.confirmUnsavedChanges ||
|
|
256
|
-
inputPropChanges.syncPayload
|
|
259
|
+
inputPropChanges.syncPayload ||
|
|
260
|
+
inputPropChanges.syncPayloadSchema
|
|
257
261
|
) {
|
|
258
262
|
inputPropsCacheRef.current = {
|
|
259
263
|
schema,
|
|
@@ -267,6 +271,7 @@ const useCreateStore = ({
|
|
|
267
271
|
params,
|
|
268
272
|
confirmUnsavedChanges,
|
|
269
273
|
syncPayload,
|
|
274
|
+
syncPayloadSchema,
|
|
270
275
|
debugInstanceId,
|
|
271
276
|
}
|
|
272
277
|
if (ctxValueRef.current.componentScope !== undefined && ctxValueRef.current.shutdownDeferred !== undefined) {
|
|
@@ -342,6 +347,7 @@ const useCreateStore = ({
|
|
|
342
347
|
params,
|
|
343
348
|
confirmUnsavedChanges,
|
|
344
349
|
syncPayload,
|
|
350
|
+
syncPayloadSchema,
|
|
345
351
|
}),
|
|
346
352
|
onBootStatus: (status) => {
|
|
347
353
|
if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
|
|
@@ -374,8 +380,7 @@ const useCreateStore = ({
|
|
|
374
380
|
provideOtel(omitUndefineds({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer })),
|
|
375
381
|
Effect.tapCauseLogPretty,
|
|
376
382
|
Effect.annotateLogs({ thread: 'window' }),
|
|
377
|
-
|
|
378
|
-
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
383
|
+
LogConfig.withLoggerConfig({ logger, logLevel }, { threadName: 'window' }),
|
|
379
384
|
Effect.runCallback,
|
|
380
385
|
)
|
|
381
386
|
|
|
@@ -405,8 +410,11 @@ const useCreateStore = ({
|
|
|
405
410
|
params,
|
|
406
411
|
confirmUnsavedChanges,
|
|
407
412
|
syncPayload,
|
|
413
|
+
syncPayloadSchema,
|
|
408
414
|
debugInstanceId,
|
|
409
415
|
interrupt,
|
|
416
|
+
logger,
|
|
417
|
+
logLevel,
|
|
410
418
|
])
|
|
411
419
|
|
|
412
420
|
return ctxValueRef.current.value
|
|
@@ -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(() => {
|