@livestore/livestore 0.4.0-dev.9 → 0.5.0-dev.0

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 (132) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +15 -13
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +24 -6
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +134 -6
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +190 -11
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +41 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +129 -9
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +30 -4
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +4 -3
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts +56 -0
  32. package/dist/live-queries/computed.d.ts.map +1 -1
  33. package/dist/live-queries/computed.js +62 -6
  34. package/dist/live-queries/computed.js.map +1 -1
  35. package/dist/live-queries/db-query.d.ts +2 -2
  36. package/dist/live-queries/db-query.d.ts.map +1 -1
  37. package/dist/live-queries/db-query.js +41 -30
  38. package/dist/live-queries/db-query.js.map +1 -1
  39. package/dist/live-queries/db-query.test.js +112 -29
  40. package/dist/live-queries/db-query.test.js.map +1 -1
  41. package/dist/live-queries/signal.d.ts +49 -0
  42. package/dist/live-queries/signal.d.ts.map +1 -1
  43. package/dist/live-queries/signal.js +51 -2
  44. package/dist/live-queries/signal.js.map +1 -1
  45. package/dist/live-queries/signal.test.js +4 -4
  46. package/dist/live-queries/signal.test.js.map +1 -1
  47. package/dist/mod.d.ts +3 -3
  48. package/dist/mod.d.ts.map +1 -1
  49. package/dist/mod.js +3 -2
  50. package/dist/mod.js.map +1 -1
  51. package/dist/reactive.d.ts +10 -10
  52. package/dist/reactive.d.ts.map +1 -1
  53. package/dist/reactive.js +29 -27
  54. package/dist/reactive.js.map +1 -1
  55. package/dist/reactive.test.js +2 -2
  56. package/dist/reactive.test.js.map +1 -1
  57. package/dist/store/StoreRegistry.d.ts +215 -0
  58. package/dist/store/StoreRegistry.d.ts.map +1 -0
  59. package/dist/store/StoreRegistry.js +267 -0
  60. package/dist/store/StoreRegistry.js.map +1 -0
  61. package/dist/store/StoreRegistry.test.d.ts +2 -0
  62. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  63. package/dist/store/StoreRegistry.test.js +384 -0
  64. package/dist/store/StoreRegistry.test.js.map +1 -0
  65. package/dist/store/create-store.d.ts +99 -22
  66. package/dist/store/create-store.d.ts.map +1 -1
  67. package/dist/store/create-store.js +85 -37
  68. package/dist/store/create-store.js.map +1 -1
  69. package/dist/store/devtools.d.ts +6 -18
  70. package/dist/store/devtools.d.ts.map +1 -1
  71. package/dist/store/devtools.js +61 -19
  72. package/dist/store/devtools.js.map +1 -1
  73. package/dist/store/store-eventstream.test.d.ts +2 -0
  74. package/dist/store/store-eventstream.test.d.ts.map +1 -0
  75. package/dist/store/store-eventstream.test.js +65 -0
  76. package/dist/store/store-eventstream.test.js.map +1 -0
  77. package/dist/store/store-types.d.ts +271 -31
  78. package/dist/store/store-types.d.ts.map +1 -1
  79. package/dist/store/store-types.js +41 -1
  80. package/dist/store/store-types.js.map +1 -1
  81. package/dist/store/store-types.test.d.ts +2 -0
  82. package/dist/store/store-types.test.d.ts.map +1 -0
  83. package/dist/store/store-types.test.js +39 -0
  84. package/dist/store/store-types.test.js.map +1 -0
  85. package/dist/store/store.d.ts +255 -68
  86. package/dist/store/store.d.ts.map +1 -1
  87. package/dist/store/store.js +533 -186
  88. package/dist/store/store.js.map +1 -1
  89. package/dist/utils/dev.d.ts.map +1 -1
  90. package/dist/utils/dev.js +1 -1
  91. package/dist/utils/dev.js.map +1 -1
  92. package/dist/utils/stack-info.d.ts.map +1 -1
  93. package/dist/utils/stack-info.js +3 -2
  94. package/dist/utils/stack-info.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +46 -39
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -0
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/otel.d.ts.map +1 -1
  100. package/dist/utils/tests/otel.js +5 -5
  101. package/dist/utils/tests/otel.js.map +1 -1
  102. package/package.json +50 -18
  103. package/src/QueryCache.ts +1 -1
  104. package/src/SqliteDbWrapper.test.ts +30 -6
  105. package/src/SqliteDbWrapper.ts +21 -17
  106. package/src/ambient.d.ts +0 -7
  107. package/src/effect/LiveStore.test.ts +61 -0
  108. package/src/effect/LiveStore.ts +426 -22
  109. package/src/effect/mod.ts +13 -1
  110. package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
  111. package/src/live-queries/base-class.ts +162 -32
  112. package/src/live-queries/client-document-get-query.ts +6 -4
  113. package/src/live-queries/computed.ts +65 -8
  114. package/src/live-queries/db-query.test.ts +168 -30
  115. package/src/live-queries/db-query.ts +58 -43
  116. package/src/live-queries/signal.test.ts +5 -4
  117. package/src/live-queries/signal.ts +52 -3
  118. package/src/mod.ts +19 -2
  119. package/src/reactive.test.ts +3 -2
  120. package/src/reactive.ts +58 -49
  121. package/src/store/StoreRegistry.test.ts +543 -0
  122. package/src/store/StoreRegistry.ts +429 -0
  123. package/src/store/create-store.ts +227 -74
  124. package/src/store/devtools.ts +305 -261
  125. package/src/store/store-eventstream.test.ts +123 -0
  126. package/src/store/store-types.test.ts +52 -0
  127. package/src/store/store-types.ts +318 -44
  128. package/src/store/store.ts +695 -283
  129. package/src/utils/dev.ts +3 -4
  130. package/src/utils/stack-info.ts +5 -2
  131. package/src/utils/tests/fixture.ts +9 -1
  132. package/src/utils/tests/otel.ts +8 -7
