@livestore/react 0.4.0-dev.12 → 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 (55) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +3 -2
  3. package/dist/__tests__/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/fixture.js +3 -2
  5. package/dist/__tests__/fixture.js.map +1 -1
  6. package/dist/experimental/mod.d.ts +1 -0
  7. package/dist/experimental/mod.d.ts.map +1 -1
  8. package/dist/experimental/mod.js +1 -0
  9. package/dist/experimental/mod.js.map +1 -1
  10. package/dist/experimental/multi-store/StoreRegistry.d.ts +75 -0
  11. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -0
  12. package/dist/experimental/multi-store/StoreRegistry.js +286 -0
  13. package/dist/experimental/multi-store/StoreRegistry.js.map +1 -0
  14. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +10 -0
  15. package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +1 -0
  16. package/dist/experimental/multi-store/StoreRegistryContext.js +15 -0
  17. package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -0
  18. package/dist/experimental/multi-store/mod.d.ts +6 -0
  19. package/dist/experimental/multi-store/mod.d.ts.map +1 -0
  20. package/dist/experimental/multi-store/mod.js +6 -0
  21. package/dist/experimental/multi-store/mod.js.map +1 -0
  22. package/dist/experimental/multi-store/storeOptions.d.ts +4 -0
  23. package/dist/experimental/multi-store/storeOptions.d.ts.map +1 -0
  24. package/dist/experimental/multi-store/storeOptions.js +4 -0
  25. package/dist/experimental/multi-store/storeOptions.js.map +1 -0
  26. package/dist/experimental/multi-store/types.d.ts +44 -0
  27. package/dist/experimental/multi-store/types.d.ts.map +1 -0
  28. package/dist/experimental/multi-store/types.js +2 -0
  29. package/dist/experimental/multi-store/types.js.map +1 -0
  30. package/dist/experimental/multi-store/useStore.d.ts +11 -0
  31. package/dist/experimental/multi-store/useStore.d.ts.map +1 -0
  32. package/dist/experimental/multi-store/useStore.js +21 -0
  33. package/dist/experimental/multi-store/useStore.js.map +1 -0
  34. package/dist/useQuery.d.ts +28 -6
  35. package/dist/useQuery.d.ts.map +1 -1
  36. package/dist/useQuery.js +54 -7
  37. package/dist/useQuery.js.map +1 -1
  38. package/dist/useQuery.test.js +24 -0
  39. package/dist/useQuery.test.js.map +1 -1
  40. package/dist/useStore.d.ts +2 -1
  41. package/dist/useStore.d.ts.map +1 -1
  42. package/dist/useStore.js +1 -1
  43. package/dist/useStore.js.map +1 -1
  44. package/package.json +6 -6
  45. package/src/__tests__/fixture.tsx +7 -16
  46. package/src/experimental/mod.ts +1 -0
  47. package/src/experimental/multi-store/StoreRegistry.ts +356 -0
  48. package/src/experimental/multi-store/StoreRegistryContext.tsx +23 -0
  49. package/src/experimental/multi-store/mod.ts +5 -0
  50. package/src/experimental/multi-store/storeOptions.ts +8 -0
  51. package/src/experimental/multi-store/types.ts +55 -0
  52. package/src/experimental/multi-store/useStore.ts +39 -0
  53. package/src/useQuery.test.tsx +45 -0
  54. package/src/useQuery.ts +84 -16
  55. package/src/useStore.ts +4 -3
package/src/useQuery.ts CHANGED
@@ -1,5 +1,13 @@
1
+ import { isQueryBuilder } from '@livestore/common'
1
2
  import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
2
- import { extractStackInfoFromStackTrace, stackInfoToString } from '@livestore/livestore'
3
+ import {
4
+ extractStackInfoFromStackTrace,
5
+ isQueryable,
6
+ type Queryable,
7
+ queryDb,
8
+ type SignalDef,
9
+ stackInfoToString,
10
+ } from '@livestore/livestore'
3
11
  import type { LiveQueries } from '@livestore/livestore/internal'
