@livestore/livestore 0.0.0-snapshot-8edfbb590201c0bff6943d89f2ebc3d5ba1a09b0 → 0.0.0-snapshot-6788a4cf00171a177a3a1e59cefc10f5bad71049
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/dist/.tsbuildinfo +1 -1
- package/dist/live-queries/db-query.test.js +30 -0
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/store/store-types.d.ts +9 -0
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +20 -24
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +72 -45
- package/dist/store/store.js.map +1 -1
- package/package.json +5 -5
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +87 -0
- package/src/live-queries/db-query.test.ts +47 -0
- package/src/mod.ts +7 -1
- package/src/store/store-types.ts +10 -0
- package/src/store/store.ts +106 -75
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
+
exports[`otel > QueryBuilder subscription - async iterator 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"_name": "createStore",
|
|
6
|
+
"attributes": {
|
|
7
|
+
"debugInstanceId": "test",
|
|
8
|
+
"storeId": "default",
|
|
9
|
+
},
|
|
10
|
+
"children": [
|
|
11
|
+
{
|
|
12
|
+
"_name": "livestore.in-memory-db:execute",
|
|
13
|
+
"attributes": {
|
|
14
|
+
"sql.query": "
|
|
15
|
+
PRAGMA page_size=32768;
|
|
16
|
+
PRAGMA cache_size=10000;
|
|
17
|
+
PRAGMA synchronous='OFF';
|
|
18
|
+
PRAGMA temp_store='MEMORY';
|
|
19
|
+
PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
|
|
20
|
+
",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"_name": "@livestore/common:LeaderSyncProcessor:push",
|
|
25
|
+
"attributes": {
|
|
26
|
+
"batch": "undefined",
|
|
27
|
+
"batchSize": 1,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"_name": "client-session-sync-processor:pull",
|
|
32
|
+
"attributes": {
|
|
33
|
+
"code.stacktrace": "<STACKTRACE>",
|
|
34
|
+
"span.label": "⚠︎ Interrupted",
|
|
35
|
+
"status.interrupted": true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"_name": "LiveStore:sync",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"_name": "LiveStore:commits",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"_name": "LiveStore:queries",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
exports[`otel > QueryBuilder subscription - async iterator 2`] = `
|
|
52
|
+
[
|
|
53
|
+
{
|
|
54
|
+
"_name": "LiveStore:commit",
|
|
55
|
+
"attributes": {
|
|
56
|
+
"livestore.eventTags": "[
|
|
57
|
+
"todo.created"
|
|
58
|
+
]",
|
|
59
|
+
"livestore.eventsCount": 1,
|
|
60
|
+
},
|
|
61
|
+
"children": [
|
|
62
|
+
{
|
|
63
|
+
"_name": "client-session-sync-processor:push",
|
|
64
|
+
"attributes": {
|
|
65
|
+
"batchSize": 1,
|
|
66
|
+
"eventCounts": "{
|
|
67
|
+
"todo.created": 1
|
|
68
|
+
}",
|
|
69
|
+
"mergeResultTag": "advance",
|
|
70
|
+
},
|
|
71
|
+
"children": [
|
|
72
|
+
{
|
|
73
|
+
"_name": "client-session-sync-processor:materialize-event",
|
|
74
|
+
"children": [
|
|
75
|
+
{
|
|
76
|
+
"_name": "livestore.in-memory-db:execute",
|
|
77
|
+
"attributes": {
|
|
78
|
+
"sql.query": "INSERT INTO 'todos' (id, text, completed) VALUES (?, ?, ?)",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
`;
|
|
89
|
+
|
|
3
90
|
exports[`otel > QueryBuilder subscription - basic functionality 1`] = `
|
|
4
91
|
{
|
|
5
92
|
"_name": "createStore",
|
|
@@ -289,6 +289,53 @@ Vitest.describe('otel', () => {
|
|
|
289
289
|
),
|
|
290
290
|
)
|
|
291
291
|
|
|
292
|
+
Vitest.scopedLive('QueryBuilder subscription - async iterator', () =>
|
|
293
|
+
Effect.gen(function* () {
|
|
294
|
+
const { store, exporter, span, provider } = yield* makeQuery
|
|
295
|
+
|
|
296
|
+
const defaultTodo = { id: '', text: '', completed: false }
|
|
297
|
+
|
|
298
|
+
const queryBuilder = tables.todos
|
|
299
|
+
.where({ completed: false })
|
|
300
|
+
.first({ behaviour: 'fallback', fallback: () => defaultTodo })
|
|
301
|
+
|
|
302
|
+
yield* Effect.promise(async () => {
|
|
303
|
+
const iterator = store.subscribe(queryBuilder)
|
|
304
|
+
|
|
305
|
+
const initial = await iterator.next()
|
|
306
|
+
expect(initial.done).toBe(false)
|
|
307
|
+
expect(initial.value).toMatchObject(defaultTodo)
|
|
308
|
+
|
|
309
|
+
store.commit(events.todoCreated({ id: 't-async', text: 'write tests', completed: false }))
|
|
310
|
+
|
|
311
|
+
const update = await iterator.next()
|
|
312
|
+
expect(update.done).toBe(false)
|
|
313
|
+
expect(update.value).toMatchObject({
|
|
314
|
+
id: 't-async',
|
|
315
|
+
text: 'write tests',
|
|
316
|
+
completed: false,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const doneResult = await iterator.return()
|
|
320
|
+
expect(doneResult.done).toBe(true)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
span.end()
|
|
324
|
+
|
|
325
|
+
return { exporter, provider }
|
|
326
|
+
}).pipe(
|
|
327
|
+
Effect.scoped,
|
|
328
|
+
Effect.tap(({ exporter, provider }) =>
|
|
329
|
+
Effect.promise(async () => {
|
|
330
|
+
await provider.forceFlush()
|
|
331
|
+
expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
332
|
+
expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
333
|
+
await provider.shutdown()
|
|
334
|
+
}),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
|
|
292
339
|
Vitest.scopedLive('QueryBuilder subscription - direct table subscription', () =>
|
|
293
340
|
Effect.gen(function* () {
|
|
294
341
|
const { store, exporter, span, provider } = yield* makeQuery
|
package/src/mod.ts
CHANGED
|
@@ -37,7 +37,13 @@ export {
|
|
|
37
37
|
export { emptyDebugInfo, SqliteDbWrapper } from './SqliteDbWrapper.ts'
|
|
38
38
|
export { type CreateStoreOptions, createStore, createStorePromise } from './store/create-store.ts'
|
|
39
39
|
export { Store } from './store/store.ts'
|
|
40
|
-
export type {
|
|
40
|
+
export type {
|
|
41
|
+
OtelOptions,
|
|
42
|
+
QueryDebugInfo,
|
|
43
|
+
RefreshReason,
|
|
44
|
+
SubscribeOptions,
|
|
45
|
+
Unsubscribe,
|
|
46
|
+
} from './store/store-types.ts'
|
|
41
47
|
export {
|
|
42
48
|
type LiveStoreContext,
|
|
43
49
|
type LiveStoreContextRunning,
|
package/src/store/store-types.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { Effect, Runtime, Scope } from '@livestore/utils/effect'
|
|
|
14
14
|
import { Deferred } from '@livestore/utils/effect'
|
|
15
15
|
import type * as otel from '@opentelemetry/api'
|
|
16
16
|
|
|
17
|
+
import type { LiveQuery } from '../live-queries/base-class.ts'
|
|
17
18
|
import type { DebugRefreshReasonBase } from '../reactive.ts'
|
|
18
19
|
import type { StackInfo } from '../utils/stack-info.ts'
|
|
19
20
|
import type { Store } from './store.ts'
|
|
@@ -135,3 +136,12 @@ export type StoreEventsOptions<TSchema extends LiveStoreSchema> = {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export type Unsubscribe = () => void
|
|
139
|
+
|
|
140
|
+
export type SubscribeOptions<TResult> = {
|
|
141
|
+
onSubscribe?: (query$: LiveQuery<TResult>) => void
|
|
142
|
+
onUnsubsubscribe?: () => void
|
|
143
|
+
label?: string
|
|
144
|
+
skipInitialRun?: boolean
|
|
145
|
+
otelContext?: otel.Context
|
|
146
|
+
stackInfo?: StackInfo
|
|
147
|
+
}
|
package/src/store/store.ts
CHANGED
|
@@ -52,16 +52,21 @@ import type { Ref } from '../reactive.ts'
|
|
|
52
52
|
import { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
|
|
53
53
|
import { ReferenceCountedSet } from '../utils/data-structures.ts'
|
|
54
54
|
import { downloadBlob, exposeDebugUtils } from '../utils/dev.ts'
|
|
55
|
-
import type { StackInfo } from '../utils/stack-info.ts'
|
|
56
55
|
import type {
|
|
57
56
|
RefreshReason,
|
|
58
57
|
StoreCommitOptions,
|
|
59
58
|
StoreEventsOptions,
|
|
60
59
|
StoreOptions,
|
|
61
60
|
StoreOtel,
|
|
61
|
+
SubscribeOptions,
|
|
62
62
|
Unsubscribe,
|
|
63
63
|
} from './store-types.ts'
|
|
64
64
|
|
|
65
|
+
type SubscribeQuery<TResult> =
|
|
66
|
+
| LiveQueryDef<TResult, 'def' | 'signal-def'>
|
|
67
|
+
| LiveQuery<TResult>
|
|
68
|
+
| QueryBuilder<TResult, any, any>
|
|
69
|
+
|
|
65
70
|
if (isDevEnv()) {
|
|
66
71
|
exposeDebugUtils()
|
|
67
72
|
}
|
|
@@ -349,98 +354,125 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
/**
|
|
352
|
-
* Subscribe to the results of a query
|
|
353
|
-
*
|
|
357
|
+
* Subscribe to the results of a query.
|
|
358
|
+
*
|
|
359
|
+
* - When providing an `onUpdate` callback it returns an {@link Unsubscribe} function.
|
|
360
|
+
* - Without a callback it returns an {@link AsyncIterable} that yields query results.
|
|
354
361
|
*
|
|
355
362
|
* @example
|
|
356
363
|
* ```ts
|
|
357
364
|
* const unsubscribe = store.subscribe(query$, (result) => console.log(result))
|
|
358
365
|
* ```
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```ts
|
|
369
|
+
* for await (const result of store.subscribe(query$)) {
|
|
370
|
+
* console.log(result)
|
|
371
|
+
* }
|
|
372
|
+
* ```
|
|
359
373
|
*/
|
|
360
|
-
subscribe
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
374
|
+
subscribe: {
|
|
375
|
+
<TResult>(
|
|
376
|
+
query: SubscribeQuery<TResult>,
|
|
377
|
+
onUpdate: (value: TResult) => void,
|
|
378
|
+
options?: SubscribeOptions<TResult>,
|
|
379
|
+
): Unsubscribe
|
|
380
|
+
<TResult>(query: SubscribeQuery<TResult>, options?: SubscribeOptions<TResult>): AsyncIterableIterator<TResult>
|
|
381
|
+
} = <TResult>(
|
|
382
|
+
query: SubscribeQuery<TResult>,
|
|
383
|
+
onUpdateOrOptions?: ((value: TResult) => void) | SubscribeOptions<TResult>,
|
|
384
|
+
maybeOptions?: SubscribeOptions<TResult>,
|
|
385
|
+
): Unsubscribe | AsyncIterableIterator<TResult> => {
|
|
386
|
+
if (typeof onUpdateOrOptions === 'function') {
|
|
387
|
+
const onUpdate = onUpdateOrOptions
|
|
388
|
+
const options = maybeOptions
|
|
389
|
+
|
|
390
|
+
this.checkShutdown('subscribe')
|
|
391
|
+
|
|
392
|
+
return this.otel.tracer.startActiveSpan(
|
|
393
|
+
`LiveStore.subscribe`,
|
|
394
|
+
{ attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } },
|
|
395
|
+
options?.otelContext ?? this.otel.queriesSpanContext,
|
|
396
|
+
(span) => {
|
|
397
|
+
// console.debug('store sub', query$.id, query$.label)
|
|
398
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
399
|
+
|
|
400
|
+
const queryRcRef = isQueryBuilder(query)
|
|
401
|
+
? queryDb(query).make(this.reactivityGraph.context!)
|
|
402
|
+
: query._tag === 'def' || query._tag === 'signal-def'
|
|
403
|
+
? query.make(this.reactivityGraph.context!)
|
|
404
|
+
: {
|
|
405
|
+
value: query as LiveQuery<TResult>,
|
|
406
|
+
deref: () => {},
|
|
407
|
+
}
|
|
408
|
+
const query$ = queryRcRef.value
|
|
380
409
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// console.debug('store sub', query$.id, query$.label)
|
|
387
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
410
|
+
const label = `subscribe:${options?.label}`
|
|
411
|
+
const effect = this.reactivityGraph.makeEffect(
|
|
412
|
+
(get, _otelContext, debugRefreshReason) => onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
|
|
413
|
+
{ label },
|
|
414
|
+
)
|
|
388
415
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
? query.make(this.reactivityGraph.context!)
|
|
393
|
-
: {
|
|
394
|
-
value: query as LiveQuery<TResult>,
|
|
395
|
-
deref: () => {},
|
|
396
|
-
}
|
|
397
|
-
const query$ = queryRcRef.value
|
|
416
|
+
if (options?.stackInfo) {
|
|
417
|
+
query$.activeSubscriptions.add(options.stackInfo)
|
|
418
|
+
}
|
|
398
419
|
|
|
399
|
-
|
|
400
|
-
const effect = this.reactivityGraph.makeEffect(
|
|
401
|
-
(get, _otelContext, debugRefreshReason) => onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
|
|
402
|
-
{ label },
|
|
403
|
-
)
|
|
420
|
+
options?.onSubscribe?.(query$)
|
|
404
421
|
|
|
405
|
-
|
|
406
|
-
query$.activeSubscriptions.add(options.stackInfo)
|
|
407
|
-
}
|
|
422
|
+
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
|
408
423
|
|
|
409
|
-
|
|
424
|
+
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
|
425
|
+
if (options?.skipInitialRun !== true && !query$.isDestroyed) {
|
|
426
|
+
effect.doEffect(otelContext, {
|
|
427
|
+
_tag: 'subscribe.initial',
|
|
428
|
+
label: `subscribe-initial-run:${options?.label}`,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
410
431
|
|
|
411
|
-
|
|
432
|
+
const unsubscribe = () => {
|
|
433
|
+
// console.debug('store unsub', query$.id, query$.label)
|
|
434
|
+
try {
|
|
435
|
+
this.reactivityGraph.destroyNode(effect)
|
|
436
|
+
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
412
437
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
438
|
+
if (options?.stackInfo) {
|
|
439
|
+
query$.activeSubscriptions.delete(options.stackInfo)
|
|
440
|
+
}
|
|
417
441
|
|
|
418
|
-
|
|
419
|
-
// console.debug('store unsub', query$.id, query$.label)
|
|
420
|
-
try {
|
|
421
|
-
this.reactivityGraph.destroyNode(effect)
|
|
422
|
-
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
442
|
+
queryRcRef.deref()
|
|
423
443
|
|
|
424
|
-
|
|
425
|
-
|
|
444
|
+
options?.onUnsubsubscribe?.()
|
|
445
|
+
} finally {
|
|
446
|
+
span.end()
|
|
426
447
|
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return unsubscribe
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
}
|
|
427
454
|
|
|
428
|
-
|
|
455
|
+
return this.subscribeAsAsyncIterator(query, onUpdateOrOptions)
|
|
456
|
+
}
|
|
429
457
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
private subscribeAsAsyncIterator = <TResult>(
|
|
459
|
+
query: SubscribeQuery<TResult>,
|
|
460
|
+
options?: SubscribeOptions<TResult>,
|
|
461
|
+
): AsyncIterableIterator<TResult> => {
|
|
462
|
+
this.checkShutdown('subscribe')
|
|
463
|
+
|
|
464
|
+
const iterator = Stream.toAsyncIterable(this.subscribeStream(query, options))[Symbol.asyncIterator]()
|
|
435
465
|
|
|
436
|
-
|
|
466
|
+
return Object.assign(iterator, {
|
|
467
|
+
[Symbol.asyncIterator]() {
|
|
468
|
+
return this
|
|
437
469
|
},
|
|
438
|
-
)
|
|
470
|
+
})
|
|
439
471
|
}
|
|
440
472
|
|
|
441
473
|
subscribeStream = <TResult>(
|
|
442
|
-
query
|
|
443
|
-
options?:
|
|
474
|
+
query: SubscribeQuery<TResult>,
|
|
475
|
+
options?: SubscribeOptions<TResult>,
|
|
444
476
|
): Stream.Stream<TResult> =>
|
|
445
477
|
Stream.asyncPush<TResult>((emit) =>
|
|
446
478
|
Effect.gen(this, function* () {
|
|
@@ -451,11 +483,10 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
451
483
|
|
|
452
484
|
yield* Effect.acquireRelease(
|
|
453
485
|
Effect.sync(() =>
|
|
454
|
-
this.subscribe(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
),
|
|
486
|
+
this.subscribe(query, (result) => emit.single(result), {
|
|
487
|
+
...(options ?? {}),
|
|
488
|
+
otelContext,
|
|
489
|
+
}),
|
|
459
490
|
),
|
|
460
491
|
(unsub) => Effect.sync(() => unsub()),
|
|
461
492
|
)
|