@@ -1,10 +1,10 @@
1
- import { isNotNil } from '@livestore/utils'
2
- import { Equal, Hash, Predicate } from '@livestore/utils/effect'
3
1
  import type * as otel from '@opentelemetry/api'
4
2
 
3
+ import { Equal, Hash, Predicate } from '@livestore/utils/effect'
4
+
5
5
  import * as RG from '../reactive.ts'
6
- import type { Store } from '../store/store.ts'
7
6
  import type { QueryDebugInfo, RefreshReason } from '../store/store-types.ts'
7
+ import type { Store } from '../store/store.ts'
8
8
  import type { StackInfo } from '../utils/stack-info.ts'
9
9
 
10
10
  export type ReactivityGraph = RG.ReactiveGraph<RefreshReason, QueryDebugInfo, ReactivityGraphContext>
@@ -23,63 +23,129 @@ export type ReactivityGraphContext = {
23
23
  effectsWrapper: (run: () => void) => void
24
24
  }
25
25
 
26
- export type GetResult<TQuery extends LiveQueryDef.Any | LiveQuery.Any | SignalDef<any>> = TQuery extends LiveQuery<
27
- infer TResult
28
- >
29
- ? TResult
30
- : TQuery extends LiveQueryDef<infer TResult>
26
+ export type GetResult<TQuery extends LiveQueryDef.Any | LiveQuery.Any | SignalDef<any>> =
27
+ TQuery extends LiveQuery<infer TResult>
31
28
  ? TResult
32
- : TQuery extends SignalDef<infer TResult>
29
+ : TQuery extends LiveQueryDef<infer TResult>
33
30
  ? TResult
34
- : unknown
31
+ : TQuery extends SignalDef<infer TResult>
32
+ ? TResult
33
+ : unknown
35
34
 
36
35
  let queryIdCounter = 0
