@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.
- package/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +15 -13
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +24 -6
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +134 -6
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +190 -11
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +41 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +129 -9
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +30 -4
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +4 -3
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts +56 -0
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +62 -6
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +2 -2
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +41 -30
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +112 -29
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.d.ts +49 -0
- package/dist/live-queries/signal.d.ts.map +1 -1
- package/dist/live-queries/signal.js +51 -2
- package/dist/live-queries/signal.js.map +1 -1
- package/dist/live-queries/signal.test.js +4 -4
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +3 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +3 -2
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +10 -10
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +29 -27
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +384 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +99 -22
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +85 -37
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +6 -18
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +61 -19
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.d.ts +2 -0
- package/dist/store/store-eventstream.test.d.ts.map +1 -0
- package/dist/store/store-eventstream.test.js +65 -0
- package/dist/store/store-eventstream.test.js.map +1 -0
- package/dist/store/store-types.d.ts +271 -31
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +41 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.d.ts +2 -0
- package/dist/store/store-types.test.d.ts.map +1 -0
- package/dist/store/store-types.test.js +39 -0
- package/dist/store/store-types.test.js.map +1 -0
- package/dist/store/store.d.ts +255 -68
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +533 -186
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +3 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +46 -39
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +7 -0
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +50 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +30 -6
- package/src/SqliteDbWrapper.ts +21 -17
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +426 -22
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
- package/src/live-queries/base-class.ts +162 -32
- package/src/live-queries/client-document-get-query.ts +6 -4
- package/src/live-queries/computed.ts +65 -8
- package/src/live-queries/db-query.test.ts +168 -30
- package/src/live-queries/db-query.ts +58 -43
- package/src/live-queries/signal.test.ts +5 -4
- package/src/live-queries/signal.ts +52 -3
- package/src/mod.ts +19 -2
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +58 -49
- package/src/store/StoreRegistry.test.ts +543 -0
- package/src/store/StoreRegistry.ts +429 -0
- package/src/store/create-store.ts +227 -74
- package/src/store/devtools.ts +305 -261
- package/src/store/store-eventstream.test.ts +123 -0
- package/src/store/store-types.test.ts +52 -0
- package/src/store/store-types.ts +318 -44
- package/src/store/store.ts +695 -283
- package/src/utils/dev.ts +3 -4
- package/src/utils/stack-info.ts +5 -2
- package/src/utils/tests/fixture.ts +9 -1
- 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>> =
|
|
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
|
|
29
|
+
: TQuery extends LiveQueryDef<infer TResult>
|
|
33
30
|
? TResult
|
|
34
|
-
:
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
170
|
-
//
|
|
280
|
+
// options?: {
|
|
281
|
+
// label?: string
|
|
282
|
+
// otelContext?: otel.Context
|
|
283
|
+
// onUnsubsubscribe?: () => void
|
|
284
|
+
// },
|
|
171
285
|
// ): (() => void) =>
|
|
172
|
-
// this.reactivityGraph.context?.store.subscribe(this, onNewValue,
|
|
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
|
|
179
|
-
debugRefreshReason?: RefreshReason
|
|
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:
|
|
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 =
|
|
145
|
+
const durationMs = performance.now() - startTimePerfNow
|
|
89
146
|
|
|
90
147
|
this.executionTimes.push(durationMs)
|
|
91
148
|
|