@livestore/livestore 0.4.0-dev.0 → 0.4.0-dev.10
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/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 +13 -7
- 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 +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 +2 -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 +21 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +134 -88
- 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 +268 -131
- package/src/live-queries/db-query.test.ts +13 -7
- package/src/live-queries/db-query.ts +7 -4
- package/src/mod.ts +2 -0
- 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 +5 -2
- package/src/store/store.ts +204 -145
- 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,13 +3,14 @@ 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,
|
@@ -19,8 +20,8 @@ import {
|
|
19
20
|
UnexpectedError,
|
20
21
|
} from '@livestore/common'
|
21
22
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
22
|
-
import {
|
23
|
-
import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
|
23
|
+
import { LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
|
24
|
+
import { assertNever, isDevEnv, notYetImplemented, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
24
25
|
import type { Scope } from '@livestore/utils/effect'
|
25
26
|
import {
|
26
27
|
Cause,
|
@@ -73,6 +74,24 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
73
74
|
schema: LiveStoreSchema
|
74
75
|
context: TContext
|
75
76
|
otel: StoreOtel
|
77
|
+
/**
|
78
|
+
* Reactive connectivity updates emitted by the backing sync backend.
|
79
|
+
*
|
80
|
+
* @example
|
81
|
+
* ```ts
|
82
|
+
* import { Effect, Stream } from 'effect'
|
83
|
+
*
|
84
|
+
* const status = await store.networkStatus.pipe(Effect.runPromise)
|
85
|
+
*
|
86
|
+
* await store.networkStatus.changes.pipe(
|
87
|
+
* Stream.tap((next) => console.log('network status update', next)),
|
88
|
+
* Stream.runDrain,
|
89
|
+
* Effect.scoped,
|
90
|
+
* Effect.runPromise,
|
91
|
+
* )
|
92
|
+
* ```
|
93
|
+
*/
|
94
|
+
readonly networkStatus: ClientSession['leaderThread']['networkStatus']
|
76
95
|
/**
|
77
96
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
78
97
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
@@ -117,6 +136,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
117
136
|
this.clientSession = clientSession
|
118
137
|
this.schema = schema
|
119
138
|
this.context = context
|
139
|
+
this.networkStatus = clientSession.leaderThread.networkStatus
|
120
140
|
|
121
141
|
this.effectContext = effectContext
|
122
142
|
|
@@ -128,72 +148,91 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
128
148
|
schema,
|
129
149
|
clientSession,
|
130
150
|
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
|
-
})
|
151
|
+
materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
|
152
|
+
(eventEncoded, { withChangeset, materializerHashLeader }) =>
|
153
|
+
// We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
|
154
|
+
Effect.gen(this, function* () {
|
155
|
+
const resolution = yield* resolveEventDef(schema, {
|
156
|
+
operation: '@livestore/livestore:store:materializeEvent',
|
157
|
+
event: eventEncoded,
|
158
|
+
})
|
159
|
+
|
160
|
+
if (resolution._tag === 'unknown') {
|
161
|
+
// Runtime schema doesn't know this event yet; skip materialization but
|
162
|
+
// keep the log entry so upgraded clients can replay it later.
|
163
|
+
return {
|
164
|
+
writeTables: new Set<string>(),
|
165
|
+
sessionChangeset: { _tag: 'no-op' as const },
|
166
|
+
materializerHash: Option.none(),
|
167
|
+
}
|
173
168
|
}
|
174
169
|
|
175
|
-
|
176
|
-
|
177
|
-
|
170
|
+
const { eventDef, materializer } = resolution
|
171
|
+
|
172
|
+
const execArgsArr = getExecStatementsFromMaterializer({
|
173
|
+
eventDef,
|
174
|
+
materializer,
|
175
|
+
dbState: this.sqliteDbWrapper,
|
176
|
+
event: { decoded: undefined, encoded: eventEncoded },
|
177
|
+
})
|
178
|
+
|
179
|
+
const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
|
180
|
+
|
181
|
+
// Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
|
182
|
+
// During push path (local commits), materializerHashLeader is always Option.none(), so this condition
|
183
|
+
// will never be met. The check happens when the same event comes back from the leader during sync,
|
184
|
+
// allowing us to compare the leader's computed hash with our local re-materialization hash.
|
185
|
+
if (
|
186
|
+
materializerHashLeader._tag === 'Some' &&
|
187
|
+
materializerHash._tag === 'Some' &&
|
188
|
+
materializerHashLeader.value !== materializerHash.value
|
189
|
+
) {
|
190
|
+
return yield* MaterializerHashMismatchError.make({ eventName: eventEncoded.name })
|
178
191
|
}
|
179
192
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
193
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
194
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
195
|
+
|
196
|
+
const writeTablesForEvent = new Set<string>()
|
197
|
+
|
198
|
+
const exec = () => {
|
199
|
+
for (const {
|
200
|
+
statementSql,
|
201
|
+
bindValues,
|
202
|
+
writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
|
203
|
+
} of execArgsArr) {
|
204
|
+
try {
|
205
|
+
this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, { otelContext, writeTables })
|
206
|
+
} catch (cause) {
|
207
|
+
// TOOD refactor with `SqliteError`
|
208
|
+
throw UnexpectedError.make({
|
209
|
+
cause,
|
210
|
+
note: `Error executing materializer for event "${eventEncoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
|
211
|
+
})
|
212
|
+
}
|
213
|
+
|
214
|
+
// durationMsTotal += durationMs
|
215
|
+
for (const table of writeTables) {
|
216
|
+
writeTablesForEvent.add(table)
|
217
|
+
}
|
218
|
+
|
219
|
+
this.sqliteDbWrapper.debug.head = eventEncoded.seqNum
|
220
|
+
}
|
221
|
+
}
|
188
222
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
223
|
+
let sessionChangeset:
|
224
|
+
| { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any }
|
225
|
+
| { _tag: 'no-op' }
|
226
|
+
| { _tag: 'unset' } = { _tag: 'unset' }
|
227
|
+
if (withChangeset === true) {
|
228
|
+
sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
|
229
|
+
} else {
|
230
|
+
exec()
|
231
|
+
}
|
194
232
|
|
195
|
-
|
196
|
-
|
233
|
+
return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
|
234
|
+
}).pipe(Effect.mapError((cause) => MaterializeError.make({ cause }))),
|
235
|
+
),
|
197
236
|
rollback: (changeset) => {
|
198
237
|
this.sqliteDbWrapper.rollback(changeset)
|
199
238
|
},
|
@@ -208,8 +247,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
208
247
|
},
|
209
248
|
span: syncSpan,
|
210
249
|
params: {
|
211
|
-
|
212
|
-
|
250
|
+
...omitUndefineds({
|
251
|
+
leaderPushBatchSize: params.leaderPushBatchSize,
|
252
|
+
}),
|
253
|
+
...(params.simulation?.clientSessionSyncProcessor !== undefined
|
254
|
+
? { simulation: params.simulation.clientSessionSyncProcessor }
|
255
|
+
: {}),
|
213
256
|
},
|
214
257
|
confirmUnsavedChanges,
|
215
258
|
})
|
@@ -411,8 +454,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
411
454
|
Effect.sync(() =>
|
412
455
|
this.subscribe(query$, {
|
413
456
|
onUpdate: (result) => emit.single(result),
|
414
|
-
otelContext,
|
415
|
-
label: options?.label,
|
457
|
+
...omitUndefineds({ otelContext, label: options?.label }),
|
416
458
|
}),
|
417
459
|
),
|
418
460
|
(unsub) => Effect.sync(() => unsub()),
|
@@ -447,7 +489,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
447
489
|
|
448
490
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
449
491
|
const res = this.sqliteDbWrapper.cachedSelect(query.query, prepareBindValues(query.bindValues, query.query), {
|
450
|
-
otelContext: options?.otelContext,
|
492
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
451
493
|
}) as any
|
452
494
|
if (query.schema) {
|
453
495
|
return Schema.decodeSync(query.schema)(res)
|
@@ -473,11 +515,23 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
473
515
|
}
|
474
516
|
|
475
517
|
const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
|
476
|
-
otelContext: options?.otelContext,
|
518
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
477
519
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
478
520
|
})
|
479
521
|
|
480
|
-
|
522
|
+
const decodeResult = Schema.decodeEither(schema)(rawRes)
|
523
|
+
if (decodeResult._tag === 'Right') {
|
524
|
+
return decodeResult.right
|
525
|
+
} else {
|
526
|
+
return shouldNeverHappen(
|
527
|
+
`Failed to decode query result with for schema:`,
|
528
|
+
schema.toString(),
|
529
|
+
'raw result:',
|
530
|
+
rawRes,
|
531
|
+
'decode error:',
|
532
|
+
decodeResult.left,
|
533
|
+
)
|
534
|
+
}
|
481
535
|
} else if (query._tag === 'def') {
|
482
536
|
const query$ = query.make(this.reactivityGraph.context!)
|
483
537
|
const result = this.query(query$.value, options)
|
@@ -487,7 +541,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
487
541
|
const signal$ = query.make(this.reactivityGraph.context!)
|
488
542
|
return signal$.value.get()
|
489
543
|
} else {
|
490
|
-
return query.run({
|
544
|
+
return query.run({
|
545
|
+
...omitUndefineds({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason }),
|
546
|
+
})
|
491
547
|
}
|
492
548
|
}
|
493
549
|
|
@@ -597,84 +653,76 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
597
653
|
|
598
654
|
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
|
599
655
|
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
const skipRefresh = options?.skipRefresh ?? false
|
607
|
-
|
608
|
-
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)!
|
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
|
656
|
+
Effect.gen(this, function* () {
|
657
|
+
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)
|
658
|
+
commitsSpan?.addEvent('commit')
|
659
|
+
const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
660
|
+
commitsSpan?.addLink({ context: currentSpan.spanContext() })
|
616
661
|
|
617
|
-
|
618
|
-
|
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)
|
662
|
+
for (const event of events) {
|
663
|
+
replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
|
664
|
+
}
|
630
665
|
|
631
|
-
|
632
|
-
// Materialize events to state
|
633
|
-
const { writeTables } = (() => {
|
634
|
-
try {
|
635
|
-
const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
|
666
|
+
if (events.length === 0) return
|
636
667
|
|
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
|
-
})()
|
668
|
+
const localRuntime = yield* Effect.runtime()
|
650
669
|
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
tablesToUpdate.push([tableRef!, null])
|
670
|
+
const materializeEventsTx = Effect.try({
|
671
|
+
try: () => {
|
672
|
+
const runMaterializeEvents = () => {
|
673
|
+
return this.syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
|
656
674
|
}
|
657
675
|
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
676
|
+
if (events.length > 1) {
|
677
|
+
return this.sqliteDbWrapper.txn(runMaterializeEvents)
|
678
|
+
} else {
|
679
|
+
return runMaterializeEvents()
|
662
680
|
}
|
681
|
+
},
|
682
|
+
catch: (cause) => UnexpectedError.make({ cause }),
|
683
|
+
})
|
663
684
|
|
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()
|
685
|
+
// Materialize events to state
|
686
|
+
const { writeTables } = yield* materializeEventsTx
|
672
687
|
|
673
|
-
|
674
|
-
|
688
|
+
const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
|
689
|
+
for (const tableName of writeTables) {
|
690
|
+
const tableRef = this.tableRefs[tableName]
|
691
|
+
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
692
|
+
tablesToUpdate.push([tableRef!, null])
|
693
|
+
}
|
675
694
|
|
676
|
-
|
677
|
-
|
695
|
+
const debugRefreshReason: RefreshReason = {
|
696
|
+
_tag: 'commit',
|
697
|
+
events,
|
698
|
+
writeTables: Array.from(writeTables),
|
699
|
+
}
|
700
|
+
const skipRefresh = options?.skipRefresh ?? false
|
701
|
+
|
702
|
+
// Update all table refs together in a batch, to only trigger one reactive update
|
703
|
+
this.reactivityGraph.setRefs(tablesToUpdate, {
|
704
|
+
debugRefreshReason,
|
705
|
+
skipRefresh,
|
706
|
+
otelContext: otel.trace.setSpan(otel.context.active(), currentSpan),
|
707
|
+
})
|
708
|
+
}).pipe(
|
709
|
+
Effect.withSpan('LiveStore:commit', {
|
710
|
+
root: true,
|
711
|
+
attributes: {
|
712
|
+
'livestore.eventsCount': events.length,
|
713
|
+
'livestore.eventTags': events.map((_) => _.name),
|
714
|
+
...(options?.label && { 'livestore.commitLabel': options.label }),
|
715
|
+
},
|
716
|
+
links: [
|
717
|
+
// Span link to LiveStore:commits
|
718
|
+
OtelTracer.makeSpanLink({ context: otel.trace.getSpanContext(this.otel.commitsSpanContext)! }),
|
719
|
+
// User-provided span links
|
720
|
+
...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
|
721
|
+
],
|
722
|
+
}),
|
723
|
+
Effect.tapErrorCause(Effect.logError),
|
724
|
+
Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))),
|
725
|
+
Runtime.runSync(this.effectContext.runtime),
|
678
726
|
)
|
679
727
|
}
|
680
728
|
// #endregion commit
|
@@ -746,9 +794,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
746
794
|
*
|
747
795
|
* This is called automatically when the store was created using the React or Effect API.
|
748
796
|
*/
|
749
|
-
shutdown = (cause?: Cause.Cause<UnexpectedError>): Effect.Effect<void> => {
|
750
|
-
this.checkShutdown('shutdown')
|
751
|
-
|
797
|
+
shutdown = (cause?: Cause.Cause<UnexpectedError | MaterializeError>): Effect.Effect<void> => {
|
752
798
|
this.isShutdown = true
|
753
799
|
return this.clientSession.shutdown(
|
754
800
|
cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
|
@@ -798,12 +844,22 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
798
844
|
.pipe(this.runEffectFork)
|
799
845
|
},
|
800
846
|
|
801
|
-
syncStates: () =>
|
847
|
+
syncStates: () =>
|
802
848
|
Effect.gen(this, function* () {
|
803
849
|
const session = yield* this.syncProcessor.syncState
|
804
|
-
console.log('Session sync state:', session.toJSON())
|
805
850
|
const leader = yield* this.clientSession.leaderThread.getSyncState
|
806
|
-
|
851
|
+
return { session, leader }
|
852
|
+
}).pipe(this.runEffectPromise),
|
853
|
+
|
854
|
+
printSyncStates: () => {
|
855
|
+
Effect.gen(this, function* () {
|
856
|
+
const session = yield* this.syncProcessor.syncState
|
857
|
+
yield* Effect.log(
|
858
|
+
`Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`,
|
859
|
+
session.toJSON(),
|
860
|
+
)
|
861
|
+
const leader = yield* this.clientSession.leaderThread.getSyncState
|
862
|
+
yield* Effect.log(`Leader sync state: ${leader.localHead} (upstream: ${leader.upstreamHead})`, leader.toJSON())
|
807
863
|
}).pipe(this.runEffectFork)
|
808
864
|
},
|
809
865
|
|
@@ -827,6 +883,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
827
883
|
Runtime.runFork(this.effectContext.runtime),
|
828
884
|
)
|
829
885
|
|
886
|
+
private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
887
|
+
effect.pipe(Effect.tapCauseLogPretty, Runtime.runPromise(this.effectContext.runtime))
|
888
|
+
|
830
889
|
private getCommitArgs = (
|
831
890
|
firstEventOrTxnFnOrOptions: any,
|
832
891
|
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 })))
|
package/src/utils/tests/otel.ts
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
+
import { omitUndefineds } from '@livestore/utils'
|
1
2
|
import { identity } from '@livestore/utils/effect'
|
2
3
|
import type { Attributes } from '@opentelemetry/api'
|
3
4
|
import type { InMemorySpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
4
5
|
|
5
6
|
type SimplifiedNestedSpan = { _name: string; attributes: any; children: SimplifiedNestedSpan[] }
|
6
7
|
|
7
|
-
|
8
|
+
type NestedSpan = { span: ReadableSpan; children: NestedSpan[] }
|
9
|
+
|
10
|
+
const buildSimplifiedRootSpans = (
|
8
11
|
exporter: InMemorySpanExporter,
|
12
|
+
rootSpanName: string,
|
9
13
|
mapAttributes?: (attributes: Attributes) => Attributes,
|
10
|
-
): SimplifiedNestedSpan => {
|
14
|
+
): SimplifiedNestedSpan[] => {
|
11
15
|
const spans = exporter.getFinishedSpans()
|
12
16
|
const spansMap = new Map<string, NestedSpan>(spans.map((span) => [span.spanContext().spanId, { span, children: [] }]))
|
13
17
|
|
@@ -21,14 +25,12 @@ export const getSimplifiedRootSpan = (
|
|
21
25
|
}
|
22
26
|
})
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
if (createStoreSpanData === undefined) {
|
28
|
+
const rootSpanDataList = spans.filter((_) => _.name === rootSpanName)
|
29
|
+
if (rootSpanDataList.length === 0) {
|
27
30
|
throw new Error(
|
28
|
-
`Could not find
|
31
|
+
`Could not find any root spans named '${rootSpanName}'. Available spans: ${spans.map((s) => s.name).join(', ')}`,
|
29
32
|
)
|
30
33
|
}
|
31
|
-
const rootSpan = spansMap.get(createStoreSpanData.spanContext().spanId)!
|
32
34
|
|
33
35
|
const simplifySpanRec = (span: NestedSpan): SimplifiedNestedSpan =>
|
34
36
|
omitEmpty({
|
@@ -40,20 +42,29 @@ export const getSimplifiedRootSpan = (
|
|
40
42
|
.map(simplifySpanRec),
|
41
43
|
})
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
// )
|
49
|
-
|
50
|
-
const simplifiedRootSpan = simplifySpanRec(rootSpan)
|
51
|
-
|
52
|
-
// console.log('simplifiedRootSpan', simplifiedRootSpan)
|
45
|
+
return rootSpanDataList.map((rootSpanData) => {
|
46
|
+
const rootSpan = spansMap.get(rootSpanData.spanContext().spanId)!
|
47
|
+
return simplifySpanRec(rootSpan)
|
48
|
+
})
|
49
|
+
}
|
53
50
|
|
54
|
-
|
51
|
+
export const getSimplifiedRootSpan = (
|
52
|
+
exporter: InMemorySpanExporter,
|
53
|
+
rootSpanName: string,
|
54
|
+
mapAttributes?: (attributes: Attributes) => Attributes,
|
55
|
+
): SimplifiedNestedSpan => {
|
56
|
+
const results = buildSimplifiedRootSpans(exporter, rootSpanName, mapAttributes)
|
57
|
+
const firstResult = results[0]
|
58
|
+
if (!firstResult) throw new Error(`Could not find the root span named '${rootSpanName}'.`)
|
59
|
+
return firstResult
|
60
|
+
}
|
55
61
|
|
56
|
-
|
62
|
+
export const getAllSimplifiedRootSpans = (
|
63
|
+
exporter: InMemorySpanExporter,
|
64
|
+
rootSpanName: string,
|
65
|
+
mapAttributes?: (attributes: Attributes) => Attributes,
|
66
|
+
): SimplifiedNestedSpan[] => {
|
67
|
+
return buildSimplifiedRootSpans(exporter, rootSpanName, mapAttributes)
|
57
68
|
}
|
58
69
|
|
59
70
|
// const compareHrTime = (a: [number, numndber], b: [number, number]) => {
|
@@ -96,7 +107,7 @@ export const toTraceFile = (spans: ReadableSpan[]) => {
|
|
96
107
|
spans: spans.map((span) => ({
|
97
108
|
traceId: span.spanContext().traceId,
|
98
109
|
spanId: span.spanContext().spanId,
|
99
|
-
...(
|
110
|
+
...omitUndefineds({ parentSpanId: span.parentSpanContext?.spanId }),
|
100
111
|
// traceState: span.spanContext().traceState ?? '',
|
101
112
|
name: span.name,
|
102
113
|
kind: 'SPAN_KIND_INTERNAL',
|
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"store-shutdown.test.d.ts","sourceRoot":"","sources":["../../src/store/store-shutdown.test.ts"],"names":[],"mappings":""}
|