37
36
 
37
+ /**
38
+ * A signal definition representing ephemeral, local-only reactive state.
39
+ *
40
+ * `SignalDef` is the type returned by {@link signal}. It's a blueprint for creating
41
+ * signal instances—the actual instance is created when you use the definition with
42
+ * a Store via `store.query()` or `store.setSignal()`.
43
+ *
44
+ * @typeParam T - The type of value the signal holds
45
+ */
38
46
  export interface SignalDef<T> extends LiveQueryDef<T, 'signal-def'> {
39
47
  _tag: 'signal-def'
48
+ /** The initial value used when the signal is first created */
40
49
  defaultValue: T
50
+ /** Unique identifier for caching and deduplication */
41
51
  hash: string
52
+ /** Human-readable label for debugging and devtools */
42
53
  label: string
54
+ /** Creates a reference-counted signal instance bound to a Store's reactivity graph */
43
55
  make: (ctx: ReactivityGraphContext) => RcRef<ISignal<T>>
44
- [Equal.symbol](that: SignalDef<T>): boolean
56
+ [Equal.symbol](that: Equal.Equal): boolean
45
57
  [Hash.symbol](): number
46
58
  }
47
59
 
60
+ /**
61
+ * Interface for a live signal instance.
62
+ *
63
+ * This represents an active signal bound to a Store's reactivity graph.
64
+ * Use `store.setSignal()` to update values and `store.query()` to read them.
65
+ *
66
+ * @typeParam T - The type of value the signal holds
67
+ */
48
68
  export interface ISignal<T> extends LiveQuery<T> {
49
69
  _tag: 'signal'
50
70
  reactivityGraph: ReactivityGraph
71
+ /** The underlying reactive reference in the graph */
51
72
  ref: RG.Ref<T, ReactivityGraphContext, RefreshReason>
73
+ /** Sets the signal's value (prefer using `store.setSignal()` instead) */
52
74
  set: (value: T) => void
75
+ /** Gets the signal's current value (prefer using `store.query()` instead) */
53
76
  get: () => T
77
+ /** Removes the signal from the reactivity graph */
54
78
  destroy: () => void
55
79
  }
56
80
 
57
81
  export const TypeId = Symbol.for('LiveQuery')
58
82
  export type TypeId = typeof TypeId
59
83
 
84
+ /**
85
+ * A reference-counted wrapper around a LiveQuery or Signal instance.
86
+ *
87
+ * LiveStore uses reference counting to manage query lifecycle. When multiple
88
+ * components or subscriptions use the same query definition, they share a single
89
+ * instance. The instance is destroyed when the last reference is released.
90
+ *
91
+ * You typically don't interact with `RcRef` directly—it's used internally by
92
+ * hooks like `useQuery` and `useQueryRef`.
93
+ */
60
94
  export interface RcRef<T> {
95
+ /** Current reference count */
61
96
  rc: number
97
+ /** The wrapped query or signal instance */
62
98
  value: T
99
+ /** Decrements the reference count; destroys the instance when it reaches zero */
63
100
  deref: () => void
64
101
  }
65
102
 
103
+ /**
104
+ * Dependency key used to identify queries on platforms where `fn.toString()` isn't reliable.
105
+ *
106
+ * On Expo/React Native, Hermes compiles functions to bytecode, so `fn.toString()` returns
107
+ * `[native code]`. To uniquely identify contextual queries, you must provide explicit `deps`.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * // On Expo, this would fail without deps:
112
+ * const filtered$ = queryDb(
113
+ * (get) => tables.todos.where({ userId: get(userId$) }),
114
+ * { deps: [userId] } // Required on Expo/React Native
115
+ * )
116
+ * ```
117
+ */
66
118
  export type DepKey = string | number | ReadonlyArray<string | number | undefined | null>
67
119
 
