@livestore/react 0.4.0-dev.13 → 0.4.0-dev.14

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 (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.js +1 -1
  3. package/dist/__tests__/fixture.js.map +1 -1
  4. package/dist/experimental/mod.d.ts +1 -0
  5. package/dist/experimental/mod.d.ts.map +1 -1
  6. package/dist/experimental/mod.js +1 -0
  7. package/dist/experimental/mod.js.map +1 -1
  8. package/dist/experimental/multi-store/StoreRegistry.d.ts +75 -0
  9. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -0
  10. package/dist/experimental/multi-store/StoreRegistry.js +286 -0
  11. package/dist/experimental/multi-store/StoreRegistry.js.map +1 -0
  12. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +10 -0
  13. package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +1 -0
  14. package/dist/experimental/multi-store/StoreRegistryContext.js +15 -0
  15. package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -0
  16. package/dist/experimental/multi-store/mod.d.ts +6 -0
  17. package/dist/experimental/multi-store/mod.d.ts.map +1 -0
  18. package/dist/experimental/multi-store/mod.js +6 -0
  19. package/dist/experimental/multi-store/mod.js.map +1 -0
  20. package/dist/experimental/multi-store/storeOptions.d.ts +4 -0
  21. package/dist/experimental/multi-store/storeOptions.d.ts.map +1 -0
  22. package/dist/experimental/multi-store/storeOptions.js +4 -0
  23. package/dist/experimental/multi-store/storeOptions.js.map +1 -0
  24. package/dist/experimental/multi-store/types.d.ts +44 -0
  25. package/dist/experimental/multi-store/types.d.ts.map +1 -0
  26. package/dist/experimental/multi-store/types.js +2 -0
  27. package/dist/experimental/multi-store/types.js.map +1 -0
  28. package/dist/experimental/multi-store/useStore.d.ts +11 -0
  29. package/dist/experimental/multi-store/useStore.d.ts.map +1 -0
  30. package/dist/experimental/multi-store/useStore.js +21 -0
  31. package/dist/experimental/multi-store/useStore.js.map +1 -0
  32. package/dist/useStore.d.ts +2 -1
  33. package/dist/useStore.d.ts.map +1 -1
  34. package/dist/useStore.js.map +1 -1
  35. package/package.json +6 -6
  36. package/src/__tests__/fixture.tsx +1 -1
  37. package/src/experimental/mod.ts +1 -0
  38. package/src/experimental/multi-store/StoreRegistry.ts +356 -0
  39. package/src/experimental/multi-store/StoreRegistryContext.tsx +23 -0
  40. package/src/experimental/multi-store/mod.ts +5 -0
  41. package/src/experimental/multi-store/storeOptions.ts +8 -0
  42. package/src/experimental/multi-store/types.ts +55 -0
  43. package/src/experimental/multi-store/useStore.ts +39 -0
  44. package/src/useStore.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/react",
3
- "version": "0.4.0-dev.13",
3
+ "version": "0.4.0-dev.14",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -9,9 +9,9 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@opentelemetry/api": "1.9.0",
12
- "@livestore/common": "0.4.0-dev.13",
13
- "@livestore/utils": "0.4.0-dev.13",
14
- "@livestore/livestore": "0.4.0-dev.13"
12
+ "@livestore/common": "0.4.0-dev.14",
13
+ "@livestore/utils": "0.4.0-dev.14",
14
+ "@livestore/livestore": "0.4.0-dev.14"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@opentelemetry/sdk-trace-base": "^2.0.1",
@@ -26,8 +26,8 @@
26
26
  "typescript": "5.9.2",
27
27
  "vite": "7.1.7",
28
28
  "vitest": "3.2.4",
29
- "@livestore/adapter-web": "0.4.0-dev.13",
30
- "@livestore/utils-dev": "0.4.0-dev.13"
29
+ "@livestore/adapter-web": "0.4.0-dev.14",
30
+ "@livestore/utils-dev": "0.4.0-dev.14"
31
31
  },
