@livestore/livestore 0.4.0-dev.1 → 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.
Files changed (58) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/effect/LiveStore.d.ts.map +1 -1
  3. package/dist/effect/LiveStore.js +2 -4
  4. package/dist/effect/LiveStore.js.map +1 -1
  5. package/dist/live-queries/db-query.d.ts.map +1 -1
  6. package/dist/live-queries/db-query.js +7 -4
  7. package/dist/live-queries/db-query.js.map +1 -1
  8. package/dist/live-queries/db-query.test.js +13 -7
  9. package/dist/live-queries/db-query.test.js.map +1 -1
  10. package/dist/mod.d.ts +1 -1
  11. package/dist/mod.d.ts.map +1 -1
  12. package/dist/mod.js +1 -1
  13. package/dist/mod.js.map +1 -1
  14. package/dist/reactive.d.ts +10 -10
  15. package/dist/reactive.d.ts.map +1 -1
  16. package/dist/reactive.js +36 -27
  17. package/dist/reactive.js.map +1 -1
  18. package/dist/reactive.test.js +115 -0
  19. package/dist/reactive.test.js.map +1 -1
  20. package/dist/store/create-store.d.ts.map +1 -1
  21. package/dist/store/create-store.js +3 -3
  22. package/dist/store/create-store.js.map +1 -1
  23. package/dist/store/store-types.d.ts +2 -2
  24. package/dist/store/store-types.d.ts.map +1 -1
  25. package/dist/store/store-types.js.map +1 -1
  26. package/dist/store/store.d.ts +21 -2
  27. package/dist/store/store.d.ts.map +1 -1
  28. package/dist/store/store.js +134 -88
  29. package/dist/store/store.js.map +1 -1
  30. package/dist/utils/dev.d.ts +3 -0
  31. package/dist/utils/dev.d.ts.map +1 -1
  32. package/dist/utils/dev.js.map +1 -1
  33. package/dist/utils/tests/fixture.d.ts.map +1 -1
  34. package/dist/utils/tests/fixture.js +2 -1
  35. package/dist/utils/tests/fixture.js.map +1 -1
  36. package/dist/utils/tests/otel.d.ts +15 -14
  37. package/dist/utils/tests/otel.d.ts.map +1 -1
  38. package/dist/utils/tests/otel.js +20 -15
  39. package/dist/utils/tests/otel.js.map +1 -1
  40. package/package.json +7 -7
  41. package/src/ambient.d.ts +3 -3
  42. package/src/effect/LiveStore.ts +2 -4
  43. package/src/live-queries/__snapshots__/db-query.test.ts.snap +268 -131
  44. package/src/live-queries/db-query.test.ts +13 -7
  45. package/src/live-queries/db-query.ts +7 -4
  46. package/src/mod.ts +2 -0
  47. package/src/reactive.test.ts +150 -1
  48. package/src/reactive.ts +47 -39
  49. package/src/store/create-store.ts +12 -4
  50. package/src/store/store-types.ts +5 -2
  51. package/src/store/store.ts +204 -145
  52. package/src/utils/dev.ts +5 -0
  53. package/src/utils/tests/fixture.ts +2 -1
  54. package/src/utils/tests/otel.ts +31 -20
  55. package/dist/store/store-shutdown.test.d.ts +0 -2
  56. package/dist/store/store-shutdown.test.d.ts.map +0 -1
  57. package/dist/store/store-shutdown.test.js +0 -103
  58. package/dist/store/store-shutdown.test.js.map +0 -1
@@ -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 { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
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: (eventDecoded, { otelContext, withChangeset, materializerHashLeader }) => {
132
- const { eventDef, materializer } = getEventDef(schema, eventDecoded.name)
133
-
134
- const execArgsArr = getExecStatementsFromMaterializer({
135
- eventDef,
136
- materializer,
137
- dbState: this.sqliteDbWrapper,
138
- event: { decoded: eventDecoded, encoded: undefined },
139
- })
140
-
141
- const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
142
-
143
- if (
144
- materializerHashLeader._tag === 'Some' &&
145
- materializerHash._tag === 'Some' &&
146
- materializerHashLeader.value !== materializerHash.value
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
- // durationMsTotal += durationMs
176
- for (const table of writeTables) {
177
- writeTablesForEvent.add(table)
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
- this.sqliteDbWrapper.debug.head = eventDecoded.seqNum
181
- }
182
- }
183
-
184
- let sessionChangeset:
185
- | { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any }
186
- | { _tag: 'no-op' }
187
- | { _tag: 'unset' } = { _tag: 'unset' }
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
- if (withChangeset === true) {
190
- sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
191
- } else {
192
- exec()
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
- return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
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
- leaderPushBatchSize: params.leaderPushBatchSize,
212
- simulation: params.simulation?.clientSessionSyncProcessor,
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
- return Schema.decodeSync(schema)(rawRes)
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({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
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
- for (const event of events) {
601
- replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
602
- }
603
-
604
- if (events.length === 0) return
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
- 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)
662
+ for (const event of events) {
663
+ replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
664
+ }
630
665
 
631
- try {
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
- if (events.length > 1) {
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
- const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
652
- for (const tableName of writeTables) {
653
- const tableRef = this.tableRefs[tableName]
654
- assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
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
- const debugRefreshReason = {
659
- _tag: 'commit' as const,
660
- events,
661
- writeTables: Array.from(writeTables),
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
- // Update all table refs together in a batch, to only trigger one reactive update
665
- this.reactivityGraph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
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
- durationMs = getDurationMsFromSpan(span)
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
- return { durationMs }
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
- console.log('Leader sync state:', leader.toJSON())
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 })))
@@ -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
- export const getSimplifiedRootSpan = (
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
- type NestedSpan = { span: ReadableSpan; children: NestedSpan[] }
25
- const createStoreSpanData = spans.find((_) => _.name === 'createStore')
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 the root span named 'createStore'. Available spans: ${spans.map((s) => s.name).join(', ')}`,
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
- // console.log('rootSpan', rootSpan.span)
44
-
45
- // console.dir(
46
- // spans.map((_) => [_.spanContext().spanId, _.name, _.attributes, _.parentSpanId]),
47
- // { depth: 10 },
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
- // writeFileSync('tmp/trace.json', JSON.stringify(toTraceFile(spans), null, 2))
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
- return simplifiedRootSpan
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
- ...(span.parentSpanContext?.spanId ? { parentSpanId: span.parentSpanContext.spanId } : {}),
110
+ ...omitUndefineds({ parentSpanId: span.parentSpanContext?.spanId }),
100
111
  // traceState: span.spanContext().traceState ?? '',
101
112
  name: span.name,
102
113
  kind: 'SPAN_KIND_INTERNAL',
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=store-shutdown.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"store-shutdown.test.d.ts","sourceRoot":"","sources":["../../src/store/store-shutdown.test.ts"],"names":[],"mappings":""}