@workflow/web-shared 5.0.0-beta.41 → 5.0.0-beta.42

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 (86) hide show
  1. package/dist/components/event-list-view.d.ts +10 -2
  2. package/dist/components/event-list-view.d.ts.map +1 -1
  3. package/dist/components/event-list-view.js +52 -26
  4. package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
  5. package/dist/components/sidebar/attributes-block.js +3 -6
  6. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  7. package/dist/components/sidebar/conversation-view.js +19 -36
  8. package/dist/components/sidebar/entity-detail-panel.d.ts +5 -0
  9. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  10. package/dist/components/sidebar/entity-detail-panel.js +2 -2
  11. package/dist/components/sidebar/events-list.d.ts +7 -1
  12. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  13. package/dist/components/sidebar/events-list.js +6 -5
  14. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  15. package/dist/components/sidebar/resolve-hook-modal.js +4 -145
  16. package/dist/components/sidebar/sidebar-data-context.d.ts +6 -0
  17. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
  18. package/dist/components/sidebar/sidebar-data-context.js +1 -1
  19. package/dist/components/stream-viewer-skeleton.js +6 -6
  20. package/dist/components/stream-viewer.js +2 -2
  21. package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
  22. package/dist/components/trace-viewer/components/detail-panel.js +2 -1
  23. package/dist/components/trace-viewer/components/timeline.d.ts.map +1 -1
  24. package/dist/components/trace-viewer/components/timeline.js +13 -22
  25. package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
  26. package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
  27. package/dist/components/trace-viewer/icons.js +5 -5
  28. package/dist/components/trace-viewer/utils.d.ts +6 -10
  29. package/dist/components/trace-viewer/utils.d.ts.map +1 -1
  30. package/dist/components/trace-viewer/utils.js +14 -24
  31. package/dist/components/trace-viewer.d.ts.map +1 -1
  32. package/dist/components/trace-viewer.js +12 -4
  33. package/dist/components/ui/context-card.d.ts.map +1 -1
  34. package/dist/components/ui/context-card.js +9 -21
  35. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  36. package/dist/components/ui/data-inspector.js +8 -35
  37. package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
  38. package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
  39. package/dist/components/ui/duplicate-event-tooltip.js +19 -0
  40. package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
  41. package/dist/components/ui/menu-dropdown.js +3 -19
  42. package/dist/components/ui/skeleton.js +2 -2
  43. package/dist/components/ui/spinner.d.ts.map +1 -1
  44. package/dist/components/ui/spinner.js +5 -11
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -1
  48. package/dist/lib/duplicate-events.d.ts +27 -0
  49. package/dist/lib/duplicate-events.d.ts.map +1 -0
  50. package/dist/lib/duplicate-events.js +166 -0
  51. package/dist/lib/event-materialization.d.ts +15 -2
  52. package/dist/lib/event-materialization.d.ts.map +1 -1
  53. package/dist/lib/event-materialization.js +19 -4
  54. package/dist/lib/trace-builder.d.ts +16 -1
  55. package/dist/lib/trace-builder.d.ts.map +1 -1
  56. package/dist/lib/trace-builder.js +23 -4
  57. package/package.json +4 -4
  58. package/src/components/event-list-view.tsx +70 -32
  59. package/src/components/sidebar/attributes-block.tsx +4 -11
  60. package/src/components/sidebar/conversation-view.tsx +25 -79
  61. package/src/components/sidebar/entity-detail-panel.tsx +6 -0
  62. package/src/components/sidebar/events-list.tsx +21 -3
  63. package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
  64. package/src/components/sidebar/sidebar-data-context.tsx +6 -0
  65. package/src/components/stream-viewer-skeleton.tsx +5 -5
  66. package/src/components/stream-viewer.tsx +1 -1
  67. package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
  68. package/src/components/trace-viewer/components/timeline.tsx +21 -39
  69. package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
  70. package/src/components/trace-viewer/icons.tsx +4 -4
  71. package/src/components/trace-viewer/utils.test.ts +5 -7
  72. package/src/components/trace-viewer/utils.ts +17 -31
  73. package/src/components/trace-viewer.tsx +15 -3
  74. package/src/components/ui/context-card.tsx +16 -26
  75. package/src/components/ui/data-inspector.tsx +20 -65
  76. package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
  77. package/src/components/ui/menu-dropdown.tsx +8 -22
  78. package/src/components/ui/skeleton.tsx +2 -2
  79. package/src/components/ui/spinner.tsx +4 -8
  80. package/src/index.ts +4 -0
  81. package/src/lib/duplicate-events.test.ts +339 -0
  82. package/src/lib/duplicate-events.ts +188 -0
  83. package/src/lib/event-materialization.test.ts +117 -0
  84. package/src/lib/event-materialization.ts +23 -3
  85. package/src/lib/trace-builder.test.ts +74 -0
  86. package/src/lib/trace-builder.ts +30 -3
