@livestore/livestore 0.4.0-dev.3 → 0.4.0-dev.6

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 (49) 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 +29 -24
  17. package/dist/reactive.js.map +1 -1
  18. package/dist/reactive.test.js +80 -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 +3 -2
  27. package/dist/store/store.d.ts.map +1 -1
  28. package/dist/store/store.js +94 -88
  29. package/dist/store/store.js.map +1 -1
  30. package/dist/utils/tests/fixture.d.ts.map +1 -1
  31. package/dist/utils/tests/fixture.js +2 -1
  32. package/dist/utils/tests/fixture.js.map +1 -1
  33. package/dist/utils/tests/otel.d.ts +15 -14
  34. package/dist/utils/tests/otel.d.ts.map +1 -1
  35. package/dist/utils/tests/otel.js +20 -15
  36. package/dist/utils/tests/otel.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/effect/LiveStore.ts +2 -4
  39. package/src/live-queries/__snapshots__/db-query.test.ts.snap +268 -131
  40. package/src/live-queries/db-query.test.ts +13 -7
  41. package/src/live-queries/db-query.ts +7 -4
  42. package/src/mod.ts +2 -0
  43. package/src/reactive.test.ts +100 -0
  44. package/src/reactive.ts +40 -35
  45. package/src/store/create-store.ts +12 -4
  46. package/src/store/store-types.ts +5 -2
  47. package/src/store/store.ts +160 -147
  48. package/src/utils/tests/fixture.ts +2 -1
  49. package/src/utils/tests/otel.ts +31 -20
@@ -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, omitUndefineds, 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
  },
@@ -208,8 +213,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
208
213
  },
209
214
  span: syncSpan,
210
215
  params: {
211
- leaderPushBatchSize: params.leaderPushBatchSize,
212
- simulation: params.simulation?.clientSessionSyncProcessor,
216
+ ...omitUndefineds({
217
+ leaderPushBatchSize: params.leaderPushBatchSize,
218
+ }),
219
+ ...(params.simulation?.clientSessionSyncProcessor !== undefined
220
+ ? { simulation: params.simulation.clientSessionSyncProcessor }
221
+ : {}),
213
222
  },
214
223
  confirmUnsavedChanges,
215
224
  })
@@ -411,8 +420,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
411
420
  Effect.sync(() =>
412
421
  this.subscribe(query$, {
413
422
  onUpdate: (result) => emit.single(result),
414
- otelContext,
415
- label: options?.label,
423
+ ...omitUndefineds({ otelContext, label: options?.label }),
416
424
  }),
417
425
  ),
418
426
  (unsub) => Effect.sync(() => unsub()),
@@ -447,7 +455,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
447
455
 
448
456
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
449
457
  const res = this.sqliteDbWrapper.cachedSelect(query.query, prepareBindValues(query.bindValues, query.query), {
450
- otelContext: options?.otelContext,
458
+ ...omitUndefineds({ otelContext: options?.otelContext }),
451
459
  }) as any
452
460
  if (query.schema) {
453
461
  return Schema.decodeSync(query.schema)(res)
@@ -473,11 +481,23 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
473
481
  }
474
482
 
475
483
  const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
476
- otelContext: options?.otelContext,
484
+ ...omitUndefineds({ otelContext: options?.otelContext }),
477
485
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
478
486
  })
479
487
 
480
- return Schema.decodeSync(schema)(rawRes)
488
+ const decodeResult = Schema.decodeEither(schema)(rawRes)
489
+ if (decodeResult._tag === 'Right') {
490
+ return decodeResult.right
491
+ } else {
492
+ return shouldNeverHappen(
493
+ `Failed to decode query result with for schema:`,
494
+ schema.toString(),
495
+ 'raw result:',
496
+ rawRes,
497
+ 'decode error:',
498
+ decodeResult.left,
499
+ )
500
+ }
481
501
  } else if (query._tag === 'def') {
482
502
  const query$ = query.make(this.reactivityGraph.context!)
483
503
  const result = this.query(query$.value, options)
@@ -487,7 +507,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
487
507
  const signal$ = query.make(this.reactivityGraph.context!)
488
508
  return signal$.value.get()
489
509
  } else {
490
- return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
510
+ return query.run({
511
+ ...omitUndefineds({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason }),
512
+ })
491
513
  }
492
514
  }
493
515
 
@@ -597,84 +619,76 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
597
619
 
598
620
  const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
599
621
 
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
622
+ Effect.gen(this, function* () {
623
+ const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)
624
+ commitsSpan?.addEvent('commit')
625
+ const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
626
+ commitsSpan?.addLink({ context: currentSpan.spanContext() })
607
627
 
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
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)
628
+ for (const event of events) {
629
+ replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
630
+ }
630
631
 
