@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,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'
|
|
@@ -52,7 +54,7 @@ Vitest.describe('otel', () => {
|
|
|
52
54
|
}
|
|
53
55
|
})
|
|
54
56
|
|
|
55
|
-
Vitest.
|
|
57
|
+
Vitest.live('otel', () =>
|
|
56
58
|
Effect.gen(function* () {
|
|
57
59
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
58
60
|
|
|
@@ -91,7 +93,7 @@ Vitest.describe('otel', () => {
|
|
|
91
93
|
),
|
|
92
94
|
)
|
|
93
95
|
|
|
94
|
-
Vitest.
|
|
96
|
+
Vitest.live('with thunks', () =>
|
|
95
97
|
Effect.gen(function* () {
|
|
96
98
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
97
99
|
|
|
@@ -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
|
|
|
@@ -148,7 +150,7 @@ Vitest.describe('otel', () => {
|
|
|
148
150
|
),
|
|
149
151
|
)
|
|
150
152
|
|
|
151
|
-
Vitest.
|
|
153
|
+
Vitest.live('with thunks with query builder and without labels', () =>
|
|
152
154
|
Effect.gen(function* () {
|
|
153
155
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
154
156
|
|
|
@@ -193,7 +195,7 @@ Vitest.describe('otel', () => {
|
|
|
193
195
|
),
|
|
194
196
|
)
|
|
195
197
|
|
|
196
|
-
Vitest.
|
|
198
|
+
Vitest.live('QueryBuilder subscription - basic functionality', () =>
|
|
197
199
|
Effect.gen(function* () {
|
|
198
200
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
199
201
|
|
|
@@ -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,7 +239,54 @@ Vitest.describe('otel', () => {
|
|
|
239
239
|
),
|
|
240
240
|
)
|
|
241
241
|
|
|
242
|
-
Vitest.
|
|
242
|
+
Vitest.live('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
|
+
|
|
289
|
+
Vitest.live('QueryBuilder subscription - unsubscribe functionality', () =>
|
|
243
290
|
Effect.gen(function* () {
|
|
244
291
|
const { store, exporter, span, provider } = yield* makeQuery
|
|
245
292
|
|
|
@@ -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
|
|
|
298
|
-
Vitest.
|
|
341
|
+
Vitest.live('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.live('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
|
+
|
|
438
|
+
Vitest.live('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)
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import type { Bindable, QueryBuilder } from '@livestore/common'
|
|
2
4
|
import {
|
|
3
|
-
getDurationMsFromSpan,
|
|
4
5
|
getResultSchema,
|
|
5
6
|
isQueryBuilder,
|
|
6
7
|
prepareBindValues,
|
|
7
8
|
QueryBuilderAstSymbol,
|
|
8
|
-
|
|
9
|
+
resolveSessionIdSymbolInBindValues,
|
|
9
10
|
SessionIdSymbol,
|
|
10
|
-
|
|
11
|
+
UnknownError,
|
|
11
12
|
} from '@livestore/common'
|
|
12
|
-
import { deepEqual, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
13
|
-
import { Equal, Hash, Predicate, Schema,
|
|
14
|
-
import * as otel from '@opentelemetry/api'
|
|
13
|
+
import { deepEqual, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
14
|
+
import { Equal, Hash, Predicate, Result, Schema, SchemaIssue } from '@livestore/utils/effect'
|
|
15
15
|
|
|
16
16
|
import type { Thunk } from '../reactive.ts'
|
|
17
17
|
import { isThunk, NOT_REFRESHED_YET } from '../reactive.ts'
|
|
18
18
|
import type { RefreshReason } from '../store/store-types.ts'
|
|
19
|
+
import { StoreInternalsSymbol } from '../store/store-types.ts'
|
|
19
20
|
import { isValidFunctionString } from '../utils/function-string.ts'
|
|
20
21
|
import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.ts'
|
|
21
|
-
import { depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
|
|
22
|
+
import { depsToString, isLiveQueryDef, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
|
|
22
23
|
import { makeExecBeforeFirstRun, rowQueryLabel } from './client-document-get-query.ts'
|
|
23
24
|
|
|
24
25
|
export type QueryInputRaw<TDecoded, TEncoded> = {
|
|
25
26
|
query: string
|
|
26
|
-
schema: Schema.
|
|
27
|
+
schema: Schema.Codec<TDecoded, TEncoded>
|
|
27
28
|
bindValues?: Bindable
|
|
28
29
|
/**
|
|
29
30
|
* Can be provided explicitly to slightly speed up initial query performance
|
|
@@ -102,7 +103,11 @@ export const queryDb: {
|
|
|
102
103
|
} = (queryInput, options) => {
|
|
103
104
|
const { queryString, extraDeps } = getQueryStringAndExtraDeps(queryInput)
|
|
104
105
|
|
|
105
|
-
const hash = [
|
|
106
|
+
const hash = [
|
|
107
|
+
queryString,
|
|
108
|
+
options?.deps !== undefined ? depsToString(options.deps) : undefined,
|
|
109
|
+
depsToString(extraDeps),
|
|
110
|
+
]
|
|
106
111
|
.filter(Boolean)
|
|
107
112
|
.join('-')
|
|
108
113
|
|
|
@@ -111,7 +116,7 @@ export const queryDb: {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
if (hash.trim() === '') {
|
|
114
|
-
return shouldNeverHappen(
|
|
119
|
+
return shouldNeverHappen('Invalid query hash for query:', objectToString(queryInput))
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
const label = options?.label ?? queryString
|
|
@@ -130,8 +135,8 @@ export const queryDb: {
|
|
|
130
135
|
}),
|
|
131
136
|
label,
|
|
132
137
|
hash,
|
|
133
|
-
[Equal.symbol](that:
|
|
134
|
-
return this.hash === that.hash
|
|
138
|
+
[Equal.symbol](that: Equal.Equal): boolean {
|
|
139
|
+
return isLiveQueryDef(that) && that._tag === 'def' && this.hash === that.hash
|
|
135
140
|
},
|
|
136
141
|
[Hash.symbol](): number {
|
|
137
142
|
return Hash.string(this.hash)
|
|
@@ -154,12 +159,12 @@ const bindValuesToDepKey = (bindValues: Bindable | undefined): DepKey => {
|
|
|
154
159
|
const getQueryStringAndExtraDeps = (
|
|
155
160
|
queryInput: QueryInput<any, any> | ((get: GetAtomResult) => QueryInput<any, any>),
|
|
156
161
|
): { queryString: string; extraDeps: DepKey } => {
|
|
157
|
-
if (isQueryBuilder(queryInput)) {
|
|
162
|
+
if (isQueryBuilder(queryInput) === true) {
|
|
158
163
|
const { query, bindValues } = queryInput.asSql()
|
|
159
164
|
return { queryString: query, extraDeps: bindValuesToDepKey(bindValues) }
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
if (isQueryInputRaw(queryInput)) {
|
|
167
|
+
if (isQueryInputRaw(queryInput) === true) {
|
|
163
168
|
return { queryString: queryInput.query, extraDeps: bindValuesToDepKey(queryInput.bindValues) }
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -167,7 +172,7 @@ const getQueryStringAndExtraDeps = (
|
|
|
167
172
|
return { queryString: queryInput.toString(), extraDeps: [] }
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
return shouldNeverHappen(`Invalid query input: ${queryInput}`)
|
|
175
|
+
return shouldNeverHappen(`Invalid query input: ${String(queryInput)}`)
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
/* An object encapsulating a reactive SQL query */
|
|
@@ -213,9 +218,13 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
213
218
|
|
|
214
219
|
this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
|
|
215
220
|
|
|
216
|
-
const schemaRef: { current: Schema.
|
|
221
|
+
const schemaRef: { current: Schema.Codec<any, any> | undefined } = {
|
|
217
222
|
current:
|
|
218
|
-
typeof queryInput === 'function'
|
|
223
|
+
typeof queryInput === 'function'
|
|
224
|
+
? undefined
|
|
225
|
+
: isQueryBuilder(queryInput) === true
|
|
226
|
+
? undefined
|
|
227
|
+
: queryInput.schema,
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
const execBeforeFirstRunRef: {
|
|
@@ -231,7 +240,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
231
240
|
const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
|
|
232
241
|
try {
|
|
233
242
|
const qbRes = qb.asSql()
|
|
234
|
-
const schema = getResultSchema(qb) as Schema.
|
|
243
|
+
const schema = getResultSchema(qb) as Schema.Codec<TResultSchema, ReadonlyArray<any>>
|
|
235
244
|
const ast = qb[QueryBuilderAstSymbol]
|
|
236
245
|
|
|
237
246
|
return {
|
|
@@ -253,7 +262,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
253
262
|
: undefined,
|
|
254
263
|
}
|
|
255
264
|
} catch (cause) {
|
|
256
|
-
throw new
|
|
265
|
+
throw new UnknownError({ cause, note: `Error building query for ${qb.toString()}`, payload: { qb } })
|
|
257
266
|
}
|
|
258
267
|
}
|
|
259
268
|
|
|
@@ -269,7 +278,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
269
278
|
|
|
270
279
|
let queryInputRaw: TQueryInputRaw
|
|
271
280
|
|
|
272
|
-
if (isQueryBuilder(queryInputResult)) {
|
|
281
|
+
if (isQueryBuilder(queryInputResult) === true) {
|
|
273
282
|
const res = fromQueryBuilder(queryInputResult, otelContext)
|
|
274
283
|
queryInputRaw = res.queryInputRaw
|
|
275
284
|
// setting label dynamically here
|
|
@@ -296,7 +305,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
296
305
|
this.queryInput$ = queryInputRaw$OrQueryInputRaw
|
|
297
306
|
} else {
|
|
298
307
|
let queryInputRaw: TQueryInputRaw
|
|
299
|
-
if (isQueryBuilder(queryInput)) {
|
|
308
|
+
if (isQueryBuilder(queryInput) === true) {
|
|
300
309
|
const res = fromQueryBuilder(queryInput, otelContext)
|
|
301
310
|
queryInputRaw = res.queryInputRaw
|
|
302
311
|
label = res.label
|
|
@@ -309,7 +318,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
309
318
|
queryInputRaw$OrQueryInputRaw = queryInputRaw
|
|
310
319
|
|
|
311
320
|
// this.label = inputLabel ? this.label : `db(${})`
|
|
312
|
-
if (inputLabel === undefined && isQueryBuilder(queryInput)) {
|
|
321
|
+
if (inputLabel === undefined && isQueryBuilder(queryInput) === true) {
|
|
313
322
|
const ast = queryInput[QueryBuilderAstSymbol]
|
|
314
323
|
if (ast._tag === 'RowQuery') {
|
|
315
324
|
label = `db(${rowQueryLabel(ast.tableDef, ast.id)})`
|
|
@@ -319,9 +328,9 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
319
328
|
|
|
320
329
|
const queriedTablesRef: { current: Set<string> | undefined } = { current: undefined }
|
|
321
330
|
|
|
322
|
-
const makeResultsEqual = (resultSchema: Schema.
|
|
331
|
+
const makeResultsEqual = (resultSchema: Schema.Codec<any, any>) => {
|
|
323
332
|
// Creating the equivalence function eagerly in outer scope as it might be expensive
|
|
324
|
-
const eq = Schema.
|
|
333
|
+
const eq = Schema.toEquivalence(resultSchema)
|
|
325
334
|
return (a: TResult, b: TResult) => (a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : eq(a, b))
|
|
326
335
|
}
|
|
327
336
|
|
|
@@ -341,13 +350,15 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
341
350
|
'db:...', // NOTE span name will be overridden further down
|
|
342
351
|
{
|
|
343
352
|
attributes: {
|
|
344
|
-
'livestore.debugRefreshReason':
|
|
345
|
-
|
|
346
|
-
|
|
353
|
+
'livestore.debugRefreshReason':
|
|
354
|
+
Predicate.hasProperty(debugRefreshReason, 'label') === true
|
|
355
|
+
? (debugRefreshReason.label as string)
|
|
356
|
+
: debugRefreshReason?._tag,
|
|
347
357
|
},
|
|
348
358
|
},
|
|
349
359
|
otelContext ?? queryContext.rootOtelContext,
|
|
350
360
|
(span) => {
|
|
361
|
+
const startTimePerfNow = performance.now()
|
|
351
362
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
352
363
|
const { store } = queryContext
|
|
353
364
|
|
|
@@ -356,45 +367,49 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
|
|
|
356
367
|
execBeforeFirstRunRef.current = undefined
|
|
357
368
|
}
|
|
358
369
|
|
|
359
|
-
const queryInputResult =
|
|
360
|
-
|
|
361
|
-
|
|
370
|
+
const queryInputResult =
|
|
371
|
+
isThunk(queryInputRaw$OrQueryInputRaw) === true
|
|
372
|
+
? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
|
|
373
|
+
: (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
|
|
362
374
|
|
|
363
375
|
const sqlString = queryInputResult.query
|
|
364
376
|
const bindValues = queryInputResult.bindValues
|
|
365
377
|
|
|
366
378
|
if (queriedTablesRef.current === undefined) {
|
|
367
|
-
queriedTablesRef.current = store.sqliteDbWrapper.getTablesUsed(sqlString)
|
|
379
|
+
queriedTablesRef.current = store[StoreInternalsSymbol].sqliteDbWrapper.getTablesUsed(sqlString)
|
|
368
380
|
}
|
|
369
381
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
382
|
+
// Live query inputs may be cached and re-run as reactive dependencies change.
|
|
383
|
+
// Resolve SessionIdSymbol per execution so the cached input stays symbolic and session-agnostic.
|
|
384
|
+
const resolvedBindValues =
|
|
385
|
+
bindValues === undefined ? undefined : resolveSessionIdSymbolInBindValues(bindValues, store.sessionId)
|
|
373
386
|
|
|
374
387
|
// Establish a reactive dependency on the tables used in the query
|
|
375
388
|
for (const tableName of queriedTablesRef.current) {
|
|
376
|
-
const tableRef =
|
|
389
|
+
const tableRef =
|
|
390
|
+
store[StoreInternalsSymbol].tableRefs[tableName] ??
|
|
391
|
+
shouldNeverHappen(`No table ref found for ${tableName}`)
|
|
377
392
|
get(tableRef, otelContext, debugRefreshReason)
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
span.setAttribute('sql.query', sqlString)
|
|
381
396
|
span.updateName(`db:${sqlString.slice(0, 50)}`)
|
|
382
397
|
|
|
383
|
-
const rawDbResults = store.sqliteDbWrapper.cachedSelect
|
|
398
|
+
const rawDbResults = store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
|
|
384
399
|
sqlString,
|
|
385
|
-
|
|
400
|
+
resolvedBindValues !== undefined ? prepareBindValues(resolvedBindValues, sqlString) : undefined,
|
|
386
401
|
{
|
|
387
|
-
queriedTables: queriedTablesRef.current,
|
|
388
402
|
otelContext,
|
|
403
|
+
...(queriedTablesRef.current !== undefined ? { queriedTables: queriedTablesRef.current } : {}),
|
|
389
404
|
},
|
|
390
405
|
)
|
|
391
406
|
|
|
392
407
|
span.setAttribute('sql.rowsCount', rawDbResults.length)
|
|
393
408
|
|
|
394
|
-
const parsedResult = Schema.
|
|
409
|
+
const parsedResult = Schema.decodeResult(schemaRef.current!)(rawDbResults)
|
|
395
410
|
|
|
396
|
-
if (parsedResult
|
|
397
|
-
const parseErrorStr =
|
|
411
|
+
if (Result.isFailure(parsedResult) === true) {
|
|
412
|
+
const parseErrorStr = SchemaIssue.makeFormatterDefault()(parsedResult.failure.issue)
|
|
398
413
|
const expectedSchemaStr = String(schemaRef.current!.ast)
|
|
399
414
|
const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
|
|
400
415
|
|
|
@@ -415,11 +430,11 @@ Result:`,
|
|
|
415
430
|
)
|
|
416
431
|
}
|
|
417
432
|
|
|
418
|
-
const result = this.mapResult(parsedResult.
|
|
433
|
+
const result = this.mapResult(parsedResult.success)
|
|
419
434
|
|
|
420
435
|
span.end()
|
|
421
436
|
|
|
422
|
-
const durationMs =
|
|
437
|
+
const durationMs = performance.now() - startTimePerfNow
|
|
423
438
|
|
|
424
439
|
this.executionTimes.push(durationMs)
|
|
425
440
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { Effect } from '@livestore/utils/effect'
|
|
2
|
-
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
3
1
|
import { expect } from 'vitest'
|
|
4
2
|
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
import { Effect } from '@livestore/utils/effect'
|
|
5
|
+
|
|
5
6
|
import { makeTodoMvc } from '../utils/tests/fixture.ts'
|
|
6
7
|
import { computed } from './computed.ts'
|
|
7
8
|
import { signal } from './signal.ts'
|
|
8
9
|
|
|
9
10
|
Vitest.describe('signal', () => {
|
|
10
|
-
Vitest.
|
|
11
|
+
Vitest.live('should be able to create a signal', () =>
|
|
11
12
|
Effect.gen(function* () {
|
|
12
13
|
const num$ = signal(0, { label: 'num$' })
|
|
13
14
|
|
|
@@ -23,7 +24,7 @@ Vitest.describe('signal', () => {
|
|
|
23
24
|
}),
|
|
24
25
|
)
|
|
25
26
|
|
|
26
|
-
Vitest.
|
|
27
|
+
Vitest.live('counter example', () =>
|
|
27
28
|
Effect.gen(function* () {
|
|
28
29
|
const count$ = signal(0, { label: 'count$' })
|
|
29
30
|
|
|
@@ -4,8 +4,50 @@ import { nanoid } from '@livestore/utils/nanoid'
|
|
|
4
4
|
import type * as RG from '../reactive.ts'
|
|
5
5
|
import type { RefreshReason } from '../store/store-types.ts'
|
|
6
6
|
import type { ISignal, ReactivityGraph, ReactivityGraphContext, SignalDef } from './base-class.ts'
|
|
7
|
-
import { LiveStoreQueryBase, withRCMap } from './base-class.ts'
|
|
7
|
+
import { isSignalDef, LiveStoreQueryBase, withRCMap } from './base-class.ts'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.
|
|
11
|
+
*
|
|
12
|
+
* Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be
|
|
13
|
+
* synced across clients or stored permanently—such as search filters, selected items, or
|
|
14
|
+
* temporary form values.
|
|
15
|
+
*
|
|
16
|
+
* Unlike database-backed state (via events), signals:
|
|
17
|
+
* - Are not persisted or synced
|
|
18
|
+
* - Exist only for the lifetime of the Store
|
|
19
|
+
* - Can hold any value type (primitives, objects, functions)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // Create a signal for search text
|
|
24
|
+
* const searchText$ = signal('', { label: 'searchText' })
|
|
25
|
+
*
|
|
26
|
+
* // Create a query that depends on the signal
|
|
27
|
+
* const filteredTodos$ = queryDb(
|
|
28
|
+
* (get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
|
|
29
|
+
* { deps: [searchText$] }
|
|
30
|
+
* )
|
|
31
|
+
*
|
|
32
|
+
* // Update the signal (triggers query re-evaluation)
|
|
33
|
+
* store.setSignal(searchText$, 'buy')
|
|
34
|
+
*
|
|
35
|
+
* // Read the current value
|
|
36
|
+
* const results = store.query(filteredTodos$)
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Counter with functional updates
|
|
42
|
+
* const count$ = signal(0, { label: 'count' })
|
|
43
|
+
*
|
|
44
|
+
* store.setSignal(count$, (prev) => prev + 1)
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param defaultValue - Initial value of the signal
|
|
48
|
+
* @param options.label - Human-readable label for debugging and devtools
|
|
49
|
+
* @returns A signal definition that can be used with `store.query()`, `store.setSignal()`, and as a dependency in other queries
|
|
50
|
+
*/
|
|
9
51
|
export const signal = <T>(
|
|
10
52
|
defaultValue: T,
|
|
11
53
|
options?: {
|
|
@@ -28,8 +70,8 @@ export const signal = <T>(
|
|
|
28
70
|
def,
|
|
29
71
|
}),
|
|
30
72
|
),
|
|
31
|
-
[Equal.symbol](that:
|
|
32
|
-
return this.hash === that.hash
|
|
73
|
+
[Equal.symbol](that: Equal.Equal): boolean {
|
|
74
|
+
return isSignalDef(that) && this.hash === that.hash
|
|
33
75
|
},
|
|
34
76
|
[Hash.symbol](): number {
|
|
35
77
|
return Hash.string(this.hash)
|
|
@@ -39,6 +81,13 @@ export const signal = <T>(
|
|
|
39
81
|
return def
|
|
40
82
|
}
|
|
41
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A live signal instance bound to a specific Store.
|
|
86
|
+
*
|
|
87
|
+
* Signal instances are created internally when you use a `SignalDef` with the Store.
|
|
88
|
+
* You typically don't construct these directly—use {@link signal} to create definitions
|
|
89
|
+
* and `store.setSignal()` / `store.query()` to interact with them.
|
|
90
|
+
*/
|
|
42
91
|
export class Signal<T> extends LiveStoreQueryBase<T> implements ISignal<T> {
|
|
43
92
|
_tag = 'signal' as const
|
|
44
93
|
readonly ref: RG.Ref<T, ReactivityGraphContext, RefreshReason>
|