@@ -17,6 +17,7 @@ import {
17
17
  isWaitEventType,
18
18
  type StepStatus,
19
19
  } from '@workflow/world';
20
+ import { findDuplicateEventIds } from './duplicate-events';
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Materialized entity types
@@ -105,9 +106,24 @@ function getEventTimestamp(event: Event | undefined): Date | undefined {
105
106
  *
106
107
  * Handles partial event lists gracefully: a step may only have a
107
108
  * step_created event with no completion yet.
109
+ *
110
+ * The derived status and timestamps come from the events the run acted on. A
111
+ * repeat of a class the log already records is read past by every replay, so
112
+ * a second terminal event written by a concurrent replay does not move a step
113
+ * off the outcome the first one recorded. Every event stays on the entity's
114
+ * `events` list.
115
+ *
116
+ * That reduction needs the whole log to be sound, so it only runs when
117
+ * `isCompleteHistory` says `events` is it. See {@link findDuplicateEventIds}.
108
118
  */
109
- export function materializeSteps(events: Event[]): MaterializedStep[] {
119
+ export function materializeSteps(
120
+ events: Event[],
121
+ { isCompleteHistory = false }: { isCompleteHistory?: boolean } = {}
122
+ ): MaterializedStep[] {
110
123
  const groups = groupByCorrelationId(events, isStepEventType);
124
+ const duplicateEventIds = findDuplicateEventIds(events, {
125
+ isCompleteHistory,
126
+ });
111
127
  const steps: MaterializedStep[] = [];
112
128
 
113
129
  for (const [correlationId, stepEvents] of groups) {
@@ -121,6 +137,7 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
121
137
  let updatedAt = getEventTimestamp(created) ?? created.createdAt;
122
138
 
123
139
  for (const e of stepEvents) {
140
+ if (duplicateEventIds.has(e.eventId)) continue;
124
141
  switch (e.eventType) {
125
142
  case 'step_started':
126
143
  status = 'running';
@@ -248,9 +265,12 @@ export function materializeWaits(events: Event[]): MaterializedWait[] {
248
265
  * Convenience function that materializes all entity types from a flat
249
266
  * event list.
250
267
  */
251
- export function materializeAll(events: Event[]): MaterializedEntities {
268
+ export function materializeAll(
269
+ events: Event[],
270
+ options: { isCompleteHistory?: boolean } = {}
271
+ ): MaterializedEntities {
252
272
  return {
253
- steps: materializeSteps(events),
273
+ steps: materializeSteps(events, options),
254
274
  hooks: materializeHooks(events),
255
275
  waits: materializeWaits(events),
256
276
  };
@@ -0,0 +1,74 @@
1
+ import type { Event, EventType, WorkflowRun } from '@workflow/world';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
4
+ import { buildTrace } from './trace-builder';
5
+
6
+ const BASE_TIME = Date.parse('2026-01-01T00:00:00.000Z');
7
+
8
+ let nextId = 0;
9
+
10
+ function event(
11
+ eventType: EventType,
12
+ options: { correlationId?: string; at: number }
13
+ ): Event {
14
+ nextId += 1;
15
+ return {
16
+ eventId: `evt_${nextId}`,
17
+ runId: 'run_1',
18
+ eventType,
19
+ correlationId: options.correlationId,
20
+ createdAt: new Date(BASE_TIME + options.at * 1000),
21
+ occurredAt: new Date(BASE_TIME + options.at * 1000),
22
+ eventData: eventType === 'step_created' ? { stepName: 'doWork' } : {},
23
+ } as unknown as Event;
24
+ }
25
+
26
+ const run = {
27
+ runId: 'run_1',
28
+ workflowName: 'demo',
29
+ status: 'running',
30
+ createdAt: new Date(BASE_TIME),
31
+ } as unknown as WorkflowRun;
32
+
33
+ describe('buildTrace', () => {
34
+ it('ends a step span on the terminal event the run acted on', () => {
35
+ const events = [
36
+ event('run_created', { at: 0 }),
37
+ event('run_started', { at: 0 }),
38
+ event('step_created', { correlationId: 'step_a', at: 1 }),
39
+ event('step_started', { correlationId: 'step_a', at: 1 }),
40
+ event('step_completed', { correlationId: 'step_a', at: 4 }),
41
+ // A concurrent replay commits the same outcome much later. Measuring the
42
+ // span against it would report a 20s step that ran for 3s.
43
+ event('step_completed', { correlationId: 'step_a', at: 20 }),
44
+ ];
45
+
46
+ const trace = buildTrace(run, events, new Date(BASE_TIME + 30_000), {
47
+ isCompleteHistory: true,
48
+ });
49
+ const stepSpan = trace.spans.find((span) => span.resource === 'step');
50
+
51
+ expect(stepSpan).toBeDefined();
52
+ expect(otelTimeToMs(stepSpan?.endTime ?? [0, 0])).toBe(BASE_TIME + 4000);
53
+ expect(trace.knownDurationMs).toBe(4000);
54
+ });
55
+
56
+ it('keeps every event in the geometry when the log may be incomplete', () => {
57
+ const events = [
58
+ event('run_created', { at: 0 }),
59
+ event('run_started', { at: 0 }),
60
+ event('step_created', { correlationId: 'step_a', at: 1 }),
61
+ event('step_started', { correlationId: 'step_a', at: 1 }),
62
+ event('step_completed', { correlationId: 'step_a', at: 4 }),
63
+ event('step_completed', { correlationId: 'step_a', at: 20 }),
64
+ ];
65
+
66
+ // Without the whole log there is no telling which of the two completions
67
+ // the run acted on, so neither is dropped and the span covers both.
68
+ const trace = buildTrace(run, events, new Date(BASE_TIME + 30_000));
69
+ const stepSpan = trace.spans.find((span) => span.resource === 'step');
70
+
71
+ expect(otelTimeToMs(stepSpan?.endTime ?? [0, 0])).toBe(BASE_TIME + 20_000);
72
+ expect(trace.duplicateEventIds.size).toBe(0);
73
+ });
74
+ });
@@ -22,6 +22,7 @@ import {
22
22
  waitToSpan,
23
23
  } from '../components/workflow-traces/trace-span-construction';
24
24
  import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
25
+ import { findDuplicateEventIds } from './duplicate-events';
25
26
  import type { Span } from './trace-types';
26
27
 
27
28
  /**
@@ -188,15 +189,40 @@ export interface TraceWithMeta {
188
189
  resources: { name: string; attributes: Record<string, string> }[];
189
190
  /** Duration in ms from trace start to the latest known event. */
190
191
  knownDurationMs: number;
192
+ /**
193
+ * The events left out of the span geometry as repeats. Empty unless the
194
+ * caller vouched for the log being complete. See
195
+ * {@link findDuplicateEventIds}.
196
+ */
197
+ duplicateEventIds: ReadonlySet<string>;
191
198
  }
192
199
 
193
200
  export function buildTrace(
194
201
  run: WorkflowRun,
195
202
  events: Event[],
196
- now: Date
203
+ now: Date,
204
+ /**
205
+ * Whether `events` is the run's whole log. Defaults to false, which builds
206
+ * the trace from every event: on a subset there is no way to tell a repeat
207
+ * from the only copy the caller was given, and dropping the wrong one moves
208
+ * a span. See {@link findDuplicateEventIds}.
209
+ */
210
+ { isCompleteHistory = false }: { isCompleteHistory?: boolean } = {}
197
211
  ): TraceWithMeta {
198
- const groupedEvents = groupEventsByCorrelation(events);
199
- const latestKnownTime = computeLatestKnownTime(events, run);
212
+ // Span geometry comes from what the run acted on. A repeat of a class the
213
+ // log already records is read past by every replay, and letting one through
214
+ // here would stretch a span to whenever a concurrent replay committed it.
215
+ // The event lists still show them, marked as repeats.
216
+ const duplicateEventIds = findDuplicateEventIds(events, {
217
+ isCompleteHistory,
218
+ });
219
+ const actedOnEvents =
220
+ duplicateEventIds.size === 0
221
+ ? events
222
+ : events.filter((event) => !duplicateEventIds.has(event.eventId));
223
+
224
+ const groupedEvents = groupEventsByCorrelation(actedOnEvents);
225
+ const latestKnownTime = computeLatestKnownTime(actedOnEvents, run);
200
226
  const { runSpan, spans } = buildSpans(
201
227
  run,
202
228
  groupedEvents,
@@ -222,5 +248,6 @@ export function buildTrace(
222
248
  },
223
249
  ],
224
250
  knownDurationMs: Math.max(0, knownDurationMs),
251
+ duplicateEventIds,
225
252
  };
226
253
  }