@livestore/livestore 0.4.0-dev.2 → 0.4.0-dev.5

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/src/reactive.ts CHANGED
@@ -211,10 +211,8 @@ export class ReactiveGraph<
211
211
 
212
212
  private refreshCallbacks: Set<() => void> = new Set()
213
213
 
214
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for debugging
215
214
  private nodeIdCounter = 0
216
215
  private uniqueNodeId = () => `node-${++this.nodeIdCounter}`
217
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for debugging
218
216
  private refreshInfoIdCounter = 0
219
217
  private uniqueRefreshInfoId = () => `refresh-info-${++this.refreshInfoIdCounter}`
220
218
 
@@ -267,8 +265,11 @@ export class ReactiveGraph<
267
265
  computeResult: (otelContext, debugRefreshReason) => {
268
266
  if (thunk.isDirty) {
269
267
  const neededCurrentRefresh = this.currentDebugRefresh === undefined
268
+ let localDebugRefresh: { refreshedAtoms: any[]; startMs: number } | undefined
270
269
  if (neededCurrentRefresh) {
271
- this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
270
+ // Use local variable to prevent corruption from nested computations
271
+ localDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
272
+ this.currentDebugRefresh = localDebugRefresh
272
273
  }
273
274
 
274
275
  // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
@@ -300,15 +301,20 @@ export class ReactiveGraph<
300
301
  debugInfo: debugInfo ?? (unknownRefreshReason() as TDebugThunkInfo),
301
302
  } satisfies AtomDebugInfo<TDebugThunkInfo>
302
303
 
303
- this.currentDebugRefresh!.refreshedAtoms.push(debugInfoForAtom)
304
+ // Use currentDebugRefresh if available (could be from parent or local)
305
+ const debugRefresh = localDebugRefresh ?? this.currentDebugRefresh
306
+ if (debugRefresh) {
307
+ debugRefresh.refreshedAtoms.push(debugInfoForAtom)
308
+ }
304
309
 
305
310
  thunk.isDirty = false
306
311
  thunk.previousResult = result
307
312
  thunk.recomputations++
308
313
 
309
- if (neededCurrentRefresh) {
310
- const refreshedAtoms = this.currentDebugRefresh!.refreshedAtoms
311
- const durationMs = performance.now() - this.currentDebugRefresh!.startMs
314
+ if (neededCurrentRefresh && localDebugRefresh) {
315
+ // Use local reference which can't be corrupted by nested calls
316
+ const refreshedAtoms = localDebugRefresh.refreshedAtoms
317
+ const durationMs = performance.now() - localDebugRefresh.startMs
312
318
  this.currentDebugRefresh = undefined
313
319
 
314
320
  this.debugRefreshInfos.push({
@@ -475,14 +481,17 @@ export class ReactiveGraph<
475
481
  ) => {
476
482
  const effectsWrapper = this.context?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
477
483
  effectsWrapper(() => {
478
- this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
484
+ // Capture debug state in local variable to prevent corruption from nested runEffects
485
+ const localDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
486
+ this.currentDebugRefresh = localDebugRefresh
479
487
 
480
488
  for (const effect of effectsToRefresh) {
481
489
  effect.doEffect(options?.otelContext, options.debugRefreshReason)
482
490
  }
483
491
 
484
- const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
485
- const durationMs = performance.now() - this.currentDebugRefresh.startMs
492
+ // Use local reference which can't be corrupted by nested calls
493
+ const refreshedAtoms = localDebugRefresh.refreshedAtoms
494
+ const durationMs = performance.now() - localDebugRefresh.startMs
486
495
  this.currentDebugRefresh = undefined
487
496
 
488
497
  const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
@@ -5,6 +5,9 @@ import {
5
5
  type ClientSessionDevtoolsChannel,
6
6
  type ClientSessionSyncProcessorSimulationParams,
7
7
  type IntentionalShutdownCause,
8
+ type InvalidPullError,
9
+ type IsOfflineError,
10
+ type MaterializeError,
8
11
  type MigrationsReport,
9
12
  provideOtel,
10
13
  type SyncError,
@@ -217,7 +220,12 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
217
220
 
218
221
  const runtime = yield* Effect.runtime<Scope.Scope>()
219
222
 
220
- const shutdown = (exit: Exit.Exit<IntentionalShutdownCause, UnexpectedError | SyncError>) =>
223
+ const shutdown = (
224
+ exit: Exit.Exit<
225
+ IntentionalShutdownCause,
226
+ UnexpectedError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
227
+ >,
228
+ ) =>
221
229
  Effect.gen(function* () {
222
230
  yield* Scope.close(lifetimeScope, exit).pipe(
223
231
  Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
@@ -2,6 +2,9 @@ import type {
2
2
  ClientSession,
3
3
  ClientSessionSyncProcessorSimulationParams,
4
4
  IntentionalShutdownCause,
5
+ InvalidPullError,
6
+ IsOfflineError,
7
+ MaterializeError,
5
8
  StoreInterrupted,
6
9
  SyncError,
7
10
  UnexpectedError,
@@ -28,11 +31,11 @@ export type LiveStoreContext =
28
31
 
29
32
  export type ShutdownDeferred = Deferred.Deferred<
30
33
  IntentionalShutdownCause,
31
- UnexpectedError | SyncError | StoreInterrupted
34
+ UnexpectedError | SyncError | StoreInterrupted | MaterializeError | InvalidPullError | IsOfflineError
32
35
  >
33
36
  export const makeShutdownDeferred: Effect.Effect<ShutdownDeferred> = Deferred.make<
34
37
  IntentionalShutdownCause,
35
- UnexpectedError | SyncError | StoreInterrupted
38
+ UnexpectedError | SyncError | StoreInterrupted | MaterializeError | InvalidPullError | IsOfflineError
36
39
  >()
37
40
 
38
41
  export type LiveStoreContextRunning = {
@@ -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,
@@ -20,7 +21,7 @@ import {
20
21
  } from '@livestore/common'
21
22
  import type { LiveStoreSchema } from '@livestore/common/schema'
22
23
  import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
23
- import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
24
+ import { assertNever, isDevEnv, notYetImplemented, shouldNeverHappen } from '@livestore/utils'
24
25
  import type { Scope } from '@livestore/utils/effect'
25
26
  import {
26
27
  Cause,
@@ -128,72 +129,76 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
128
129
  schema,
129
130
  clientSession,
130
131
  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
- })
132
+ materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
133
+ (eventDecoded, { withChangeset, materializerHashLeader }) =>
134
+ // We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
135
+ Effect.gen(this, function* () {
136
+ const { eventDef, materializer } = getEventDef(schema, eventDecoded.name)
137
+
138
+ const execArgsArr = getExecStatementsFromMaterializer({
139
+ eventDef,
140
+ materializer,
141
+ dbState: this.sqliteDbWrapper,
142
+ event: { decoded: eventDecoded, encoded: undefined },
143
+ })
144
+
145
+ const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
146
+
147
+ // Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
148
+ // During push path (local commits), materializerHashLeader is always Option.none(), so this condition
149
+ // will never be met. The check happens when the same event comes back from the leader during sync,
150
+ // allowing us to compare the leader's computed hash with our local re-materialization hash.
151
+ if (
152
+ materializerHashLeader._tag === 'Some' &&
153
+ materializerHash._tag === 'Some' &&
154
+ materializerHashLeader.value !== materializerHash.value
155
+ ) {
156
+ return yield* MaterializerHashMismatchError.make({ eventName: eventDecoded.name })
173
157
  }
174
158
 
175
- // durationMsTotal += durationMs
176
- for (const table of writeTables) {
177
- writeTablesForEvent.add(table)
159
+ const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
160
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
161
+
162
+ const writeTablesForEvent = new Set<string>()
163
+
164
+ const exec = () => {
165
+ for (const {
166
+ statementSql,
167
+ bindValues,
168
+ writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
169
+ } of execArgsArr) {
170
+ try {
171
+ this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, { otelContext, writeTables })
172
+ } catch (cause) {
173
+ // TOOD refactor with `SqliteError`
174
+ throw UnexpectedError.make({
175
+ cause,
176
+ note: `Error executing materializer for event "${eventDecoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
177
+ })
178
+ }
179
+
180
+ // durationMsTotal += durationMs
181
+ for (const table of writeTables) {
182
+ writeTablesForEvent.add(table)
183
+ }
184
+
185
+ this.sqliteDbWrapper.debug.head = eventDecoded.seqNum
186
+ }
178
187
  }
179
188
 
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' }
188
-
189
- if (withChangeset === true) {
190
- sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
191
- } else {
192
- exec()
193
- }
189
+ let sessionChangeset:
190
+ | { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any }
191
+ | { _tag: 'no-op' }
192
+ | { _tag: 'unset' } = { _tag: 'unset' }
193
+ if (withChangeset === true) {
194
+ sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
195
+ } else {
196
+ exec()
197
+ }
194
198
 
195
- return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
196
- },
199
+ return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
200
+ }).pipe(Effect.mapError((cause) => MaterializeError.make({ cause }))),
201
+ ),
197
202
  rollback: (changeset) => {
198
203
  this.sqliteDbWrapper.rollback(changeset)
199
204
  },
@@ -477,7 +482,19 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
477
482
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
478
483
  })
479
484
 
480
- return Schema.decodeSync(schema)(rawRes)
485
+ const decodeResult = Schema.decodeEither(schema)(rawRes)
486
+ if (decodeResult._tag === 'Right') {
487
+ return decodeResult.right
488
+ } else {
489
+ return shouldNeverHappen(
490
+ `Failed to decode query result with for schema:`,
491
+ schema.toString(),
492
+ 'raw result:',
493
+ rawRes,
494
+ 'decode error:',
495
+ decodeResult.left,
496
+ )
497
+ }
481
498
  } else if (query._tag === 'def') {
482
499
  const query$ = query.make(this.reactivityGraph.context!)
483
500
  const result = this.query(query$.value, options)
@@ -597,84 +614,76 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
597
614
 
598
615
  const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
599
616
 
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')
617
+ Effect.gen(this, function* () {
618
+ const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)
619
+ commitsSpan?.addEvent('commit')
620
+ const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
621
+ commitsSpan?.addLink({ context: currentSpan.spanContext() })
610
622
 
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)
623
+ for (const event of events) {
624
+ replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
625
+ }
630
626
 
631
- try {
632
- // Materialize events to state
633
- const { writeTables } = (() => {
634
- try {
635
- const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
627
+ if (events.length === 0) return
636
628
 
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
- })()
629
+ const localRuntime = yield* Effect.runtime()
650
630
 
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])
631
+ const materializeEventsTx = Effect.try({
632
+ try: () => {
633
+ const runMaterializeEvents = () => {
634
+ return this.syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
656
635
  }
657
636
 
658
- const debugRefreshReason = {
659
- _tag: 'commit' as const,
660
- events,
661
- writeTables: Array.from(writeTables),
637
+ if (events.length > 1) {
638
+ return this.sqliteDbWrapper.txn(runMaterializeEvents)
639
+ } else {
640
+ return runMaterializeEvents()
662
641
  }
642
+ },
643
+ catch: (cause) => UnexpectedError.make({ cause }),
644
+ })
663
645
 
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()
646
+ // Materialize events to state
647
+ const { writeTables } = yield* materializeEventsTx
672
648
 
673
- durationMs = getDurationMsFromSpan(span)
674
- }
649
+ const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
650
+ for (const tableName of writeTables) {
651
+ const tableRef = this.tableRefs[tableName]
652
+ assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
653
+ tablesToUpdate.push([tableRef!, null])
654
+ }
675
655
 
676
- return { durationMs }
677
- },
656
+ const debugRefreshReason: RefreshReason = {
657
+ _tag: 'commit',
658
+ events,
659
+ writeTables: Array.from(writeTables),
660
+ }
661
+ const skipRefresh = options?.skipRefresh ?? false
662
+
663
+ // Update all table refs together in a batch, to only trigger one reactive update
664
+ this.reactivityGraph.setRefs(tablesToUpdate, {
665
+ debugRefreshReason,
666
+ skipRefresh,
667
+ otelContext: otel.trace.setSpan(otel.context.active(), currentSpan),
668
+ })
669
+ }).pipe(
670
+ Effect.withSpan('LiveStore:commit', {
671
+ root: true,
672
+ attributes: {
673
+ 'livestore.eventsCount': events.length,
674
+ 'livestore.eventTags': events.map((_) => _.name),
675
+ ...(options?.label && { 'livestore.commitLabel': options.label }),
676
+ },
677
+ links: [
678
+ // Span link to LiveStore:commits
679
+ OtelTracer.makeSpanLink({ context: otel.trace.getSpanContext(this.otel.commitsSpanContext)! }),
680
+ // User-provided span links
681
+ ...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
682
+ ],
683
+ }),
684
+ Effect.tapErrorCause(Effect.logError),
685
+ Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))),
686
+ Runtime.runSync(this.effectContext.runtime),
678
687
  )
679
688
  }
680
689
  // #endregion commit
@@ -746,9 +755,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
746
755
  *
747
756
  * This is called automatically when the store was created using the React or Effect API.
748
757
  */
749
- shutdown = (cause?: Cause.Cause<UnexpectedError>): Effect.Effect<void> => {
750
- this.checkShutdown('shutdown')
751
-
758
+ shutdown = (cause?: Cause.Cause<UnexpectedError | MaterializeError>): Effect.Effect<void> => {
752
759
  this.isShutdown = true
753
760
  return this.clientSession.shutdown(
754
761
  cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
@@ -798,14 +805,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
798
805
  .pipe(this.runEffectFork)
799
806
  },
800
807
 
801
- syncStates: () => {
808
+ syncStates: () =>
802
809
  Effect.gen(this, function* () {
803
810
  const session = yield* this.syncProcessor.syncState
804
- console.log('Session sync state:', session.toJSON())
805
811
  const leader = yield* this.clientSession.leaderThread.getSyncState
806
- console.log('Leader sync state:', leader.toJSON())
807
- }).pipe(this.runEffectFork)
808
- },
812
+ return { session, leader }
813
+ }).pipe(this.runEffectPromise),
809
814
 
810
815
  version: liveStoreVersion,
811
816
 
@@ -827,6 +832,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
827
832
  Runtime.runFork(this.effectContext.runtime),
828
833
  )
829
834
 
835
+ private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
836
+ effect.pipe(Effect.tapCauseLogPretty, Runtime.runPromise(this.effectContext.runtime))
837
+
830
838
  private getCommitArgs = (
831
839
  firstEventOrTxnFnOrOptions: any,
832
840
  restEvents: any[],
@@ -4,10 +4,13 @@ import type { InMemorySpanExporter, ReadableSpan } from '@opentelemetry/sdk-trac
4
4
 
5
5
  type SimplifiedNestedSpan = { _name: string; attributes: any; children: SimplifiedNestedSpan[] }
6
6
 
7
- export const getSimplifiedRootSpan = (
7
+ type NestedSpan = { span: ReadableSpan; children: NestedSpan[] }
8
+
9
+ const buildSimplifiedRootSpans = (
8
10
  exporter: InMemorySpanExporter,
11
+ rootSpanName: string,
9
12
  mapAttributes?: (attributes: Attributes) => Attributes,
10
- ): SimplifiedNestedSpan => {
13
+ ): SimplifiedNestedSpan[] => {
11
14
  const spans = exporter.getFinishedSpans()
12
15
  const spansMap = new Map<string, NestedSpan>(spans.map((span) => [span.spanContext().spanId, { span, children: [] }]))
13
16
 
@@ -21,14 +24,12 @@ export const getSimplifiedRootSpan = (
21
24
  }
22
25
  })
23
26
 
24
- type NestedSpan = { span: ReadableSpan; children: NestedSpan[] }
25
- const createStoreSpanData = spans.find((_) => _.name === 'createStore')
26
- if (createStoreSpanData === undefined) {
27
+ const rootSpanDataList = spans.filter((_) => _.name === rootSpanName)
28
+ if (rootSpanDataList.length === 0) {
27
29
  throw new Error(
28
- `Could not find the root span named 'createStore'. Available spans: ${spans.map((s) => s.name).join(', ')}`,
30
+ `Could not find any root spans named '${rootSpanName}'. Available spans: ${spans.map((s) => s.name).join(', ')}`,
29
31
  )
30
32
  }
31
- const rootSpan = spansMap.get(createStoreSpanData.spanContext().spanId)!
32
33
 
33
34
  const simplifySpanRec = (span: NestedSpan): SimplifiedNestedSpan =>
34
35
  omitEmpty({
@@ -40,20 +41,29 @@ export const getSimplifiedRootSpan = (
40
41
  .map(simplifySpanRec),
41
42
  })
42
43
 
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)
44
+ return rootSpanDataList.map((rootSpanData) => {
45
+ const rootSpan = spansMap.get(rootSpanData.spanContext().spanId)!
46
+ return simplifySpanRec(rootSpan)
47
+ })
48
+ }
53
49
 
54
- // writeFileSync('tmp/trace.json', JSON.stringify(toTraceFile(spans), null, 2))
50
+ export const getSimplifiedRootSpan = (
51
+ exporter: InMemorySpanExporter,
52
+ rootSpanName: string,
53
+ mapAttributes?: (attributes: Attributes) => Attributes,
54
+ ): SimplifiedNestedSpan => {
55
+ const results = buildSimplifiedRootSpans(exporter, rootSpanName, mapAttributes)
56
+ const firstResult = results[0]
57
+ if (!firstResult) throw new Error(`Could not find the root span named '${rootSpanName}'.`)
58
+ return firstResult
59
+ }
55
60
 
56
- return simplifiedRootSpan
61
+ export const getAllSimplifiedRootSpans = (
62
+ exporter: InMemorySpanExporter,
63
+ rootSpanName: string,
64
+ mapAttributes?: (attributes: Attributes) => Attributes,
65
+ ): SimplifiedNestedSpan[] => {
66
+ return buildSimplifiedRootSpans(exporter, rootSpanName, mapAttributes)
57
67
  }
58
68
 
59
69
  // const compareHrTime = (a: [number, numndber], b: [number, number]) => {