68
120
  export const depsToString = (deps: DepKey): string => {
69
121
  if (typeof deps === 'string' || typeof deps === 'number') {
70
122
  return deps.toString()
71
123
  }
72
- return deps.filter(isNotNil).join(',')
124
+ return deps.filter(Predicate.isNotNullish).join(',')
73
125
  }
74
126
 
127
+ /**
128
+ * A query definition representing a blueprint for a reactive query.
129
+ *
130
+ * Query definitions are created by {@link queryDb}, {@link computed}, and {@link signal}.
131
+ * They're lightweight and can be defined at module scope. The actual query instance
132
+ * (which holds state) is created lazily when you use the definition with a Store.
133
+ *
134
+ * Multiple uses of the same definition share a single instance via reference counting.
135
+ *
136
+ * @typeParam TResult - The type of value the query returns
137
+ * @typeParam TTag - Internal discriminator tag ('def' for queries, 'signal-def' for signals)
138
+ */
75
139
  // TODO we should refactor/clean up how LiveQueryDef / SignalDef / LiveQuery / ISignal are defined (particularly on the type-level)
76
140
  export interface LiveQueryDef<TResult, TTag extends string = 'def'> {
77
141
  _tag: TTag
78
- /** Creates a new LiveQuery instance bound to a specific store/reactivityGraph */
142
+ /** Creates a reference-counted query instance bound to a Store's reactivity graph */
79
143
  make: (ctx: ReactivityGraphContext, otelContext?: otel.Context) => RcRef<LiveQuery<TResult> | ISignal<TResult>>
144
+ /** Human-readable label for debugging and devtools */
80
145
  label: string
146
+ /** Unique identifier derived from the query string or explicit deps; used for caching */
81
147
  hash: string
82
- [Equal.symbol](that: LiveQueryDef<TResult, TTag>): boolean
148
+ [Equal.symbol](that: Equal.Equal): boolean
83
149
  [Hash.symbol](): number
84
150
  }
85
151
 
@@ -88,39 +154,84 @@ export namespace LiveQueryDef {
88
154
  }
89
155
 
90
156
  /**
91
- * A LiveQuery is stateful
157
+ * Type guard that checks if a value is a query or signal definition.
158
+ *
159
+ * Use this to distinguish between definitions (blueprints) and instances (live queries).
160
+ * Definitions are created by `queryDb()`, `computed()`, and `signal()`.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const todos$ = queryDb(tables.todos.all())
165
+ *
166
+ * if (isLiveQueryDef(todos$)) {
167
+ * console.log('This is a definition:', todos$.label)
168
+ * }
169
+ * ```
170
+ */
171
+ export const isLiveQueryDef = (value: unknown): value is LiveQueryDef<any> | SignalDef<any> => {
172
+ if (Predicate.isTagged(value, 'def') === false && Predicate.isTagged(value, 'signal-def') === false) {
173
+ return false
174
+ }
175
+
176
+ return (
177
+ Predicate.hasProperty(value, 'make') &&
178
+ Predicate.isFunction(value.make) &&
179
+ Predicate.hasProperty(value, 'hash') &&
180
+ Predicate.isString(value.hash) &&
181
+ Predicate.hasProperty(value, 'label') &&
182
+ Predicate.isString(value.label)
183
+ )
184
+ }
185
+
186
+ export const isSignalDef = (value: unknown): value is SignalDef<any> =>
187
+ isLiveQueryDef(value) && Predicate.isTagged(value, 'signal-def')
188
+
189
+ /**
190
+ * A live query instance bound to a specific Store.
191
+ *
192
+ * `LiveQuery` represents an active, stateful query in the reactivity graph. Unlike
193
+ * query definitions (`LiveQueryDef`), instances maintain state like execution counts,
194
+ * timing data, and active subscriptions.
195
+ *
196
+ * You typically don't work with `LiveQuery` directly—use `store.query()` for one-shot
197
+ * reads or `store.subscribe()` for reactive subscriptions. The instance is managed
198
+ * automatically via reference counting.
199
+ *
200
+ * @typeParam TResult - The type of value the query returns
92
201
  */
