@livestore/livestore 0.4.0-dev.1 → 0.4.0-dev.11
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/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +2 -4
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +7 -4
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +53 -24
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -1
- 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 +36 -27
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +115 -0
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +3 -3
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/store-types.d.ts +13 -2
- 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 +45 -29
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +165 -100
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.d.ts +3 -0
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +2 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts +15 -14
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +20 -15
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +7 -7
- package/src/ambient.d.ts +3 -3
- package/src/effect/LiveStore.ts +2 -4
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +354 -130
- package/src/live-queries/base-class.ts +6 -3
- package/src/live-queries/db-query.test.ts +70 -24
- package/src/live-queries/db-query.ts +7 -4
- package/src/mod.ts +10 -1
- package/src/reactive.test.ts +150 -1
- package/src/reactive.ts +47 -39
- package/src/store/create-store.ts +12 -4
- package/src/store/store-types.ts +23 -2
- package/src/store/store.ts +262 -193
- package/src/utils/dev.ts +5 -0
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +31 -20
- package/dist/store/store-shutdown.test.d.ts +0 -2
- package/dist/store/store-shutdown.test.d.ts.map +0 -1
- package/dist/store/store-shutdown.test.js +0 -103
- package/dist/store/store-shutdown.test.js.map +0 -1
package/src/store/store.ts
CHANGED
|
@@ -3,24 +3,24 @@ import {
|
|
|
3
3
|
type ClientSession,
|
|
4
4
|
type ClientSessionSyncProcessor,
|
|
5
5
|
Devtools,
|
|
6
|
-
getDurationMsFromSpan,
|
|
7
6
|
getExecStatementsFromMaterializer,
|
|
8
7
|
getResultSchema,
|
|
9
8
|
hashMaterializerResults,
|
|
10
9
|
IntentionalShutdownCause,
|
|
11
10
|
isQueryBuilder,
|
|
12
11
|
liveStoreVersion,
|
|
12
|
+
MaterializeError,
|
|
13
|
+
MaterializerHashMismatchError,
|
|
13
14
|
makeClientSessionSyncProcessor,
|
|
14
15
|
type PreparedBindValues,
|
|
15
16
|
prepareBindValues,
|
|
16
|
-
type QueryBuilder,
|
|
17
17
|
QueryBuilderAstSymbol,
|
|
18
18
|
replaceSessionIdSymbol,
|
|
19
19
|
UnexpectedError,
|
|
20
20
|
} from '@livestore/common'
|
|
21
21
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
22
|
-
import {
|
|
23
|
-
import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
|
|
22
|
+
import { LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
|
|
23
|
+
import { assertNever, isDevEnv, notYetImplemented, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
24
24
|
import type { Scope } from '@livestore/utils/effect'
|
|
25
25
|
import {
|
|
26
26
|
Cause,
|
|
@@ -37,13 +37,7 @@ import {
|
|
|
37
37
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
38
38
|
import * as otel from '@opentelemetry/api'
|
|
39
39
|
|
|
40
|
-
import type {
|
|
41
|
-
LiveQuery,
|
|
42
|
-
LiveQueryDef,
|
|
43
|
-
ReactivityGraph,
|
|
44
|
-
ReactivityGraphContext,
|
|
45
|
-
SignalDef,
|
|
46
|
-
} from '../live-queries/base-class.ts'
|
|
40
|
+
import type { LiveQuery, ReactivityGraph, ReactivityGraphContext, SignalDef } from '../live-queries/base-class.ts'
|
|
47
41
|
import { makeReactivityGraph } from '../live-queries/base-class.ts'
|
|
48
42
|
import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.ts'
|
|
49
43
|
import { queryDb } from '../live-queries/db-query.ts'
|
|
@@ -51,16 +45,26 @@ import type { Ref } from '../reactive.ts'
|
|
|
51
45
|
import { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
|
|
52
46
|
import { ReferenceCountedSet } from '../utils/data-structures.ts'
|
|
53
47
|
import { downloadBlob, exposeDebugUtils } from '../utils/dev.ts'
|
|
54
|
-
import type { StackInfo } from '../utils/stack-info.ts'
|
|
55
48
|
import type {
|
|
49
|
+
Queryable,
|
|
56
50
|
RefreshReason,
|
|
57
51
|
StoreCommitOptions,
|
|
58
52
|
StoreEventsOptions,
|
|
59
53
|
StoreOptions,
|
|
60
54
|
StoreOtel,
|
|
55
|
+
SubscribeOptions,
|
|
61
56
|
Unsubscribe,
|
|
62
57
|
} from './store-types.ts'
|
|
63
58
|
|
|
59
|
+
type SubscribeFn = {
|
|
60
|
+
<TResult>(
|
|
61
|
+
query: Queryable<TResult>,
|
|
62
|
+
onUpdate: (value: TResult) => void,
|
|
63
|
+
options?: SubscribeOptions<TResult>,
|
|
64
|
+
): Unsubscribe
|
|
65
|
+
<TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): AsyncIterable<TResult>
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
if (isDevEnv()) {
|
|
65
69
|
exposeDebugUtils()
|
|
66
70
|
}
|
|
@@ -73,6 +77,24 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
73
77
|
schema: LiveStoreSchema
|
|
74
78
|
context: TContext
|
|
75
79
|
otel: StoreOtel
|
|
80
|
+
/**
|
|
81
|
+
* Reactive connectivity updates emitted by the backing sync backend.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { Effect, Stream } from 'effect'
|
|
86
|
+
*
|
|
87
|
+
* const status = await store.networkStatus.pipe(Effect.runPromise)
|
|
88
|
+
*
|
|
89
|
+
* await store.networkStatus.changes.pipe(
|
|
90
|
+
* Stream.tap((next) => console.log('network status update', next)),
|
|
91
|
+
* Stream.runDrain,
|
|
92
|
+
* Effect.scoped,
|
|
93
|
+
* Effect.runPromise,
|
|
94
|
+
* )
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
readonly networkStatus: ClientSession['leaderThread']['networkStatus']
|
|
76
98
|
/**
|
|
77
99
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
78
100
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
@@ -117,6 +139,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
117
139
|
this.clientSession = clientSession
|
|
118
140
|
this.schema = schema
|
|
119
141
|
this.context = context
|
|
142
|
+
this.networkStatus = clientSession.leaderThread.networkStatus
|
|
120
143
|
|
|
121
144
|
this.effectContext = effectContext
|
|
122
145
|
|
|
@@ -128,72 +151,91 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
128
151
|
schema,
|
|
129
152
|
clientSession,
|
|
130
153
|
runtime: effectContext.runtime,
|
|
131
|
-
materializeEvent: (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
void this.shutdown(
|
|
149
|
-
Cause.fail(
|
|
150
|
-
UnexpectedError.make({
|
|
151
|
-
cause: `Materializer hash mismatch detected for event "${eventDecoded.name}".`,
|
|
152
|
-
note: `Please make sure your event materializer is a pure function without side effects.`,
|
|
153
|
-
}),
|
|
154
|
-
),
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const writeTablesForEvent = new Set<string>()
|
|
159
|
-
|
|
160
|
-
const exec = () => {
|
|
161
|
-
for (const {
|
|
162
|
-
statementSql,
|
|
163
|
-
bindValues,
|
|
164
|
-
writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
|
|
165
|
-
} of execArgsArr) {
|
|
166
|
-
try {
|
|
167
|
-
this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, { otelContext, writeTables })
|
|
168
|
-
} catch (cause) {
|
|
169
|
-
throw UnexpectedError.make({
|
|
170
|
-
cause,
|
|
171
|
-
note: `Error executing materializer for event "${eventDecoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
|
|
172
|
-
})
|
|
154
|
+
materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
|
|
155
|
+
(eventEncoded, { withChangeset, materializerHashLeader }) =>
|
|
156
|
+
// We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
|
|
157
|
+
Effect.gen(this, function* () {
|
|
158
|
+
const resolution = yield* resolveEventDef(schema, {
|
|
159
|
+
operation: '@livestore/livestore:store:materializeEvent',
|
|
160
|
+
event: eventEncoded,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (resolution._tag === 'unknown') {
|
|
164
|
+
// Runtime schema doesn't know this event yet; skip materialization but
|
|
165
|
+
// keep the log entry so upgraded clients can replay it later.
|
|
166
|
+
return {
|
|
167
|
+
writeTables: new Set<string>(),
|
|
168
|
+
sessionChangeset: { _tag: 'no-op' as const },
|
|
169
|
+
materializerHash: Option.none(),
|
|
170
|
+
}
|
|
173
171
|
}
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
const { eventDef, materializer } = resolution
|
|
174
|
+
|
|
175
|
+
const execArgsArr = getExecStatementsFromMaterializer({
|
|
176
|
+
eventDef,
|
|
177
|
+
materializer,
|
|
178
|
+
dbState: this.sqliteDbWrapper,
|
|
179
|
+
event: { decoded: undefined, encoded: eventEncoded },
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
|
|
183
|
+
|
|
184
|
+
// Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
|
|
185
|
+
// During push path (local commits), materializerHashLeader is always Option.none(), so this condition
|
|
186
|
+
// will never be met. The check happens when the same event comes back from the leader during sync,
|
|
187
|
+
// allowing us to compare the leader's computed hash with our local re-materialization hash.
|
|
188
|
+
if (
|
|
189
|
+
materializerHashLeader._tag === 'Some' &&
|
|
190
|
+
materializerHash._tag === 'Some' &&
|
|
191
|
+
materializerHashLeader.value !== materializerHash.value
|
|
192
|
+
) {
|
|
193
|
+
return yield* MaterializerHashMismatchError.make({ eventName: eventEncoded.name })
|
|
178
194
|
}
|
|
179
195
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
197
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
198
|
+
|
|
199
|
+
const writeTablesForEvent = new Set<string>()
|
|
200
|
+
|
|
201
|
+
const exec = () => {
|
|
202
|
+
for (const {
|
|
203
|
+
statementSql,
|
|
204
|
+
bindValues,
|
|
205
|
+
writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
|
|
206
|
+
} of execArgsArr) {
|
|
207
|
+
try {
|
|
208
|
+
this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, { otelContext, writeTables })
|
|
209
|
+
} catch (cause) {
|
|
210
|
+
// TOOD refactor with `SqliteError`
|
|
211
|
+
throw UnexpectedError.make({
|
|
212
|
+
cause,
|
|
213
|
+
note: `Error executing materializer for event "${eventEncoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// durationMsTotal += durationMs
|
|
218
|
+
for (const table of writeTables) {
|
|
219
|
+
writeTablesForEvent.add(table)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.sqliteDbWrapper.debug.head = eventEncoded.seqNum
|
|
223
|
+
}
|
|
224
|
+
}
|
|
188
225
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
226
|
+
let sessionChangeset:
|
|
227
|
+
| { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any }
|
|
228
|
+
| { _tag: 'no-op' }
|
|
229
|
+
| { _tag: 'unset' } = { _tag: 'unset' }
|
|
230
|
+
if (withChangeset === true) {
|
|
231
|
+
sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
|
|
232
|
+
} else {
|
|
233
|
+
exec()
|
|
234
|
+
}
|
|
194
235
|
|
|
195
|
-
|
|
196
|
-
|
|
236
|
+
return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
|
|
237
|
+
}).pipe(Effect.mapError((cause) => MaterializeError.make({ cause }))),
|
|
238
|
+
),
|
|
197
239
|
rollback: (changeset) => {
|
|
198
240
|
this.sqliteDbWrapper.rollback(changeset)
|
|
199
241
|
},
|
|
@@ -208,8 +250,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
208
250
|
},
|
|
209
251
|
span: syncSpan,
|
|
210
252
|
params: {
|
|
211
|
-
|
|
212
|
-
|
|
253
|
+
...omitUndefineds({
|
|
254
|
+
leaderPushBatchSize: params.leaderPushBatchSize,
|
|
255
|
+
}),
|
|
256
|
+
...(params.simulation?.clientSessionSyncProcessor !== undefined
|
|
257
|
+
? { simulation: params.simulation.clientSessionSyncProcessor }
|
|
258
|
+
: {}),
|
|
213
259
|
},
|
|
214
260
|
confirmUnsavedChanges,
|
|
215
261
|
})
|
|
@@ -306,32 +352,39 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
306
352
|
}
|
|
307
353
|
|
|
308
354
|
/**
|
|
309
|
-
* Subscribe to the results of a query
|
|
310
|
-
*
|
|
355
|
+
* Subscribe to the results of a query.
|
|
356
|
+
*
|
|
357
|
+
* - When providing an `onUpdate` callback it returns an {@link Unsubscribe} function.
|
|
358
|
+
* - Without a callback it returns an {@link AsyncIterable} that yields query results.
|
|
311
359
|
*
|
|
312
360
|
* @example
|
|
313
361
|
* ```ts
|
|
314
|
-
* const unsubscribe = store.subscribe(query$,
|
|
362
|
+
* const unsubscribe = store.subscribe(query$, (result) => console.log(result))
|
|
363
|
+
* ```
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* for await (const result of store.subscribe(query$)) {
|
|
368
|
+
* console.log(result)
|
|
369
|
+
* }
|
|
315
370
|
* ```
|
|
316
371
|
*/
|
|
317
|
-
subscribe = <TResult>(
|
|
318
|
-
query:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
stackInfo?: StackInfo
|
|
334
|
-
},
|
|
372
|
+
subscribe = (<TResult>(
|
|
373
|
+
query: Queryable<TResult>,
|
|
374
|
+
onUpdateOrOptions?: ((value: TResult) => void) | SubscribeOptions<TResult>,
|
|
375
|
+
maybeOptions?: SubscribeOptions<TResult>,
|
|
376
|
+
): Unsubscribe | AsyncIterable<TResult> => {
|
|
377
|
+
if (typeof onUpdateOrOptions === 'function') {
|
|
378
|
+
return this.subscribeWithCallback(query, onUpdateOrOptions, maybeOptions)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return this.subscribeAsAsyncIterable(query, onUpdateOrOptions)
|
|
382
|
+
}) as SubscribeFn
|
|
383
|
+
|
|
384
|
+
private subscribeWithCallback = <TResult>(
|
|
385
|
+
query: Queryable<TResult>,
|
|
386
|
+
onUpdate: (value: TResult) => void,
|
|
387
|
+
options?: SubscribeOptions<TResult>,
|
|
335
388
|
): Unsubscribe => {
|
|
336
389
|
this.checkShutdown('subscribe')
|
|
337
390
|
|
|
@@ -340,7 +393,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
340
393
|
{ attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } },
|
|
341
394
|
options?.otelContext ?? this.otel.queriesSpanContext,
|
|
342
395
|
(span) => {
|
|
343
|
-
// console.debug('store sub', query$.id, query$.label)
|
|
344
396
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
345
397
|
|
|
346
398
|
const queryRcRef = isQueryBuilder(query)
|
|
@@ -355,8 +407,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
355
407
|
|
|
356
408
|
const label = `subscribe:${options?.label}`
|
|
357
409
|
const effect = this.reactivityGraph.makeEffect(
|
|
358
|
-
(get, _otelContext, debugRefreshReason) =>
|
|
359
|
-
options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
|
|
410
|
+
(get, _otelContext, debugRefreshReason) => onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
|
|
360
411
|
{ label },
|
|
361
412
|
)
|
|
362
413
|
|
|
@@ -368,13 +419,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
368
419
|
|
|
369
420
|
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
|
370
421
|
|
|
371
|
-
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
|
372
422
|
if (options?.skipInitialRun !== true && !query$.isDestroyed) {
|
|
373
|
-
effect.doEffect(otelContext, {
|
|
423
|
+
effect.doEffect(otelContext, {
|
|
424
|
+
_tag: 'subscribe.initial',
|
|
425
|
+
label: `subscribe-initial-run:${options?.label}`,
|
|
426
|
+
})
|
|
374
427
|
}
|
|
375
428
|
|
|
376
429
|
const unsubscribe = () => {
|
|
377
|
-
// console.debug('store unsub', query$.id, query$.label)
|
|
378
430
|
try {
|
|
379
431
|
this.reactivityGraph.destroyNode(effect)
|
|
380
432
|
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
@@ -396,10 +448,16 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
396
448
|
)
|
|
397
449
|
}
|
|
398
450
|
|
|
399
|
-
|
|
400
|
-
query
|
|
401
|
-
options?:
|
|
402
|
-
):
|
|
451
|
+
private subscribeAsAsyncIterable = <TResult>(
|
|
452
|
+
query: Queryable<TResult>,
|
|
453
|
+
options?: SubscribeOptions<TResult>,
|
|
454
|
+
): AsyncIterable<TResult> => {
|
|
455
|
+
this.checkShutdown('subscribe')
|
|
456
|
+
|
|
457
|
+
return Stream.toAsyncIterable(this.subscribeStream(query, options))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
subscribeStream = <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): Stream.Stream<TResult> =>
|
|
403
461
|
Stream.asyncPush<TResult>((emit) =>
|
|
404
462
|
Effect.gen(this, function* () {
|
|
405
463
|
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
|
|
@@ -409,10 +467,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
409
467
|
|
|
410
468
|
yield* Effect.acquireRelease(
|
|
411
469
|
Effect.sync(() =>
|
|
412
|
-
this.subscribe(query
|
|
413
|
-
|
|
470
|
+
this.subscribe(query, (result) => emit.single(result), {
|
|
471
|
+
...(options ?? {}),
|
|
414
472
|
otelContext,
|
|
415
|
-
label: options?.label,
|
|
416
473
|
}),
|
|
417
474
|
),
|
|
418
475
|
(unsub) => Effect.sync(() => unsub()),
|
|
@@ -435,19 +492,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
435
492
|
* ```
|
|
436
493
|
*/
|
|
437
494
|
query = <TResult>(
|
|
438
|
-
query:
|
|
439
|
-
| QueryBuilder<TResult, any, any>
|
|
440
|
-
| LiveQuery<TResult>
|
|
441
|
-
| LiveQueryDef<TResult>
|
|
442
|
-
| SignalDef<TResult>
|
|
443
|
-
| { query: string; bindValues: Bindable; schema?: Schema.Schema<TResult> },
|
|
495
|
+
query: Queryable<TResult> | { query: string; bindValues: Bindable; schema?: Schema.Schema<TResult> },
|
|
444
496
|
options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
|
|
445
497
|
): TResult => {
|
|
446
498
|
this.checkShutdown('query')
|
|
447
499
|
|
|
448
500
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
|
449
501
|
const res = this.sqliteDbWrapper.cachedSelect(query.query, prepareBindValues(query.bindValues, query.query), {
|
|
450
|
-
otelContext: options?.otelContext,
|
|
502
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
451
503
|
}) as any
|
|
452
504
|
if (query.schema) {
|
|
453
505
|
return Schema.decodeSync(query.schema)(res)
|
|
@@ -473,11 +525,23 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
473
525
|
}
|
|
474
526
|
|
|
475
527
|
const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
|
|
476
|
-
otelContext: options?.otelContext,
|
|
528
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
477
529
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
|
478
530
|
})
|
|
479
531
|
|
|
480
|
-
|
|
532
|
+
const decodeResult = Schema.decodeEither(schema)(rawRes)
|
|
533
|
+
if (decodeResult._tag === 'Right') {
|
|
534
|
+
return decodeResult.right
|
|
535
|
+
} else {
|
|
536
|
+
return shouldNeverHappen(
|
|
537
|
+
`Failed to decode query result with for schema:`,
|
|
538
|
+
schema.toString(),
|
|
539
|
+
'raw result:',
|
|
540
|
+
rawRes,
|
|
541
|
+
'decode error:',
|
|
542
|
+
decodeResult.left,
|
|
543
|
+
)
|
|
544
|
+
}
|
|
481
545
|
} else if (query._tag === 'def') {
|
|
482
546
|
const query$ = query.make(this.reactivityGraph.context!)
|
|
483
547
|
const result = this.query(query$.value, options)
|
|
@@ -487,7 +551,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
487
551
|
const signal$ = query.make(this.reactivityGraph.context!)
|
|
488
552
|
return signal$.value.get()
|
|
489
553
|
} else {
|
|
490
|
-
return query.run({
|
|
554
|
+
return query.run({
|
|
555
|
+
...omitUndefineds({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason }),
|
|
556
|
+
})
|
|
491
557
|
}
|
|
492
558
|
}
|
|
493
559
|
|
|
@@ -597,84 +663,76 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
597
663
|
|
|
598
664
|
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
|
|
599
665
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
666
|
+
Effect.gen(this, function* () {
|
|
667
|
+
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)
|
|
668
|
+
commitsSpan?.addEvent('commit')
|
|
669
|
+
const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
670
|
+
commitsSpan?.addLink({ context: currentSpan.spanContext() })
|
|
605
671
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
commitsSpan.addEvent('commit')
|
|
610
|
-
|
|
611
|
-
// console.group('LiveStore.commit', { skipRefresh })
|
|
612
|
-
// events.forEach((_) => console.debug(_.name, _.args))
|
|
613
|
-
// console.groupEnd()
|
|
614
|
-
|
|
615
|
-
let durationMs: number
|
|
616
|
-
|
|
617
|
-
return this.otel.tracer.startActiveSpan(
|
|
618
|
-
'LiveStore:commit',
|
|
619
|
-
{
|
|
620
|
-
attributes: {
|
|
621
|
-
'livestore.eventsCount': events.length,
|
|
622
|
-
'livestore.eventTags': events.map((_) => _.name),
|
|
623
|
-
'livestore.commitLabel': options?.label,
|
|
624
|
-
},
|
|
625
|
-
links: options?.spanLinks,
|
|
626
|
-
},
|
|
627
|
-
options?.otelContext ?? this.otel.commitsSpanContext,
|
|
628
|
-
(span) => {
|
|
629
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
672
|
+
for (const event of events) {
|
|
673
|
+
replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
|
|
674
|
+
}
|
|
630
675
|
|
|
631
|
-
|
|
632
|
-
// Materialize events to state
|
|
633
|
-
const { writeTables } = (() => {
|
|
634
|
-
try {
|
|
635
|
-
const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
|
|
676
|
+
if (events.length === 0) return
|
|
636
677
|
|
|
637
|
-
|
|
638
|
-
return this.sqliteDbWrapper.txn(materializeEvents)
|
|
639
|
-
} else {
|
|
640
|
-
return materializeEvents()
|
|
641
|
-
}
|
|
642
|
-
} catch (e: any) {
|
|
643
|
-
console.error(e)
|
|
644
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
645
|
-
throw e
|
|
646
|
-
} finally {
|
|
647
|
-
span.end()
|
|
648
|
-
}
|
|
649
|
-
})()
|
|
678
|
+
const localRuntime = yield* Effect.runtime()
|
|
650
679
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
tablesToUpdate.push([tableRef!, null])
|
|
680
|
+
const materializeEventsTx = Effect.try({
|
|
681
|
+
try: () => {
|
|
682
|
+
const runMaterializeEvents = () => {
|
|
683
|
+
return this.syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
|
|
656
684
|
}
|
|
657
685
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
686
|
+
if (events.length > 1) {
|
|
687
|
+
return this.sqliteDbWrapper.txn(runMaterializeEvents)
|
|
688
|
+
} else {
|
|
689
|
+
return runMaterializeEvents()
|
|
662
690
|
}
|
|
691
|
+
},
|
|
692
|
+
catch: (cause) => UnexpectedError.make({ cause }),
|
|
693
|
+
})
|
|
663
694
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} catch (e: any) {
|
|
667
|
-
console.error(e)
|
|
668
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
669
|
-
throw e
|
|
670
|
-
} finally {
|
|
671
|
-
span.end()
|
|
695
|
+
// Materialize events to state
|
|
696
|
+
const { writeTables } = yield* materializeEventsTx
|
|
672
697
|
|
|
673
|
-
|
|
674
|
-
|
|
698
|
+
const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
|
|
699
|
+
for (const tableName of writeTables) {
|
|
700
|
+
const tableRef = this.tableRefs[tableName]
|
|
701
|
+
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
702
|
+
tablesToUpdate.push([tableRef!, null])
|
|
703
|
+
}
|
|
675
704
|
|
|
676
|
-
|
|
677
|
-
|
|
705
|
+
const debugRefreshReason: RefreshReason = {
|
|
706
|
+
_tag: 'commit',
|
|
707
|
+
events,
|
|
708
|
+
writeTables: Array.from(writeTables),
|
|
709
|
+
}
|
|
710
|
+
const skipRefresh = options?.skipRefresh ?? false
|
|
711
|
+
|
|
712
|
+
// Update all table refs together in a batch, to only trigger one reactive update
|
|
713
|
+
this.reactivityGraph.setRefs(tablesToUpdate, {
|
|
714
|
+
debugRefreshReason,
|
|
715
|
+
skipRefresh,
|
|
716
|
+
otelContext: otel.trace.setSpan(otel.context.active(), currentSpan),
|
|
717
|
+
})
|
|
718
|
+
}).pipe(
|
|
719
|
+
Effect.withSpan('LiveStore:commit', {
|
|
720
|
+
root: true,
|
|
721
|
+
attributes: {
|
|
722
|
+
'livestore.eventsCount': events.length,
|
|
723
|
+
'livestore.eventTags': events.map((_) => _.name),
|
|
724
|
+
...(options?.label && { 'livestore.commitLabel': options.label }),
|
|
725
|
+
},
|
|
726
|
+
links: [
|
|
727
|
+
// Span link to LiveStore:commits
|
|
728
|
+
OtelTracer.makeSpanLink({ context: otel.trace.getSpanContext(this.otel.commitsSpanContext)! }),
|
|
729
|
+
// User-provided span links
|
|
730
|
+
...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
|
|
731
|
+
],
|
|
732
|
+
}),
|
|
733
|
+
Effect.tapErrorCause(Effect.logError),
|
|
734
|
+
Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))),
|
|
735
|
+
Runtime.runSync(this.effectContext.runtime),
|
|
678
736
|
)
|
|
679
737
|
}
|
|
680
738
|
// #endregion commit
|
|
@@ -746,9 +804,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
746
804
|
*
|
|
747
805
|
* This is called automatically when the store was created using the React or Effect API.
|
|
748
806
|
*/
|
|
749
|
-
shutdown = (cause?: Cause.Cause<UnexpectedError>): Effect.Effect<void> => {
|
|
750
|
-
this.checkShutdown('shutdown')
|
|
751
|
-
|
|
807
|
+
shutdown = (cause?: Cause.Cause<UnexpectedError | MaterializeError>): Effect.Effect<void> => {
|
|
752
808
|
this.isShutdown = true
|
|
753
809
|
return this.clientSession.shutdown(
|
|
754
810
|
cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
|
|
@@ -798,12 +854,22 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
798
854
|
.pipe(this.runEffectFork)
|
|
799
855
|
},
|
|
800
856
|
|
|
801
|
-
syncStates: () =>
|
|
857
|
+
syncStates: () =>
|
|
802
858
|
Effect.gen(this, function* () {
|
|
803
859
|
const session = yield* this.syncProcessor.syncState
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
860
|
+
const leader = yield* this.clientSession.leaderThread.syncState
|
|
861
|
+
return { session, leader }
|
|
862
|
+
}).pipe(this.runEffectPromise),
|
|
863
|
+
|
|
864
|
+
printSyncStates: () => {
|
|
865
|
+
Effect.gen(this, function* () {
|
|
866
|
+
const session = yield* this.syncProcessor.syncState
|
|
867
|
+
yield* Effect.log(
|
|
868
|
+
`Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`,
|
|
869
|
+
session.toJSON(),
|
|
870
|
+
)
|
|
871
|
+
const leader = yield* this.clientSession.leaderThread.syncState
|
|
872
|
+
yield* Effect.log(`Leader sync state: ${leader.localHead} (upstream: ${leader.upstreamHead})`, leader.toJSON())
|
|
807
873
|
}).pipe(this.runEffectFork)
|
|
808
874
|
},
|
|
809
875
|
|
|
@@ -827,6 +893,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
827
893
|
Runtime.runFork(this.effectContext.runtime),
|
|
828
894
|
)
|
|
829
895
|
|
|
896
|
+
private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
|
897
|
+
effect.pipe(Effect.tapCauseLogPretty, Runtime.runPromise(this.effectContext.runtime))
|
|
898
|
+
|
|
830
899
|
private getCommitArgs = (
|
|
831
900
|
firstEventOrTxnFnOrOptions: any,
|
|
832
901
|
restEvents: any[],
|
package/src/utils/dev.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { SqliteDb } from '@livestore/common'
|
|
|
2
2
|
import { prettyBytes } from '@livestore/utils'
|
|
3
3
|
import { Effect } from '@livestore/utils/effect'
|
|
4
4
|
|
|
5
|
+
declare global {
|
|
6
|
+
// declaring a global *value* is the least fussy when augmenting inline
|
|
7
|
+
var __debugLiveStoreUtils: any
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export const downloadBlob = (
|
|
6
11
|
data: Uint8Array<ArrayBuffer> | Blob | string,
|
|
7
12
|
fileName: string,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
2
|
import { provideOtel } from '@livestore/common'
|
|
3
3
|
import { createStore, Events, makeSchema, State } from '@livestore/livestore'
|
|
4
|
+
import { omitUndefineds } from '@livestore/utils'
|
|
4
5
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
5
6
|
import type * as otel from '@opentelemetry/api'
|
|
6
7
|
|
|
@@ -71,4 +72,4 @@ export const makeTodoMvc = ({
|
|
|
71
72
|
})
|
|
72
73
|
|
|
73
74
|
return store
|
|
74
|
-
}).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer: otelTracer }))
|
|
75
|
+
}).pipe(provideOtel(omitUndefineds({ parentSpanContext: otelContext, otelTracer: otelTracer })))
|