32
32
  "files": [
33
33
  "package.json",
@@ -111,7 +111,7 @@ export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect
111
111
  Scope.Scope
112
112
  > = (opts: MakeTodoMvcReactOptions = {}) =>
113
113
  Effect.gen(function* () {
114
- const { otelTracer, otelContext, strictMode } = opts
114
+ const { strictMode } = opts
115
115
  const makeRenderCount = () => {
116
116
  let val = 0
117
117
 
@@ -1 +1,2 @@
1
1
  export { LiveList, type LiveListProps } from './components/LiveList.tsx'
2
+ export * from './multi-store/mod.ts'
@@ -0,0 +1,356 @@
1
+ import type { LiveStoreSchema } from '@livestore/common/schema'
2
+ import { createStorePromise, type Store, type Unsubscribe } from '@livestore/livestore'
3
+ import { noop } from '@livestore/utils'
4
+ import type { CachedStoreOptions, StoreId } from './types.ts'
5
+
6
+ /**
7
+ * Minimal cache entry that tracks store, error, and in-flight promise along with subscribers.
8
+ *
9
+ * @typeParam TSchema - The schema for this entry's store.
10
+ * @internal
11
+ */
12
+ class StoreEntry<TSchema extends LiveStoreSchema = LiveStoreSchema> {
13
+ /**
14
+ * The resolved store.
15
+ *
16
+ * @remarks
17
+ * A value of `undefined` indicates "not loaded yet".
18
+ */
19
+ store: Store<TSchema> | undefined = undefined
20
+
21
+ /**
22
+ * The most recent error encountered for this entry, if any.
23
+ */
24
+ error: unknown = undefined
25
+
26
+ /**
27
+ * The in-flight promise for loading the store, or `undefined` if not yet loading or already resolved.
28
+ */
29
+ promise: Promise<Store<TSchema>> | undefined = undefined
30
+
31
+ /**
32
+ * Set of subscriber callbacks to notify on state changes.
33
+ */
34
+ #subscribers = new Set<() => void>()
35
+
36
+ /**
37
+ * Monotonic counter that increments on every notify.
38
+ */
39
+ version = 0
40
+
41
+ /**
42
+ * The number of active subscribers for this entry.
43
+ */
44
+ get subscriberCount() {
45
+ return this.#subscribers.size
46
+ }
47
+
48
+ /**
49
+ * Subscribes to this entry's updates.
50
+ *
51
+ * @param listener - Callback invoked when the entry changes
52
+ * @returns Unsubscribe function
53
+ */
54
+ subscribe = (listener: () => void): Unsubscribe => {
55
+ this.#subscribers.add(listener)
56
+ return () => {
57
+ this.#subscribers.delete(listener)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Notifies all subscribers and increments the version counter.
63
+ *
64
+ * @remarks
65
+ * This should be called after any meaningful state change.
66
+ */
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
+ }
77
+
78
+ setStore = (store: Store<TSchema>): void => {
79
+ this.store = store
80
+ this.error = undefined
81
+ this.promise = undefined
82
+ this.notify()
83
+ }
84
+
85
+ setError = (error: unknown): void => {
86
+ this.error = error
87
+ this.promise = undefined
88
+ this.notify()
89
+ }
90
+ }
91
+
92
+ /**
93
+ * In-memory map of {@link StoreEntry} instances keyed by {@link StoreId}.
94
+ *
95
+ * @privateRemarks
96
+ * The cache is intentionally small; eviction and GC timers are coordinated by the client.
97
+ *
98
+ * @internal
99
+ */
100
+ class StoreCache {
101
+ readonly #entries = new Map<StoreId, StoreEntry>()
102
+
103
+ get = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> | undefined => {
104
+ return this.#entries.get(storeId) as StoreEntry<TSchema> | undefined
105
+ }
106
+
107
+ getOrCreate = <TSchema extends LiveStoreSchema>(storeId: StoreId): StoreEntry<TSchema> => {
108
+ let entry = this.#entries.get(storeId) as StoreEntry<TSchema> | undefined
109
+
110
+ if (!entry) {
111
+ entry = new StoreEntry<TSchema>()
112
+ this.#entries.set(storeId, entry as unknown as StoreEntry)
113
+ }
114
+
115
+ return entry
116
+ }
117
+
118
+ /**
119
+ * Removes an entry from the cache and notifies its subscribers.
120
+ *
121
+ * @param storeId - The ID of the store to remove
122
+ * @remarks
123
+ * Notifying subscribers prompts consumers to re-render and re-read as needed.
124
+ */
125
+ remove = (storeId: StoreId): void => {
126
+ const entry = this.#entries.get(storeId)
127
+ if (!entry) return
128
+ 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
+ }
143
+ }
144
+
145
+ const DEFAULT_GC_TIME = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : 60_000
146
+
147
+ type DefaultStoreOptions = Partial<
148
+ Pick<
149
+ CachedStoreOptions<any>,
150
+ 'batchUpdates' | 'disableDevtools' | 'confirmUnsavedChanges' | 'syncPayload' | 'debug' | 'otelOptions'
151
+ >
152
+ > & {
153
+ /**
154
+ * The time in milliseconds that inactive stores remain in memory.
155
+ * When a store becomes inactive, it will be garbage collected
156
+ * after this duration.
157
+ *
158
+ * Stores transition to the inactive state as soon as they have no
159
+ * subscriptions registered, so when all components which use that
160
+ * store have unmounted.
161
+ *
162
+ * @remarks
163
+ * - If set to `infinity`, will disable garbage collection
164
+ * - The maximum allowed time is about {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value | 24 days}
165
+ *
166
+ * @defaultValue `60_000` (60 seconds) or `Infinity` during SSR to avoid
167
+ * disposing stores before server render completes.
168
+ */
169
+ gcTime?: number
170
+ }
171
+
172
+ type StoreRegistryConfig = {
173
+ defaultOptions?: DefaultStoreOptions
174
+ }
175
+
176
+ /**
177
+ * Store Registry coordinating cache, GC, and Suspense reads.
178
+ *
179
+ * @public
180
+ */
181
+ export class StoreRegistry {
182
+ readonly #cache = new StoreCache()
183
+ readonly #gcTimeouts = new Map<StoreId, ReturnType<typeof setTimeout>>()
184
+ readonly #defaultOptions: DefaultStoreOptions
185
+
186
+ constructor({ defaultOptions = {} }: StoreRegistryConfig = {}) {
187
+ this.#defaultOptions = defaultOptions
188
+ }
189
+
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)
240
+
241
+ throw error
242
+ })
243
+
244
+ return entry.promise
245
+ }
246
+
247
+ /**
248
+ * Reads a store, returning it directly if loaded or a promise if loading.
249
+ * Designed to work with React.use() for Suspense integration.
250
+ *
251
+ * @typeParam TSchema - The schema of the store to load
252
+ * @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
254
+ *
255
+ * @remarks
256
+ * - When the store is already loaded, returns the store instance directly (not wrapped in a Promise)
257
+ * - When loading, returns a stable Promise reference that can be used with React.use()
258
+ * - 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
+ */
261
+ read = <TSchema extends LiveStoreSchema>(
262
+ options: CachedStoreOptions<TSchema>,
263
+ ): Store<TSchema> | Promise<Store<TSchema>> => {
264
+ 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
269
+
270
+ // If a previous error exists, throw it
271
+ if (entry.error !== undefined) throw entry.error
272
+
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.
282
+ 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
+ })
294
+
295
+ return entry.promise
296
+ }
297
+
298
+ /**
299
+ * Warms the cache for a store without mounting a subscriber.
300
+ *
301
+ * @typeParam TSchema - The schema of the store to preload
302
+ * @returns A promise that resolves when the loading is complete (success or failure)
303
+ *
304
+ * @remarks
305
+ * - We don't return the store or throw as this is a fire-and-forget operation.
306
+ * - If the entry remains unused after preload resolves/rejects, it is scheduled for GC.
307
+ */
308
+ preload = async <TSchema extends LiveStoreSchema>(options: CachedStoreOptions<TSchema>): Promise<void> => {
309
+ return this.load(options).then(noop).catch(noop)
310
+ }
311
+
312
+ subscribe = <TSchema extends LiveStoreSchema>(storeId: StoreId, listener: () => void): Unsubscribe => {
313
+ const entry = this.ensureStoreEntry<TSchema>(storeId)
314
+ // Active subscriber: cancel any scheduled GC
315
+ this.#cancelGC(storeId)
316
+
317
+ const unsubscribe = entry.subscribe(listener)
318
+
319
+ return () => {
320
+ unsubscribe()
321
+ // 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)
354
+ }
355
+ }
356
+ }
@@ -0,0 +1,23 @@
1
+ import * as React from 'react'
2
+ import type { StoreRegistry } from './StoreRegistry.js'
3
+
4
+ export const StoreRegistryContext = React.createContext<StoreRegistry | undefined>(undefined)
5
+
6
+ export type StoreRegistryProviderProps = {
7
+ storeRegistry: StoreRegistry
8
+ children: React.ReactNode
9
+ }
10
+
11
+ export const StoreRegistryProvider = ({ storeRegistry, children }: StoreRegistryProviderProps): React.JSX.Element => {
12
+ return <StoreRegistryContext value={storeRegistry}>{children}</StoreRegistryContext>
13
+ }
14
+
15
+ export const useStoreRegistry = (override?: StoreRegistry) => {
16
+ if (override) return override
17
+
18
+ const storeRegistry = React.use(StoreRegistryContext)
19
+
20
+ if (!storeRegistry) throw new Error('useStoreRegistry() must be used within <MultiStoreProvider>')
21
+
22
+ return storeRegistry
23
+ }
@@ -0,0 +1,5 @@
1
+ export * from './StoreRegistry.ts'
2
+ export * from './StoreRegistryContext.tsx'
3
+ export * from './storeOptions.ts'
4
+ export * from './types.ts'
5
+ export * from './useStore.ts'
@@ -0,0 +1,8 @@
1
+ import type { LiveStoreSchema } from '@livestore/common/schema'
2
+ import type { CachedStoreOptions } from './types.ts'
3
+
4
+ export function storeOptions<TSchema extends LiveStoreSchema>(
5
+ options: CachedStoreOptions<TSchema>,
6
+ ): CachedStoreOptions<TSchema> {
7
+ return options
8
+ }
@@ -0,0 +1,55 @@
1
+ import type { Adapter } from '@livestore/common'
2
+ import type { LiveStoreSchema } from '@livestore/common/schema'
3
+ import type { CreateStoreOptions, OtelOptions } from '@livestore/livestore'
4
+
5
+ export type StoreId = string
6
+
7
+ /**
8
+ * Minimum information required to create a store
9
+ */
10
+ export type StoreDescriptor<TSchema extends LiveStoreSchema> = {
11
+ /**
12
+ * Schema describing the data structure.
13
+ */
14
+ readonly schema: TSchema
15
+
16
+ /**
17
+ * Adapter for persistence and synchronization.
18
+ */
19
+ readonly adapter: Adapter
20
+
21
+ /**
22
+ * The ID of the store.
23
+ */
24
+ readonly storeId: StoreId
25
+ }
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 inactive. When this store becomes
40
+ * inactive, it will be garbage collected after this duration.
41
+ *
42
+ * Stores transition to the inactive 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 `gcTime` config are used for the same store, the longest one will be used.
48
+ * - If set to `Infinity`, will disable garbage collection
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
+ gcTime?: number
55
+ }
@@ -0,0 +1,39 @@
1
+ import type { LiveStoreSchema } from '@livestore/common/schema'
2
+ import type { Store } from '@livestore/livestore'
3
+ import * as React from 'react'
4
+ import type { ReactApi } from '../../LiveStoreContext.ts'
5
+ import { withReactApi } from '../../useStore.ts'
6
+ import { useStoreRegistry } from './StoreRegistryContext.tsx'
7
+ import type { CachedStoreOptions } from './types.ts'
8
+
9
+ /**
10
+ * Suspense + Error Boundary friendly hook.
11
+ * - Returns data or throws (Promise|Error).
12
+ * - No loading or error states are returned.
13
+ */
14
+ export const useStore = <TSchema extends LiveStoreSchema>(
15
+ options: CachedStoreOptions<TSchema>,
16
+ ): Store<TSchema> & ReactApi => {
17
+ const storeRegistry = useStoreRegistry()
18
+
19
+ storeRegistry.ensureStoreEntry(options.storeId)
20
+
21
+ const subscribe = React.useCallback(
22
+ (onChange: () => void) => storeRegistry.subscribe(options.storeId, onChange),
23
+ [storeRegistry, options.storeId],
24
+ )
25
+ const getSnapshot = React.useCallback(
26
+ () => storeRegistry.getVersion(options.storeId),
27
+ [storeRegistry, options.storeId],
28
+ )
29
+
30
+ React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
31
+
32
+ const storeOrPromise = storeRegistry.read(options)
33
+
34
+ // If read() returns a Promise, use React.use() to suspend
35
+ // If it returns a Store directly, use it immediately
36
+ const loadedStore = storeOrPromise instanceof Promise ? React.use(storeOrPromise) : storeOrPromise
37
+
38
+ return withReactApi(loadedStore)
39
+ }
package/src/useStore.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { LiveStoreSchema } from '@livestore/common/schema'
1
2
  import type { Store } from '@livestore/livestore'
2
3
  import React from 'react'
3
4
 
@@ -6,14 +7,14 @@ import { LiveStoreContext } from './LiveStoreContext.ts'
6
7
  import { useClientDocument } from './useClientDocument.ts'
7
8
  import { useQuery } from './useQuery.ts'
8
9
 
9
- export const withReactApi = (store: Store): Store & ReactApi => {
10
+ export const withReactApi = <TSchema extends LiveStoreSchema>(store: Store<TSchema>): Store<TSchema> & ReactApi => {
10
11
  // @ts-expect-error TODO properly implement this
11
12
 
12
13
  store.useQuery = (queryable) => useQuery(queryable, { store })
13
14
  // @ts-expect-error TODO properly implement this
14
15
 
15
16
  store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store })
16
- return store as Store & ReactApi
17
+ return store as Store<TSchema> & ReactApi
17
18
  }
18
19
 
19
20
  export const useStore = (options?: { store?: Store }): { store: Store & ReactApi } => {