@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.
Files changed (34) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreProvider.d.ts +4 -3
  3. package/dist/LiveStoreProvider.d.ts.map +1 -1
  4. package/dist/LiveStoreProvider.js +10 -3
  5. package/dist/LiveStoreProvider.js.map +1 -1
  6. package/dist/LiveStoreProvider.test.js +3 -3
  7. package/dist/LiveStoreProvider.test.js.map +1 -1
  8. package/dist/__tests__/fixture.d.ts +1 -1
  9. package/dist/__tests__/fixture.js +1 -1
  10. package/dist/experimental/components/LiveList.js +1 -1
  11. package/dist/experimental/multi-store/StoreRegistry.d.ts +4 -17
  12. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
  13. package/dist/experimental/multi-store/StoreRegistry.js +118 -165
  14. package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
  15. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +1 -1
  16. package/dist/experimental/multi-store/useStore.d.ts.map +1 -1
  17. package/dist/experimental/multi-store/useStore.js +2 -6
  18. package/dist/experimental/multi-store/useStore.js.map +1 -1
  19. package/dist/useClientDocument.test.js +2 -2
  20. package/dist/useClientDocument.test.js.map +1 -1
  21. package/dist/useQuery.test.js +2 -2
  22. package/dist/useQuery.test.js.map +1 -1
  23. package/dist/useRcResource.test.js +1 -1
  24. package/package.json +6 -6
  25. package/src/LiveStoreProvider.test.tsx +3 -3
  26. package/src/LiveStoreProvider.tsx +15 -5
  27. package/src/__tests__/fixture.tsx +1 -1
  28. package/src/experimental/components/LiveList.tsx +1 -1
  29. package/src/experimental/multi-store/StoreRegistry.ts +121 -173
  30. package/src/experimental/multi-store/StoreRegistryContext.tsx +1 -1
  31. package/src/experimental/multi-store/useStore.ts +2 -11
  32. package/src/useClientDocument.test.tsx +3 -3
  33. package/src/useQuery.test.tsx +2 -2
  34. 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.js'
11
- import { LiveStoreProvider } from './LiveStoreProvider.js'
12
- import * as LiveStoreReact from './mod.js'
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
- syncPayload?: Schema.JsonValue
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.js'
11
+ import * as LiveStoreReact from '../mod.ts'
12
12
 
13
13
  export type Todo = {
14
14
  id: string
@@ -2,7 +2,7 @@ import type { LiveQueryDef } from '@livestore/livestore'
2
2
  import { computed } from '@livestore/livestore'
3
3
  import React from 'react'
4
4
 
5
- import { useQuery } from '../../useQuery.js'
5
+ import { useQuery } from '../../useQuery.ts'
6
6
 
7
7
  /*
8
8
  TODO:
@@ -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, error, and in-flight promise along with subscribers.
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
- * The resolved store.
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
- store: Store<TSchema> | undefined = undefined
23
+ #subscribers = new Set<() => void>()
20
24
 
21
25
  /**
22
- * The most recent error encountered for this entry, if any.
26
+ * The number of active subscribers for this entry.
23
27
  */
24
- error: unknown = undefined
28
+ get subscriberCount() {
29
+ return this.#subscribers.size
30
+ }
25
31
 
26
32
  /**
27
- * The in-flight promise for loading the store, or `undefined` if not yet loading or already resolved.
33
+ * Transitions to the loading state.
28
34
  */
29
- promise: Promise<Store<TSchema>> | undefined = undefined
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
- * Set of subscriber callbacks to notify on state changes.
42
+ * Transitions to the success state.
33
43
  */
34
- #subscribers = new Set<() => void>()
44
+ #setStore = (store: Store<TSchema>): void => {
45
+ this.#state = { status: 'success', store }
46
+ this.#notify()
47
+ }
35
48
 
36
49
  /**
37
- * Monotonic counter that increments on every notify.
50
+ * Transitions to the error state.
38
51
  */
39
- version = 0
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
- * The number of active subscribers for this entry.
63
+ * Notifies all subscribers of state changes.
64
+ *
65
+ * @remarks
66
+ * This should be called after any meaningful state change.
43
67
  */
44
- get subscriberCount() {
45
- return this.#subscribers.size
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
- * Notifies all subscribers and increments the version counter.
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 should be called after any meaningful state change.
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
- notify = (): void => {
68
- this.version++
69
- for (const sub of this.#subscribers) {
70
- try {
71
- sub()
72
- } catch {
73
- // Swallow to protect other listeners
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
- setStore = (store: Store<TSchema>): void => {
79
- this.store = store
80
- this.error = undefined
81
- this.promise = undefined
82
- this.notify()
120
+ return promise
83
121
  }
84
122
 
85
- setError = (error: unknown): void => {
86
- this.error = error
87
- this.promise = undefined
88
- this.notify()
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
- getOrCreate = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> => {
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 and notifies its subscribers.
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
- * Notifying subscribers prompts consumers to re-render and re-read as needed.
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
- * Ensures a store entry exists in the cache.
192
- *
193
- * @param storeId - The ID of the store
194
- * @returns The existing or newly created store entry
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
- throw error
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
- return entry.promise
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
- * Reads a store, returning it directly if loaded or a promise if loading.
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 to integrate with React Error Boundaries
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
- read = <TSchema extends LiveStoreSchema>(
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.ensureStoreEntry<TSchema>(optionsWithDefaults.storeId)
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
- // If a previous error exists, throw it
271
- if (entry.error !== undefined) throw entry.error
261
+ const storeOrPromise = entry.getOrLoad(optionsWithDefaults)
272
262
 
273
- // If a load is already in flight, return the existing promise
274
- if (entry.promise) return entry.promise
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 entry.promise
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
- return this.load(options).then(noop).catch(noop)
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.ensureStoreEntry<TSchema>(storeId)
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
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react'
2
- import type { StoreRegistry } from './StoreRegistry.js'
2
+ import type { StoreRegistry } from './StoreRegistry.ts'
3
3
 
4
4
  export const StoreRegistryContext = React.createContext<StoreRegistry | undefined>(undefined)
5
5
 
@@ -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 = storeRegistry.read(options)
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.js'
14
- import type * as LiveStoreReact from './mod.js'
15
- import { __resetUseRcResourceCache } from './useRcResource.js'
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
 
@@ -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.js'
14
- import { __resetUseRcResourceCache } from './useRcResource.js'
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.js'
5
+ import { __resetUseRcResourceCache, useRcResource } from './useRcResource.ts'
6
6
 
7
7
  describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (strictMode=%s)', ({ strictMode }) => {
8
8
  beforeEach(() => {