4
12
  import { deepEqual, indent, shouldNeverHappen } from '@livestore/utils'
5
13
  import * as otel from '@opentelemetry/api'
@@ -21,15 +29,36 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
21
29
  * }
22
30
  * ```
23
31
  */
24
- export const useQuery = <TQuery extends LiveQueryDef.Any>(
25
- queryDef: TQuery,
32
+ export const useQuery = <TQueryable extends Queryable<any>>(
33
+ queryable: TQueryable,
26
34
  options?: { store?: Store },
27
- ): LiveQueries.GetResult<TQuery> => useQueryRef(queryDef, options).valueRef.current
35
+ ): Queryable.Result<TQueryable> => useQueryRef(queryable, options).valueRef.current
28
36
 
29
37
  /**
38
+ * Like `useQuery`, but also returns a reference to the underlying LiveQuery instance.
39
+ *
40
+ * Usage
41
+ * - Accepts any `Queryable<TResult>`: a `LiveQueryDef`, `SignalDef`, a `LiveQuery` instance
42
+ * or a SQL `QueryBuilder`. Unions of queryables are supported and the result type is
43
+ * inferred via `Queryable.Result<TQueryable>`.
44
+ * - Creates an OpenTelemetry span per unique query, reusing it while the ref-counted
45
+ * resource is alive. The span name is updated once the dynamic label is known.
46
+ * - Manages a reference-counted resource under-the-hood so query instances are shared
47
+ * across re-renders and properly disposed once no longer referenced.
48
+ *
49
+ * Parameters
50
+ * - `queryable`: The query definition/instance/builder to run and subscribe to.
51
+ * - `options.store`: Optional store to use; by default the store from `LiveStoreContext` is used.
52
+ * - `options.otelContext`: Optional parent otel context for the query span.
53
+ * - `options.otelSpanName`: Optional explicit span name; otherwise derived from the query label.
54
+ *
55
+ * Returns
56
+ * - `valueRef`: A React ref whose `current` holds the latest query result. The type is
57
+ * `Queryable.Result<TQueryable>` with full inference for unions.
58
+ * - `queryRcRef`: The underlying reference-counted `LiveQuery` instance used by the store.
30
59
  */
31
- export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
32
- queryDef: TQuery,
60
+ export const useQueryRef = <TQueryable extends Queryable<any>>(
61
+ queryable: TQueryable,
33
62
  options?: {
34
63
  store?: Store
35
64
  /** Parent otel context for the query */
@@ -38,16 +67,50 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
38
67
  otelSpanName?: string
39
68
  },
40
69
  ): {
41
- valueRef: React.RefObject<LiveQueries.GetResult<TQuery>>
42
- queryRcRef: LiveQueries.RcRef<LiveQuery<LiveQueries.GetResult<TQuery>>>
70
+ valueRef: React.RefObject<Queryable.Result<TQueryable>>
71
+ queryRcRef: LiveQueries.RcRef<LiveQuery<Queryable.Result<TQueryable>>>
43
72
  } => {
44
73
  const store =
45
74
  options?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
46
75
  React.useContext(LiveStoreContext)?.store ??
47
76
  shouldNeverHappen(`No store provided to useQuery`)
48
77
 
78
+ type TResult = Queryable.Result<TQueryable>
79
+ type NormalizedQueryable =
80
+ | { _tag: 'definition'; def: LiveQueryDef<TResult> | SignalDef<TResult> }
81
+ | { _tag: 'live-query'; query$: LiveQuery<TResult> }
82
+
83
+ const normalized = React.useMemo<NormalizedQueryable>(() => {
84
+ if (!isQueryable(queryable)) {
85
+ return shouldNeverHappen('useQuery expected a Queryable value')
86
+ }
87
+
88
+ if (isQueryBuilder(queryable)) {
89
+ return { _tag: 'definition', def: queryDb(queryable) }
90
+ }
91
+
92
+ if (
93
+ (queryable as LiveQueryDef<TResult> | SignalDef<TResult>)._tag === 'def' ||
94
+ (queryable as LiveQueryDef<TResult> | SignalDef<TResult>)._tag === 'signal-def'
95
+ ) {
96
+ return { _tag: 'definition', def: queryable as LiveQueryDef<TResult> | SignalDef<TResult> }
97
+ }
98
+
99
+ return { _tag: 'live-query', query$: queryable as LiveQuery<TResult> }
100
+ }, [queryable])
101
+
49
102
  // It's important to use all "aspects" of a store instance here, otherwise we get unexpected cache mappings
50
- const rcRefKey = `${store.storeId}_${store.clientId}_${store.sessionId}_${queryDef.hash}`
103
+ const rcRefKey = React.useMemo(() => {
104
+ const base = `${store.storeId}_${store.clientId}_${store.sessionId}`
105
+
106
+ if (normalized._tag === 'definition') {
107
+ return `${base}:def:${normalized.def.hash}`
108
+ }
109
+
110
+ return `${base}:instance:${normalized.query$.id}`
111
+ }, [normalized, store.clientId, store.sessionId, store.storeId])
112
+
113
+ const resourceLabel = normalized._tag === 'definition' ? normalized.def.label : normalized.query$.label
51
114
 
52
115
  const stackInfo = React.useMemo(() => {
53
116
  Error.stackTraceLimit = 10
@@ -59,17 +122,22 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
59
122
  const { queryRcRef, span, otelContext } = useRcResource(
60
123
  rcRefKey,
61
124
  () => {
62
- const queryDefLabel = queryDef.label
63
-
64
125
  const span = store.otel.tracer.startSpan(
65
- options?.otelSpanName ?? `LiveStore:useQuery:${queryDefLabel}`,
66
- { attributes: { label: queryDefLabel, firstStackInfo: JSON.stringify(stackInfo) } },
126
+ options?.otelSpanName ?? `LiveStore:useQuery:${resourceLabel}`,
127
+ { attributes: { label: resourceLabel, firstStackInfo: JSON.stringify(stackInfo) } },
67
128
  options?.otelContext ?? store.otel.queriesSpanContext,
68
129
  )
69
130
 
70
131
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
71
132
 
72
- const queryRcRef = queryDef.make(store.reactivityGraph.context!, otelContext)
133
+ const queryRcRef =
134
+ normalized._tag === 'definition'
135
+ ? normalized.def.make(store.reactivityGraph.context!, otelContext)
136
+ : ({
137
+ value: normalized.query$,
138
+ deref: () => {},
139
+ rc: Number.POSITIVE_INFINITY,
140
+ } satisfies LiveQueries.RcRef<LiveQuery<TResult>>)
73
141
 
74
142
  return { queryRcRef, span, otelContext }
75
143
  },
@@ -82,7 +150,7 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
82
150
  // const queryRcRef.value.get()
83
151
  // }
84
152
 
85
- const query$ = queryRcRef.value as LiveQuery<LiveQueries.GetResult<TQuery>>
153
+ const query$ = queryRcRef.value as LiveQuery<TResult>
86
154
 
87
155
  React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
88
156
  // console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
@@ -118,7 +186,7 @@ Stack trace:
118
186
  }, [otelContext, query$, stackInfo])
119
187
 
120
188
  // We know the query has a result by the time we use it; so we can synchronously populate a default state
121
- const [valueRef, setValue] = useStateRefWithReactiveInput<LiveQueries.GetResult<TQuery>>(initialResult)
189
+ const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
122
190
 
123
191
  // TODO we probably need to change the order of `useEffect` calls, so we destroy the query at the end
124
192
  // before calling the LS `onEffect` on it
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
- store.useQuery = (queryDef) => useQuery(queryDef, { store })
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 } => {