631
- try {
632
- // Materialize events to state
633
- const { writeTables } = (() => {
634
- try {
635
- const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
632
+ if (events.length === 0) return
636
633
 
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
- })()
634
+ const localRuntime = yield* Effect.runtime()
650
635
 
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])
636
+ const materializeEventsTx = Effect.try({
637
+ try: () => {
638
+ const runMaterializeEvents = () => {
639
+ return this.syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
656
640
  }
657
641
 
658
- const debugRefreshReason = {
659
- _tag: 'commit' as const,
660
- events,
661
- writeTables: Array.from(writeTables),
642
+ if (events.length > 1) {
643
+ return this.sqliteDbWrapper.txn(runMaterializeEvents)
644
+ } else {
645
+ return runMaterializeEvents()
662
646
  }
647
+ },
648
+ catch: (cause) => UnexpectedError.make({ cause }),
649
+ })
663
650
 
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()
651
+ // Materialize events to state
652
+ const { writeTables } = yield* materializeEventsTx
672
653
 
673
- durationMs = getDurationMsFromSpan(span)
674
- }
654
+ const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
655
+ for (const tableName of writeTables) {
656
+ const tableRef = this.tableRefs[tableName]
657
+ assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
658
+ tablesToUpdate.push([tableRef!, null])
659
+ }
675
660
 
676
- return { durationMs }
677
- },
661
+ const debugRefreshReason: RefreshReason = {
662
+ _tag: 'commit',
663
+ events,
664
+ writeTables: Array.from(writeTables),
665
+ }
666
+ const skipRefresh = options?.skipRefresh ?? false
667
+
668
+ // Update all table refs together in a batch, to only trigger one reactive update
669
+ this.reactivityGraph.setRefs(tablesToUpdate, {
670
+ debugRefreshReason,
671
+ skipRefresh,
672
+ otelContext: otel.trace.setSpan(otel.context.active(), currentSpan),
673
+ })
674
+ }).pipe(
675
+ Effect.withSpan('LiveStore:commit', {
676
+ root: true,
677
+ attributes: {
678
+ 'livestore.eventsCount': events.length,
679
+ 'livestore.eventTags': events.map((_) => _.name),
680
+ ...(options?.label && { 'livestore.commitLabel': options.label }),
681
+ },
682
+ links: [
683
+ // Span link to LiveStore:commits
684
+ OtelTracer.makeSpanLink({ context: otel.trace.getSpanContext(this.otel.commitsSpanContext)! }),
685
+ // User-provided span links
686
+ ...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
687
+ ],
688
+ }),
689
+ Effect.tapErrorCause(Effect.logError),
690
+ Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))),
691
+ Runtime.runSync(this.effectContext.runtime),
678
692
  )
679
693
  }
680
694
  // #endregion commit
@@ -746,9 +760,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
746
760
  *
747
761
  * This is called automatically when the store was created using the React or Effect API.
748
762
  */
749
- shutdown = (cause?: Cause.Cause<UnexpectedError>): Effect.Effect<void> => {
750
- this.checkShutdown('shutdown')
751
-
763
+ shutdown = (cause?: Cause.Cause<UnexpectedError | MaterializeError>): Effect.Effect<void> => {
752
764
  this.isShutdown = true
753
765
  return this.clientSession.shutdown(
754
766
  cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
@@ -798,14 +810,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
798
810
  .pipe(this.runEffectFork)
799
811
  },
800
812
 
801
- syncStates: () => {
813
+ syncStates: () =>
802
814
  Effect.gen(this, function* () {
803
815
  const session = yield* this.syncProcessor.syncState
804
- console.log('Session sync state:', session.toJSON())
805
816
  const leader = yield* this.clientSession.leaderThread.getSyncState
806
- console.log('Leader sync state:', leader.toJSON())
807
- }).pipe(this.runEffectFork)
808
- },
817
+ return { session, leader }
818
+ }).pipe(this.runEffectPromise),
809
819
 
810
820
  version: liveStoreVersion,
811
821
 
@@ -827,6 +837,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
827
837
  Runtime.runFork(this.effectContext.runtime),
828
838
  )
829
839
 
840
+ private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
841
+ effect.pipe(Effect.tapCauseLogPretty, Runtime.runPromise(this.effectContext.runtime))
842
+
830
843
  private getCommitArgs = (
831
844
  firstEventOrTxnFnOrOptions: any,
832
845
  restEvents: any[],
@@ -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',