93
202
  export interface LiveQuery<TResult> {
203
+ /** Unique identifier for this query instance */
94
204
  id: number
205
+ /** Discriminator for the query type */
95
206
  _tag: 'computed' | 'db' | 'graphql' | 'signal'
96
207
  [TypeId]: TypeId
97
208
 
98
- // reactivityGraph: ReactivityGraph
99
-
100
- /** This should only be used on a type-level and doesn't hold any value during runtime */
209
+ /** Type-level only—extracts the result type from a LiveQuery */
101
210
  '__result!': TResult
102
211
 
103
- /** A reactive thunk representing the query results */
212
+ /** The underlying reactive atom in the graph that holds the query result */
104
213
  results$: RG.Atom<TResult, ReactivityGraphContext, RefreshReason>
105
214
 
215
+ /** Human-readable label for debugging and devtools */
106
216
  label: string
107
217
 
218
+ /** Executes the query and returns the result */
108
219
  run: (args: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason }) => TResult
109
220
 
221
+ /** Removes the query from the reactivity graph */
110
222
  destroy: () => void
223
+ /** Whether this query instance has been destroyed */
111
224
  isDestroyed: boolean
112
225
 
113
- // subscribe(
114
- // onNewValue: (value: TResult) => void,
115
- // onUnsubsubscribe?: () => void,
116
- // options?: { label?: string; otelContext?: otel.Context },
117
- // ): () => void
118
-
226
+ /** Stack traces of active subscriptions (for debugging) */
119
227
  activeSubscriptions: Set<StackInfo>
120
228
 
229
+ /** Number of times this query has been executed */
121
230
  runs: number
122
231
 
232
+ /** Execution times in milliseconds (for performance monitoring) */
123
233
  executionTimes: number[]
234
+ /** The definition that created this instance */
124
235
  def: LiveQueryDef<TResult> | SignalDef<TResult>
125
236
  }
126
237
 
@@ -166,17 +277,36 @@ export abstract class LiveStoreQueryBase<TResult> implements LiveQuery<TResult>
166
277
 
167
278
  // subscribe = (
168
279
  // onNewValue: (value: TResult) => void,
169
- // onUnsubsubscribe?: () => void,
170
- // options?: { label?: string; otelContext?: otel.Context } | undefined,
280
+ // options?: {
281
+ // label?: string
282
+ // otelContext?: otel.Context
283
+ // onUnsubsubscribe?: () => void
284
+ // },
171
285
  // ): (() => void) =>
172
- // this.reactivityGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
286
+ // this.reactivityGraph.context?.store.subscribe(this, onNewValue, options) ??
173
287
  // RG.throwContextNotSetError(this.reactivityGraph)
174
288
  }
175
289
 
290
+ /**
291
+ * Function signature for the `get` parameter in `computed()` and `queryDb()` callbacks.
292
+ *
293
+ * Call `get()` with a query definition, signal, or live query instance to:
294
+ * 1. Read its current value
295
+ * 2. Establish a reactive dependency (the caller re-runs when the dependency changes)
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * const filtered$ = computed((get) => {
300
+ * const todos = get(todos$) // Depends on todos$
301
+ * const filter = get(filterText$) // Depends on filterText$
302
+ * return todos.filter((t) => t.text.includes(filter))
303
+ * })
304
+ * ```
305
+ */
176
306
  export type GetAtomResult = <T>(
177
307
  atom: RG.Atom<T, any, RefreshReason> | LiveQueryDef<T> | LiveQuery<T> | ISignal<T> | SignalDef<T>,
178
- otelContext?: otel.Context | undefined,
179
- debugRefreshReason?: RefreshReason | undefined,
308
+ otelContext?: otel.Context,
309
+ debugRefreshReason?: RefreshReason,
180
310
  ) => T
181
311
 
182
312
  export type DependencyQueriesRef = Set<RcRef<LiveQuery.Any | ISignal<any>>>
