@livestore/livestore 0.4.0-dev.8 → 0.4.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 +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +4 -3
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +133 -5
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +187 -8
- 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 +42 -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 +110 -7
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- 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 +58 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +21 -19
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +106 -23
- 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 +49 -0
- package/dist/live-queries/signal.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- 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 +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- 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 +381 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +98 -18
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +49 -20
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +5 -16
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +59 -18
- 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 +285 -27
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +77 -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 +253 -66
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +442 -153
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +20 -5
- 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 +59 -17
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +5 -3
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +388 -13
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +604 -192
- package/src/live-queries/base-class.ts +126 -28
- package/src/live-queries/client-document-get-query.ts +6 -4
- package/src/live-queries/computed.ts +59 -2
- package/src/live-queries/db-query.test.ts +162 -24
- package/src/live-queries/db-query.ts +23 -20
- package/src/live-queries/signal.test.ts +3 -2
- package/src/live-queries/signal.ts +49 -0
- package/src/mod.ts +19 -2
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +540 -0
- package/src/store/StoreRegistry.ts +418 -0
- package/src/store/create-store.ts +158 -39
- package/src/store/devtools.ts +77 -33
- package/src/store/store-eventstream.test.ts +114 -0
- package/src/store/store-types.test.ts +52 -0
- package/src/store/store-types.ts +360 -40
- package/src/store/store.ts +571 -236
- package/src/utils/dev.ts +2 -3
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +9 -1
- package/src/utils/tests/otel.ts +8 -7
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import { isNotNil } from '@livestore/utils'
|
|
2
4
|
import { Equal, Hash, Predicate } from '@livestore/utils/effect'
|
|
3
|
-
import type * as otel from '@opentelemetry/api'
|
|
4
5
|
|
|
5
6
|
import * as RG from '../reactive.ts'
|
|
6
|
-
import type { Store } from '../store/store.ts'
|
|
7
7
|
import type { QueryDebugInfo, RefreshReason } from '../store/store-types.ts'
|
|
8
|
+
import type { Store } from '../store/store.ts'
|
|
8
9
|
import type { StackInfo } from '../utils/stack-info.ts'
|
|
9
10
|
|
|
10
11
|
export type ReactivityGraph = RG.ReactiveGraph<RefreshReason, QueryDebugInfo, ReactivityGraphContext>
|
|
@@ -23,46 +24,98 @@ export type ReactivityGraphContext = {
|
|
|
23
24
|
effectsWrapper: (run: () => void) => void
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export type GetResult<TQuery extends LiveQueryDef.Any | LiveQuery.Any | SignalDef<any>> =
|
|
27
|
-
infer TResult
|
|
28
|
-
>
|
|
29
|
-
? TResult
|
|
30
|
-
: TQuery extends LiveQueryDef<infer TResult>
|
|
27
|
+
export type GetResult<TQuery extends LiveQueryDef.Any | LiveQuery.Any | SignalDef<any>> =
|
|
28
|
+
TQuery extends LiveQuery<infer TResult>
|
|
31
29
|
? TResult
|
|
32
|
-
: TQuery extends
|
|
30
|
+
: TQuery extends LiveQueryDef<infer TResult>
|
|
33
31
|
? TResult
|
|
34
|
-
:
|
|
32
|
+
: TQuery extends SignalDef<infer TResult>
|
|
33
|
+
? TResult
|
|
34
|
+
: unknown
|
|
35
35
|
|
|
36
36
|
let queryIdCounter = 0
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* A signal definition representing ephemeral, local-only reactive state.
|
|
40
|
+
*
|
|
41
|
+
* `SignalDef` is the type returned by {@link signal}. It's a blueprint for creating
|
|
42
|
+
* signal instances—the actual instance is created when you use the definition with
|
|
43
|
+
* a Store via `store.query()` or `store.setSignal()`.
|
|
44
|
+
*
|
|
45
|
+
* @typeParam T - The type of value the signal holds
|
|
46
|
+
*/
|
|
38
47
|
export interface SignalDef<T> extends LiveQueryDef<T, 'signal-def'> {
|
|
39
48
|
_tag: 'signal-def'
|
|
49
|
+
/** The initial value used when the signal is first created */
|
|
40
50
|
defaultValue: T
|
|
51
|
+
/** Unique identifier for caching and deduplication */
|
|
41
52
|
hash: string
|
|
53
|
+
/** Human-readable label for debugging and devtools */
|
|
42
54
|
label: string
|
|
55
|
+
/** Creates a reference-counted signal instance bound to a Store's reactivity graph */
|
|
43
56
|
make: (ctx: ReactivityGraphContext) => RcRef<ISignal<T>>
|
|
44
57
|
[Equal.symbol](that: SignalDef<T>): boolean
|
|
45
58
|
[Hash.symbol](): number
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Interface for a live signal instance.
|
|
63
|
+
*
|
|
64
|
+
* This represents an active signal bound to a Store's reactivity graph.
|
|
65
|
+
* Use `store.setSignal()` to update values and `store.query()` to read them.
|
|
66
|
+
*
|
|
67
|
+
* @typeParam T - The type of value the signal holds
|
|
68
|
+
*/
|
|
48
69
|
export interface ISignal<T> extends LiveQuery<T> {
|
|
49
70
|
_tag: 'signal'
|
|
50
71
|
reactivityGraph: ReactivityGraph
|
|
72
|
+
/** The underlying reactive reference in the graph */
|
|
51
73
|
ref: RG.Ref<T, ReactivityGraphContext, RefreshReason>
|
|
74
|
+
/** Sets the signal's value (prefer using `store.setSignal()` instead) */
|
|
52
75
|
set: (value: T) => void
|
|
76
|
+
/** Gets the signal's current value (prefer using `store.query()` instead) */
|
|
53
77
|
get: () => T
|
|
78
|
+
/** Removes the signal from the reactivity graph */
|
|
54
79
|
destroy: () => void
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
export const TypeId = Symbol.for('LiveQuery')
|
|
58
83
|
export type TypeId = typeof TypeId
|
|
59
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A reference-counted wrapper around a LiveQuery or Signal instance.
|
|
87
|
+
*
|
|
88
|
+
* LiveStore uses reference counting to manage query lifecycle. When multiple
|
|
89
|
+
* components or subscriptions use the same query definition, they share a single
|
|
90
|
+
* instance. The instance is destroyed when the last reference is released.
|
|
91
|
+
*
|
|
92
|
+
* You typically don't interact with `RcRef` directly—it's used internally by
|
|
93
|
+
* hooks like `useQuery` and `useQueryRef`.
|
|
94
|
+
*/
|
|
60
95
|
export interface RcRef<T> {
|
|
96
|
+
/** Current reference count */
|
|
61
97
|
rc: number
|
|
98
|
+
/** The wrapped query or signal instance */
|
|
62
99
|
value: T
|
|
100
|
+
/** Decrements the reference count; destroys the instance when it reaches zero */
|
|
63
101
|
deref: () => void
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Dependency key used to identify queries on platforms where `fn.toString()` isn't reliable.
|
|
106
|
+
*
|
|
107
|
+
* On Expo/React Native, Hermes compiles functions to bytecode, so `fn.toString()` returns
|
|
108
|
+
* `[native code]`. To uniquely identify contextual queries, you must provide explicit `deps`.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* // On Expo, this would fail without deps:
|
|
113
|
+
* const filtered$ = queryDb(
|
|
114
|
+
* (get) => tables.todos.where({ userId: get(userId$) }),
|
|
115
|
+
* { deps: [userId] } // Required on Expo/React Native
|
|
116
|
+
* )
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
66
119
|
export type DepKey = string | number | ReadonlyArray<string | number | undefined | null>
|
|
67
120
|
|
|
68
121
|
export const depsToString = (deps: DepKey): string => {
|
|
@@ -72,12 +125,26 @@ export const depsToString = (deps: DepKey): string => {
|
|
|
72
125
|
return deps.filter(isNotNil).join(',')
|
|
73
126
|
}
|
|
74
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A query definition representing a blueprint for a reactive query.
|
|
130
|
+
*
|
|
131
|
+
* Query definitions are created by {@link queryDb}, {@link computed}, and {@link signal}.
|
|
132
|
+
* They're lightweight and can be defined at module scope. The actual query instance
|
|
133
|
+
* (which holds state) is created lazily when you use the definition with a Store.
|
|
134
|
+
*
|
|
135
|
+
* Multiple uses of the same definition share a single instance via reference counting.
|
|
136
|
+
*
|
|
137
|
+
* @typeParam TResult - The type of value the query returns
|
|
138
|
+
* @typeParam TTag - Internal discriminator tag ('def' for queries, 'signal-def' for signals)
|
|
139
|
+
*/
|
|
75
140
|
// TODO we should refactor/clean up how LiveQueryDef / SignalDef / LiveQuery / ISignal are defined (particularly on the type-level)
|
|
76
141
|
export interface LiveQueryDef<TResult, TTag extends string = 'def'> {
|
|
77
142
|
_tag: TTag
|
|
78
|
-
/** Creates a
|
|
143
|
+
/** Creates a reference-counted query instance bound to a Store's reactivity graph */
|
|
79
144
|
make: (ctx: ReactivityGraphContext, otelContext?: otel.Context) => RcRef<LiveQuery<TResult> | ISignal<TResult>>
|
|
145
|
+
/** Human-readable label for debugging and devtools */
|
|
80
146
|
label: string
|
|
147
|
+
/** Unique identifier derived from the query string or explicit deps; used for caching */
|
|
81
148
|
hash: string
|
|
82
149
|
[Equal.symbol](that: LiveQueryDef<TResult, TTag>): boolean
|
|
83
150
|
[Hash.symbol](): number
|
|
@@ -88,39 +155,51 @@ export namespace LiveQueryDef {
|
|
|
88
155
|
}
|
|
89
156
|
|
|
90
157
|
/**
|
|
91
|
-
* A
|
|
158
|
+
* A live query instance bound to a specific Store.
|
|
159
|
+
*
|
|
160
|
+
* `LiveQuery` represents an active, stateful query in the reactivity graph. Unlike
|
|
161
|
+
* query definitions (`LiveQueryDef`), instances maintain state like execution counts,
|
|
162
|
+
* timing data, and active subscriptions.
|
|
163
|
+
*
|
|
164
|
+
* You typically don't work with `LiveQuery` directly—use `store.query()` for one-shot
|
|
165
|
+
* reads or `store.subscribe()` for reactive subscriptions. The instance is managed
|
|
166
|
+
* automatically via reference counting.
|
|
167
|
+
*
|
|
168
|
+
* @typeParam TResult - The type of value the query returns
|
|
92
169
|
*/
|
|
93
170
|
export interface LiveQuery<TResult> {
|
|
171
|
+
/** Unique identifier for this query instance */
|
|
94
172
|
id: number
|
|
173
|
+
/** Discriminator for the query type */
|
|
95
174
|
_tag: 'computed' | 'db' | 'graphql' | 'signal'
|
|
96
175
|
[TypeId]: TypeId
|
|
97
176
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** This should only be used on a type-level and doesn't hold any value during runtime */
|
|
177
|
+
/** Type-level only—extracts the result type from a LiveQuery */
|
|
101
178
|
'__result!': TResult
|
|
102
179
|
|
|
103
|
-
/**
|
|
180
|
+
/** The underlying reactive atom in the graph that holds the query result */
|
|
104
181
|
results$: RG.Atom<TResult, ReactivityGraphContext, RefreshReason>
|
|
105
182
|
|
|
183
|
+
/** Human-readable label for debugging and devtools */
|
|
106
184
|
label: string
|
|
107
185
|
|
|
186
|
+
/** Executes the query and returns the result */
|
|
108
187
|
run: (args: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason }) => TResult
|
|
109
188
|
|
|
189
|
+
/** Removes the query from the reactivity graph */
|
|
110
190
|
destroy: () => void
|
|
191
|
+
/** Whether this query instance has been destroyed */
|
|
111
192
|
isDestroyed: boolean
|
|
112
193
|
|
|
113
|
-
|
|
114
|
-
// onNewValue: (value: TResult) => void,
|
|
115
|
-
// onUnsubsubscribe?: () => void,
|
|
116
|
-
// options?: { label?: string; otelContext?: otel.Context },
|
|
117
|
-
// ): () => void
|
|
118
|
-
|
|
194
|
+
/** Stack traces of active subscriptions (for debugging) */
|
|
119
195
|
activeSubscriptions: Set<StackInfo>
|
|
120
196
|
|
|
197
|
+
/** Number of times this query has been executed */
|
|
121
198
|
runs: number
|
|
122
199
|
|
|
200
|
+
/** Execution times in milliseconds (for performance monitoring) */
|
|
123
201
|
executionTimes: number[]
|
|
202
|
+
/** The definition that created this instance */
|
|
124
203
|
def: LiveQueryDef<TResult> | SignalDef<TResult>
|
|
125
204
|
}
|
|
126
205
|
|
|
@@ -166,17 +245,36 @@ export abstract class LiveStoreQueryBase<TResult> implements LiveQuery<TResult>
|
|
|
166
245
|
|
|
167
246
|
// subscribe = (
|
|
168
247
|
// onNewValue: (value: TResult) => void,
|
|
169
|
-
//
|
|
170
|
-
//
|
|
248
|
+
// options?: {
|
|
249
|
+
// label?: string
|
|
250
|
+
// otelContext?: otel.Context
|
|
251
|
+
// onUnsubsubscribe?: () => void
|
|
252
|
+
// },
|
|
171
253
|
// ): (() => void) =>
|
|
172
|
-
// this.reactivityGraph.context?.store.subscribe(this, onNewValue,
|
|
254
|
+
// this.reactivityGraph.context?.store.subscribe(this, onNewValue, options) ??
|
|
173
255
|
// RG.throwContextNotSetError(this.reactivityGraph)
|
|
174
256
|
}
|
|
175
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Function signature for the `get` parameter in `computed()` and `queryDb()` callbacks.
|
|
260
|
+
*
|
|
261
|
+
* Call `get()` with a query definition, signal, or live query instance to:
|
|
262
|
+
* 1. Read its current value
|
|
263
|
+
* 2. Establish a reactive dependency (the caller re-runs when the dependency changes)
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* const filtered$ = computed((get) => {
|
|
268
|
+
* const todos = get(todos$) // Depends on todos$
|
|
269
|
+
* const filter = get(filterText$) // Depends on filterText$
|
|
270
|
+
* return todos.filter((t) => t.text.includes(filter))
|
|
271
|
+
* })
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
176
274
|
export type GetAtomResult = <T>(
|
|
177
275
|
atom: RG.Atom<T, any, RefreshReason> | LiveQueryDef<T> | LiveQuery<T> | ISignal<T> | SignalDef<T>,
|
|
178
|
-
otelContext?: otel.Context
|
|
179
|
-
debugRefreshReason?: RefreshReason
|
|
276
|
+
otelContext?: otel.Context ,
|
|
277
|
+
debugRefreshReason?: RefreshReason ,
|
|
180
278
|
) => T
|
|
181
279
|
|
|
182
280
|
export type DependencyQueriesRef = Set<RcRef<LiveQuery.Any | ISignal<any>>>
|
|
@@ -201,7 +299,7 @@ export const makeGetAtomResult = (
|
|
|
201
299
|
}
|
|
202
300
|
|
|
203
301
|
// Signal case
|
|
204
|
-
if (atom._tag === 'signal' && Predicate.hasProperty(atom, 'ref')) {
|
|
302
|
+
if (atom._tag === 'signal' && Predicate.hasProperty(atom, 'ref') === true) {
|
|
205
303
|
return get(atom.ref, otelContext, debugRefreshReason)
|
|
206
304
|
}
|
|
207
305
|
|
|
@@ -218,7 +316,7 @@ export const withRCMap = <T extends LiveQuery.Any | ISignal<any>>(
|
|
|
218
316
|
): ((ctx: ReactivityGraphContext, otelContext?: otel.Context) => RcRef<T>) => {
|
|
219
317
|
return (ctx, otelContext) => {
|
|
220
318
|
let item = ctx.defRcMap.get(id)
|
|
221
|
-
if (item) {
|
|
319
|
+
if (item !== undefined) {
|
|
222
320
|
item.rc++
|
|
223
321
|
return item as RcRef<T>
|
|
224
322
|
}
|
|
@@ -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,6 +1,7 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import { getDurationMsFromSpan } from '@livestore/common'
|
|
2
4
|
import { Equal, Hash } from '@livestore/utils/effect'
|
|
3
|
-
import * as otel from '@opentelemetry/api'
|
|
4
5
|
|
|
5
6
|
import type { Thunk } from '../reactive.ts'
|
|
6
7
|
import type { RefreshReason } from '../store/store-types.ts'
|
|
@@ -8,6 +9,55 @@ import { isValidFunctionString } from '../utils/function-string.ts'
|
|
|
8
9
|
import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.ts'
|
|
9
10
|
import { depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Creates a derived query that computes a value from other queries or signals.
|
|
14
|
+
*
|
|
15
|
+
* Computed queries are memoized—they only re-evaluate when their dependencies change,
|
|
16
|
+
* and if the new result equals the previous result, downstream dependents won't re-run.
|
|
17
|
+
* Use them for expensive calculations, aggregations, or transformations.
|
|
18
|
+
*
|
|
19
|
+
* The `get` function inside `computed` establishes reactive dependencies automatically.
|
|
20
|
+
* When any dependency updates, the computed re-evaluates.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Derive a count from a database query
|
|
25
|
+
* const todos$ = queryDb(tables.todos.all())
|
|
26
|
+
* const todoCount$ = computed((get) => get(todos$).length, { label: 'todoCount' })
|
|
27
|
+
*
|
|
28
|
+
* // Use in a component
|
|
29
|
+
* const count = store.query(todoCount$) // 5
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // Combine multiple queries into derived stats
|
|
35
|
+
* const stats$ = computed((get) => {
|
|
36
|
+
* const todos = get(todos$)
|
|
37
|
+
* const completed = todos.filter((t) => t.completed).length
|
|
38
|
+
* return {
|
|
39
|
+
* total: todos.length,
|
|
40
|
+
* completed,
|
|
41
|
+
* remaining: todos.length - completed,
|
|
42
|
+
* percentComplete: todos.length > 0 ? (completed / todos.length) * 100 : 0,
|
|
43
|
+
* }
|
|
44
|
+
* }, { label: 'todoStats' })
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* // Chain computed queries
|
|
50
|
+
* const hasCompletedTodos$ = computed(
|
|
51
|
+
* (get) => get(stats$).completed > 0,
|
|
52
|
+
* { label: 'hasCompletedTodos' }
|
|
53
|
+
* )
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @param fn - Pure function that computes the result. Use `get()` to read dependencies.
|
|
57
|
+
* @param options.label - Human-readable label for debugging and devtools
|
|
58
|
+
* @param options.deps - Explicit dependency keys (required on Expo/React Native where `fn.toString()` returns `[native code]`)
|
|
59
|
+
* @returns A query definition usable with `store.query()`, `store.subscribe()`, and as a dependency in other queries
|
|
60
|
+
*/
|
|
11
61
|
export const computed = <TResult>(
|
|
12
62
|
fn: (get: GetAtomResult) => TResult,
|
|
13
63
|
options?: {
|
|
@@ -15,7 +65,7 @@ export const computed = <TResult>(
|
|
|
15
65
|
deps?: DepKey
|
|
16
66
|
},
|
|
17
67
|
): LiveQueryDef<TResult> => {
|
|
18
|
-
const hash = options?.deps ? depsToString(options.deps) : fn.toString()
|
|
68
|
+
const hash = options?.deps !== undefined ? depsToString(options.deps) : fn.toString()
|
|
19
69
|
if (isValidFunctionString(hash)._tag === 'invalid') {
|
|
20
70
|
throw new Error(`On Expo/React Native, computed queries must provide a \`deps\` option`)
|
|
21
71
|
}
|
|
@@ -47,6 +97,13 @@ export const computed = <TResult>(
|
|
|
47
97
|
return def
|
|
48
98
|
}
|
|
49
99
|
|
|
100
|
+
/**
|
|
101
|
+
* A live computed query instance bound to a specific Store.
|
|
102
|
+
*
|
|
103
|
+
* Computed query instances are created internally when you use a `LiveQueryDef` (from {@link computed})
|
|
104
|
+
* with the Store. You typically don't construct these directly—use `computed()` to create definitions
|
|
105
|
+
* and `store.query()` / `store.subscribe()` to interact with them.
|
|
106
|
+
*/
|
|
50
107
|
export class LiveStoreComputedQuery<TResult> extends LiveStoreQueryBase<TResult> {
|
|
51
108
|
_tag = 'computed' as const
|
|
52
109
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
2
|
-
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
3
1
|
import * as otel from '@opentelemetry/api'
|
|
4
2
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
5
|
-
import { expect } from 'vitest'
|
|
3
|
+
import { assert, expect } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
6
|
+
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
6
7
|
|
|
7
8
|
import * as RG from '../reactive.ts'
|
|
8
|
-
import {
|
|
9
|
+
import { StoreInternalsSymbol } from '../store/store-types.ts'
|
|
10
|
+
import { events, makeTodoMvc, type Todo, tables } from '../utils/tests/fixture.ts'
|
|
9
11
|
import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '../utils/tests/otel.ts'
|
|
10
12
|
import { computed } from './computed.ts'
|
|
11
13
|
import { queryDb } from './db-query.ts'
|
|
@@ -106,7 +108,7 @@ Vitest.describe('otel', () => {
|
|
|
106
108
|
{ label: 'all todos' },
|
|
107
109
|
)
|
|
108
110
|
|
|
109
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
111
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
110
112
|
|
|
111
113
|
expect(store.query(query$)).toMatchInlineSnapshot(`
|
|
112
114
|
{
|
|
@@ -116,11 +118,11 @@ Vitest.describe('otel', () => {
|
|
|
116
118
|
}
|
|
117
119
|
`)
|
|
118
120
|
|
|
119
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
121
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
120
122
|
|
|
121
123
|
store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false }))
|
|
122
124
|
|
|
123
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
125
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
124
126
|
|
|
125
127
|
expect(store.query(query$)).toMatchInlineSnapshot(`
|
|
126
128
|
{
|
|
@@ -130,7 +132,7 @@ Vitest.describe('otel', () => {
|
|
|
130
132
|
}
|
|
131
133
|
`)
|
|
132
134
|
|
|
133
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
135
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
134
136
|
|
|
135
137
|
span.end()
|
|
136
138
|
|
|
@@ -204,10 +206,8 @@ Vitest.describe('otel', () => {
|
|
|
204
206
|
.where({ completed: false })
|
|
205
207
|
.first({ behaviour: 'fallback', fallback: () => defaultTodo })
|
|
206
208
|
|
|
207
|
-
const unsubscribe = store.subscribe(queryBuilder, {
|
|
208
|
-
|
|
209
|
-
callbackResults.push(result)
|
|
210
|
-
},
|
|
209
|
+
const unsubscribe = store.subscribe(queryBuilder, (result) => {
|
|
210
|
+
callbackResults.push(result)
|
|
211
211
|
})
|
|
212
212
|
|
|
213
213
|
expect(callbackResults).toHaveLength(1)
|
|
@@ -239,6 +239,53 @@ Vitest.describe('otel', () => {
|
|
|
239
239
|
),
|
|
240
240
|
)
|
|
241
241
|
|
|
242
|
+
Vitest.scopedLive('QueryBuilder subscription - skipInitialRun', () =>
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
const { store, exporter, span, provider } = yield* makeQuery
|
|
245
|
+
|
|
246
|
+
const callbackResults: Todo[] = []
|
|
247
|
+
const defaultTodo: Todo = { id: '', text: '', completed: false }
|
|
248
|
+
|
|
249
|
+
const queryBuilder = tables.todos
|
|
250
|
+
.where({ completed: false })
|
|
251
|
+
.first({ behaviour: 'fallback', fallback: () => defaultTodo })
|
|
252
|
+
|
|
253
|
+
const unsubscribe = store.subscribe(
|
|
254
|
+
queryBuilder,
|
|
255
|
+
(result) => {
|
|
256
|
+
callbackResults.push(result)
|
|
257
|
+
},
|
|
258
|
+
{ skipInitialRun: true },
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expect(callbackResults).toHaveLength(0)
|
|
262
|
+
|
|
263
|
+
store.commit(events.todoCreated({ id: 't-skip', text: 'skip initial', completed: false }))
|
|
264
|
+
|
|
265
|
+
expect(callbackResults).toHaveLength(1)
|
|
266
|
+
expect(callbackResults[0]).toMatchObject({
|
|
267
|
+
id: 't-skip',
|
|
268
|
+
text: 'skip initial',
|
|
269
|
+
completed: false,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
unsubscribe()
|
|
273
|
+
span.end()
|
|
274
|
+
|
|
275
|
+
return { exporter, provider }
|
|
276
|
+
}).pipe(
|
|
277
|
+
Effect.scoped,
|
|
278
|
+
Effect.tap(({ exporter, provider }) =>
|
|
279
|
+
Effect.promise(async () => {
|
|
280
|
+
await provider.forceFlush()
|
|
281
|
+
expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
282
|
+
expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
283
|
+
await provider.shutdown()
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
|
|
242
289
|
Vitest.scopedLive('QueryBuilder subscription - unsubscribe functionality', () =>
|
|
243
290
|
Effect.gen(function* () {
|
|
244
291
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
@@ -251,16 +298,12 @@ Vitest.describe('otel', () => {
|
|
|
251
298
|
.where({ completed: false })
|
|
252
299
|
.first({ behaviour: 'fallback', fallback: () => defaultTodo })
|
|
253
300
|
|
|
254
|
-
const unsubscribe1 = store.subscribe(queryBuilder, {
|
|
255
|
-
|
|
256
|
-
callbackResults1.push(result)
|
|
257
|
-
},
|
|
301
|
+
const unsubscribe1 = store.subscribe(queryBuilder, (result) => {
|
|
302
|
+
callbackResults1.push(result)
|
|
258
303
|
})
|
|
259
304
|
|
|
260
|
-
const unsubscribe2 = store.subscribe(queryBuilder, {
|
|
261
|
-
|
|
262
|
-
callbackResults2.push(result)
|
|
263
|
-
},
|
|
305
|
+
const unsubscribe2 = store.subscribe(queryBuilder, (result) => {
|
|
306
|
+
callbackResults2.push(result)
|
|
264
307
|
})
|
|
265
308
|
|
|
266
309
|
expect(callbackResults1).toHaveLength(1)
|
|
@@ -295,16 +338,111 @@ Vitest.describe('otel', () => {
|
|
|
295
338
|
),
|
|
296
339
|
)
|
|
297
340
|
|
|
341
|
+
Vitest.scopedLive('QueryBuilder subscription - async iterator', () =>
|
|
342
|
+
Effect.gen(function* () {
|
|
343
|
+
const { store, exporter, span, provider } = yield* makeQuery
|
|
344
|
+
|
|
345
|
+
const defaultTodo: Todo = { id: '', text: '', completed: false }
|
|
346
|
+
|
|
347
|
+
const queryBuilder = tables.todos
|
|
348
|
+
.where({ completed: false })
|
|
349
|
+
.first({ behaviour: 'fallback', fallback: () => defaultTodo })
|
|
350
|
+
|
|
351
|
+
yield* Effect.promise(async () => {
|
|
352
|
+
const iterator = store.subscribe(queryBuilder)[Symbol.asyncIterator]()
|
|
353
|
+
|
|
354
|
+
const initial = await iterator.next()
|
|
355
|
+
expect(initial.done).toBe(false)
|
|
356
|
+
expect(initial.value).toMatchObject(defaultTodo)
|
|
357
|
+
|
|
358
|
+
store.commit(events.todoCreated({ id: 't-async', text: 'write tests', completed: false }))
|
|
359
|
+
|
|
360
|
+
const update = await iterator.next()
|
|
361
|
+
expect(update.done).toBe(false)
|
|
362
|
+
expect(update.value).toMatchObject({
|
|
363
|
+
id: 't-async',
|
|
364
|
+
text: 'write tests',
|
|
365
|
+
completed: false,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const doneResult = await iterator.return?.()
|
|
369
|
+
assert(doneResult)
|
|
370
|
+
expect(doneResult.done).toBe(true)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
span.end()
|
|
374
|
+
|
|
375
|
+
return { exporter, provider }
|
|
376
|
+
}).pipe(
|
|
377
|
+
Effect.scoped,
|
|
378
|
+
Effect.tap(({ exporter, provider }) =>
|
|
379
|
+
Effect.promise(async () => {
|
|
380
|
+
await provider.forceFlush()
|
|
381
|
+
expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
382
|
+
expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
383
|
+
await provider.shutdown()
|
|
384
|
+
}),
|
|
385
|
+
),
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
Vitest.scopedLive('QueryBuilder subscription - async iterator with skipInitialRun', () =>
|
|
390
|
+
Effect.gen(function* () {
|
|
391
|
+
const { store, exporter, span, provider } = yield* makeQuery
|
|
392
|
+
|
|
393
|
+
const defaultTodo: Todo = { id: '', text: '', completed: false }
|
|
394
|
+
|
|
395
|
+
const queryBuilder = tables.todos
|
|
396
|
+
.where({ completed: false })
|
|
397
|
+
.first({ behaviour: 'fallback', fallback: () => defaultTodo })
|
|
398
|
+
|
|
399
|
+
yield* Effect.promise(async () => {
|
|
400
|
+
const iterator = store.subscribe(queryBuilder, { skipInitialRun: true })[Symbol.asyncIterator]()
|
|
401
|
+
|
|
402
|
+
const pending = Symbol('pending')
|
|
403
|
+
const nextPromise = iterator.next()
|
|
404
|
+
const raceResult = await Promise.race([nextPromise, Promise.resolve(pending)])
|
|
405
|
+
expect(raceResult).toBe(pending)
|
|
406
|
+
|
|
407
|
+
store.commit(events.todoCreated({ id: 't-async-skip', text: 'write tests later', completed: false }))
|
|
408
|
+
|
|
409
|
+
const update = await nextPromise
|
|
410
|
+
expect(update.done).toBe(false)
|
|
411
|
+
expect(update.value).toMatchObject({
|
|
412
|
+
id: 't-async-skip',
|
|
413
|
+
text: 'write tests later',
|
|
414
|
+
completed: false,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const doneResult = await iterator.return?.()
|
|
418
|
+
assert(doneResult)
|
|
419
|
+
expect(doneResult.done).toBe(true)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
span.end()
|
|
423
|
+
|
|
424
|
+
return { exporter, provider }
|
|
425
|
+
}).pipe(
|
|
426
|
+
Effect.scoped,
|
|
427
|
+
Effect.tap(({ exporter, provider }) =>
|
|
428
|
+
Effect.promise(async () => {
|
|
429
|
+
await provider.forceFlush()
|
|
430
|
+
expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
431
|
+
expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
432
|
+
await provider.shutdown()
|
|
433
|
+
}),
|
|
434
|
+
),
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
|
|
298
438
|
Vitest.scopedLive('QueryBuilder subscription - direct table subscription', () =>
|
|
299
439
|
Effect.gen(function* () {
|
|
300
440
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
301
441
|
|
|
302
442
|
const callbackResults: any[] = []
|
|
303
443
|
|
|
304
|
-
const unsubscribe = store.subscribe(tables.todos, {
|
|
305
|
-
|
|
306
|
-
callbackResults.push(result)
|
|
307
|
-
},
|
|
444
|
+
const unsubscribe = store.subscribe(tables.todos, (result) => {
|
|
445
|
+
callbackResults.push(result)
|
|
308
446
|
})
|
|
309
447
|
|
|
310
448
|
expect(callbackResults).toHaveLength(1)
|