@@ -201,7 +331,7 @@ export const makeGetAtomResult = (
201
331
  }
202
332
 
203
333
  // Signal case
204
- if (atom._tag === 'signal' && Predicate.hasProperty(atom, 'ref')) {
334
+ if (atom._tag === 'signal' && Predicate.hasProperty(atom, 'ref') === true) {
205
335
  return get(atom.ref, otelContext, debugRefreshReason)
206
336
  }
207
337
 
@@ -218,7 +348,7 @@ export const withRCMap = <T extends LiveQuery.Any | ISignal<any>>(
218
348
  ): ((ctx: ReactivityGraphContext, otelContext?: otel.Context) => RcRef<T>) => {
219
349
  return (ctx, otelContext) => {
220
350
  let item = ctx.defRcMap.get(id)
221
- if (item) {
351
+ if (item !== undefined) {
222
352
  item.rc++
223
353
  return item as RcRef<T>
224
354
  }
@@ -1,9 +1,11 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+
1
3
  import type { PreparedBindValues } from '@livestore/common'
2
4
  import { SessionIdSymbol } from '@livestore/common'
3
5
  import { State } from '@livestore/common/schema'
4
6
  import { shouldNeverHappen } from '@livestore/utils'
5
- import type * as otel from '@opentelemetry/api'
6
7
 
8
+ import { StoreInternalsSymbol } from '../store/store-types.ts'
7
9
  import type { ReactivityGraphContext } from './base-class.ts'
8
10
 
9
11
  export const rowQueryLabel = (
@@ -30,17 +32,17 @@ export const makeExecBeforeFirstRun =
30
32
  )
31
33
  }
32
34
 
33
- const otelContext = otelContext_ ?? store.otel.queriesSpanContext
35
+ const otelContext = otelContext_ ?? store[StoreInternalsSymbol].otel.queriesSpanContext
34
36
 
35
37
  const idVal = id === SessionIdSymbol ? store.sessionId : id!
36
38
  const rowExists =
37
- store.sqliteDbWrapper.cachedSelect(
39
+ store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
38
40
  `SELECT 1 FROM '${table.sqliteDef.name}' WHERE id = ?`,
39
41
  [idVal] as any as PreparedBindValues,
40
42
  { otelContext },
41
43
  ).length === 1
42
44
 
43
- if (rowExists) return
45
+ if (rowExists === true) return
44
46
 
45
47
  // It's important that we only commit and don't refresh here, as this function might be called during a render
46
48
  // and otherwise we might end up in a "reactive loop"
@@ -1,13 +1,62 @@
1
- import { getDurationMsFromSpan } from '@livestore/common'
2
- import { Equal, Hash } from '@livestore/utils/effect'
3
1
  import * as otel from '@opentelemetry/api'
4
2
 
3
+ import { Equal, Hash } from '@livestore/utils/effect'
4
+
5
5
  import type { Thunk } from '../reactive.ts'
6
6
  import type { RefreshReason } from '../store/store-types.ts'
7
7
  import { isValidFunctionString } from '../utils/function-string.ts'
8
8
  import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.ts'
9
- import { depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
10
-
9
+ import { depsToString, isLiveQueryDef, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
10
+
11
+ /**
12
+ * Creates a derived query that computes a value from other queries or signals.
13
+ *
14
+ * Computed queries are memoized—they only re-evaluate when their dependencies change,
15
+ * and if the new result equals the previous result, downstream dependents won't re-run.
16
+ * Use them for expensive calculations, aggregations, or transformations.
17
+ *
18
+ * The `get` function inside `computed` establishes reactive dependencies automatically.
19
+ * When any dependency updates, the computed re-evaluates.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // Derive a count from a database query
24
+ * const todos$ = queryDb(tables.todos.all())
25
+ * const todoCount$ = computed((get) => get(todos$).length, { label: 'todoCount' })
26
+ *
27
+ * // Use in a component
28
+ * const count = store.query(todoCount$) // 5
29
+ * ```
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // Combine multiple queries into derived stats
34
+ * const stats$ = computed((get) => {
35
+ * const todos = get(todos$)
36
+ * const completed = todos.filter((t) => t.completed).length
37
+ * return {
38
+ * total: todos.length,
39
+ * completed,
40
+ * remaining: todos.length - completed,
41
+ * percentComplete: todos.length > 0 ? (completed / todos.length) * 100 : 0,
42
+ * }
43
+ * }, { label: 'todoStats' })
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * // Chain computed queries
49
+ * const hasCompletedTodos$ = computed(
50
+ * (get) => get(stats$).completed > 0,
51
+ * { label: 'hasCompletedTodos' }
52
+ * )
53
+ * ```
54
+ *
55
+ * @param fn - Pure function that computes the result. Use `get()` to read dependencies.
56
+ * @param options.label - Human-readable label for debugging and devtools
57
+ * @param options.deps - Explicit dependency keys (required on Expo/React Native where `fn.toString()` returns `[native code]`)
58
+ * @returns A query definition usable with `store.query()`, `store.subscribe()`, and as a dependency in other queries
59
+ */
11
60
  export const computed = <TResult>(
12
61
  fn: (get: GetAtomResult) => TResult,
13
62
  options?: {
@@ -15,7 +64,7 @@ export const computed = <TResult>(
15
64
  deps?: DepKey
16
65
  },
17
66
  ): LiveQueryDef<TResult> => {
18
- const hash = options?.deps ? depsToString(options.deps) : fn.toString()
67
+ const hash = options?.deps !== undefined ? depsToString(options.deps) : fn.toString()
19
68
  if (isValidFunctionString(hash)._tag === 'invalid') {
20
69
  throw new Error(`On Expo/React Native, computed queries must provide a \`deps\` option`)
21
70
  }
@@ -36,8 +85,8 @@ export const computed = <TResult>(
36
85
  // TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
37
86
  // NOTE `fn.toString()` doesn't work in Expo as it always produces `[native code]`
38
87
  hash,
39
- [Equal.symbol](that: LiveQueryDef<any>): boolean {
40
- return this.hash === that.hash
88
+ [Equal.symbol](that: Equal.Equal): boolean {
89
+ return isLiveQueryDef(that) && that._tag === 'def' && this.hash === that.hash
41
90
  },
42
91
  [Hash.symbol](): number {
43
92
  return Hash.string(this.hash)
@@ -47,6 +96,13 @@ export const computed = <TResult>(
47
96
  return def
48
97
  }
49
98
 
99
+ /**
100
+ * A live computed query instance bound to a specific Store.
101
+ *
102
+ * Computed query instances are created internally when you use a `LiveQueryDef` (from {@link computed})
103
+ * with the Store. You typically don't construct these directly—use `computed()` to create definitions
104
+ * and `store.query()` / `store.subscribe()` to interact with them.
105
+ */
50
106
  export class LiveStoreComputedQuery<TResult> extends LiveStoreQueryBase<TResult> {
51
107
  _tag = 'computed' as const
52
108
 
@@ -80,12 +136,13 @@ export class LiveStoreComputedQuery<TResult> extends LiveStoreQueryBase<TResult>
80
136
  this.results$ = this.reactivityGraph.makeThunk(
81
137
  (get, setDebugInfo, ctx, otelContext) =>
82
138
  ctx.otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? ctx.rootOtelContext, (span) => {
139
+ const startTimePerfNow = performance.now()
83
140
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
84
141
  const res = fn(makeGetAtomResult(get, ctx, otelContext, this.dependencyQueriesRef))
85
142
 
86
143
  span.end()
87
144
 
88
- const durationMs = getDurationMsFromSpan(span)
145
+ const durationMs = performance.now() - startTimePerfNow
89
146
 
90
147
  this.executionTimes.push(durationMs)
91
148