@workflow/web-shared 5.0.0-beta.43 → 5.0.0-beta.44

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 (106) hide show
  1. package/README.md +4 -4
  2. package/dist/components/event-list-view.d.ts +12 -0
  3. package/dist/components/event-list-view.d.ts.map +1 -1
  4. package/dist/components/event-list-view.js +41 -20
  5. package/dist/components/sidebar/attribute-panel.d.ts +6 -2
  6. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  7. package/dist/components/sidebar/attribute-panel.js +10 -8
  8. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  9. package/dist/components/sidebar/copyable-data-block.js +2 -2
  10. package/dist/components/sidebar/entity-detail-panel.d.ts +6 -2
  11. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  12. package/dist/components/sidebar/entity-detail-panel.js +13 -5
  13. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  14. package/dist/components/sidebar/events-list.js +12 -5
  15. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  16. package/dist/components/sidebar/sidebar-data-context.d.ts +2 -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.js +2 -2
  20. package/dist/components/trace-viewer/components/detail-panel-width.d.ts +4 -4
  21. package/dist/components/trace-viewer/components/detail-panel-width.d.ts.map +1 -1
  22. package/dist/components/trace-viewer/components/detail-panel-width.js +5 -5
  23. package/dist/components/trace-viewer/components/detail-panel.d.ts +1 -1
  24. package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
  25. package/dist/components/trace-viewer/components/detail-panel.js +3 -3
  26. package/dist/components/trace-viewer/components/draggable-border.d.ts +1 -1
  27. package/dist/components/trace-viewer/components/draggable-border.js +2 -2
  28. package/dist/components/trace-viewer/components/minimap.js +4 -4
  29. package/dist/components/trace-viewer/components/span-markers.d.ts +8 -9
  30. package/dist/components/trace-viewer/components/span-markers.d.ts.map +1 -1
  31. package/dist/components/trace-viewer/components/span-markers.js +13 -10
  32. package/dist/components/trace-viewer/components/timeline.d.ts +1 -1
  33. package/dist/components/trace-viewer/components/timeline.js +4 -4
  34. package/dist/components/trace-viewer/components/use-row-window.d.ts +1 -1
  35. package/dist/components/trace-viewer/components/use-row-window.js +2 -2
  36. package/dist/components/trace-viewer/trace-viewer.js +3 -3
  37. package/dist/components/trace-viewer/utils.d.ts +3 -1
  38. package/dist/components/trace-viewer/utils.d.ts.map +1 -1
  39. package/dist/components/trace-viewer/utils.js +6 -5
  40. package/dist/components/ui/data-inspector.d.ts +7 -1
  41. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  42. package/dist/components/ui/data-inspector.js +10 -5
  43. package/dist/components/ui/decrypt-button.d.ts +5 -1
  44. package/dist/components/ui/decrypt-button.d.ts.map +1 -1
  45. package/dist/components/ui/decrypt-button.js +4 -4
  46. package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -5
  47. package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -1
  48. package/dist/components/ui/duplicate-event-tooltip.js +18 -9
  49. package/dist/components/ui/error-stack-block.js +4 -4
  50. package/dist/components/ui/timestamp-tooltip.d.ts +5 -3
  51. package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
  52. package/dist/components/ui/timestamp-tooltip.js +9 -9
  53. package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
  54. package/dist/components/workflow-traces/event-colors.js +11 -1
  55. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  56. package/dist/components/workflow-traces/trace-span-construction.js +5 -4
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -1
  60. package/dist/lib/duplicate-events.d.ts +1 -1
  61. package/dist/lib/duplicate-events.js +2 -2
  62. package/dist/lib/hydration.d.ts.map +1 -1
  63. package/dist/lib/hydration.js +8 -13
  64. package/dist/lib/sealed-events.d.ts +28 -0
  65. package/dist/lib/sealed-events.d.ts.map +1 -0
  66. package/dist/lib/sealed-events.js +30 -0
  67. package/dist/lib/trace-builder.d.ts.map +1 -1
  68. package/dist/lib/trace-builder.js +8 -5
  69. package/dist/lib/utils.d.ts +1 -1
  70. package/dist/lib/utils.js +2 -2
  71. package/dist/lib/zstd-browser-decoder.js +2 -2
  72. package/package.json +5 -5
  73. package/src/components/event-list-view.tsx +57 -22
  74. package/src/components/sidebar/attribute-panel.tsx +15 -7
  75. package/src/components/sidebar/copyable-data-block.tsx +8 -1
  76. package/src/components/sidebar/entity-detail-panel.tsx +18 -2
  77. package/src/components/sidebar/events-list.tsx +16 -6
  78. package/src/components/sidebar/resolve-hook-modal.tsx +2 -2
  79. package/src/components/sidebar/sidebar-data-context.tsx +2 -0
  80. package/src/components/stream-viewer.tsx +1 -1
  81. package/src/components/trace-viewer/components/detail-panel-width.ts +4 -4
  82. package/src/components/trace-viewer/components/detail-panel.tsx +3 -1
  83. package/src/components/trace-viewer/components/draggable-border.tsx +1 -1
  84. package/src/components/trace-viewer/components/minimap.tsx +3 -3
  85. package/src/components/trace-viewer/components/span-markers.tsx +18 -13
  86. package/src/components/trace-viewer/components/timeline.tsx +4 -4
  87. package/src/components/trace-viewer/components/use-row-window.ts +1 -1
  88. package/src/components/trace-viewer/trace-viewer.tsx +2 -2
  89. package/src/components/trace-viewer/utils.test.ts +5 -3
  90. package/src/components/trace-viewer/utils.ts +12 -8
  91. package/src/components/ui/data-inspector.tsx +19 -3
  92. package/src/components/ui/decrypt-button.tsx +9 -2
  93. package/src/components/ui/duplicate-event-tooltip.tsx +32 -10
  94. package/src/components/ui/error-stack-block.tsx +3 -3
  95. package/src/components/ui/timestamp-tooltip.tsx +16 -6
  96. package/src/components/workflow-traces/event-colors.ts +11 -0
  97. package/src/components/workflow-traces/trace-span-construction.test.ts +27 -1
  98. package/src/components/workflow-traces/trace-span-construction.ts +5 -3
  99. package/src/index.ts +4 -0
  100. package/src/lib/duplicate-events.ts +1 -1
  101. package/src/lib/hydration.ts +7 -12
  102. package/src/lib/sealed-events.ts +32 -0
  103. package/src/lib/trace-builder.test.ts +29 -1
  104. package/src/lib/trace-builder.ts +10 -5
  105. package/src/lib/utils.ts +1 -1
  106. package/src/lib/zstd-browser-decoder.ts +1 -1
@@ -137,8 +137,8 @@ export function getWebRevivers(): Revivers {
137
137
  // entry for each built-in Error subclass plus the workflow-specific
138
138
  // `FatalError` / `RetryableError` / `HookConflictError` /
139
139
  // `RuntimeDecryptionError` and `AggregateError`. Without
140
- // matching revivers here, `devalue.unflatten` throws "Unknown type X"
141
- // which surfaces in the web o11y UI as "Failed to load resource
140
+ // matching revivers here, `devalue.unflatten` throws "Unknown type X",
141
+ // which surfaces in the web o11y UI as "Failed to load resource
142
142
  // details: Unknown type FatalError".
143
143
  Error: (value) => {
144
144
  const opts = 'cause' in value ? { cause: value.cause } : undefined;
@@ -171,7 +171,7 @@ export function getWebRevivers(): Revivers {
171
171
  // `FatalError` and `RetryableError` are not built-in browser globals,
172
172
  // so we can't resolve a constructor from globalThis. The web o11y UI
173
173
  // doesn't need `instanceof FatalError` to pass (no user code runs
174
- // here) it just needs `name`, `message`, `stack`, and any extra
174
+ // here). The web o11y UI needs `name`, `message`, `stack`, and any extra
175
175
  // enumerable fields to render. Construct a plain `Error` with `name`
176
176
  // set; ObjectInspector reads `constructor.name` for the displayed
177
177
  // class label, but we don't have the real class, so we emit a tagged
@@ -209,7 +209,7 @@ export function getWebRevivers(): Revivers {
209
209
  // RetryableError reducer for the rationale around realm-safety).
210
210
  // Rehydrate as a Date so o11y consumers can render it directly.
211
211
  // Guard against payloads from older runtime versions that predate
212
- // the field without this check, `new Date(undefined)` would
212
+ // the field: without this check, `new Date(undefined)` would
213
213
  // produce an Invalid Date rather than omitting the property.
214
214
  if (value.retryAfter != null) {
215
215
  error.retryAfter = new Date(value.retryAfter);
@@ -492,17 +492,12 @@ export async function hydrateResourceIOAsync<T>(
492
492
  );
493
493
  // Payloads may be zstd-compressed (the Web DecompressionStream has no zstd);
494
494
  // register the WASM-backed browser decoder before hydrating. Idempotent and
495
- // lazy the WASM is only compiled when a zstd payload is actually decoded.
495
+ // lazy: the WASM is only compiled when a zstd payload is actually decoded.
496
496
  const { ensureZstdDecoderRegistered } = await import(
497
497
  './zstd-browser-decoder.js'
498
498
  );
499
499
  ensureZstdDecoderRegistered();
500
- // Resolve the *full* key capability, not just the symmetric key: a run's
501
- // event log can contain sealed ('encp') payloads that another run wrote to
502
- // it (a cross-deployment hook resumption, say), and opening those needs the
503
- // run's X25519 scalar in addition to its AES key. Both are derived from the
504
- // same 32 bytes the key-retrieval endpoint returns.
505
- // Resolve the *full* key capability, not just the symmetric key: a run's
500
+ // Resolve the *full* key capability: a run's
506
501
  // event log can contain sealed ('encp') payloads that another run wrote to
507
502
  // it (a cross-deployment hook resumption, say), and opening those needs the
508
503
  // run's X25519 scalar in addition to its AES key. Both derive from the same
@@ -520,7 +515,7 @@ export async function hydrateResourceIOAsync<T>(
520
515
  if (value instanceof Uint8Array) {
521
516
  return hydrateDataWithKey(value, revivers, cryptoKey);
522
517
  }
523
- // Not serialized return as-is.
518
+ // Not serialized, so return as-is.
524
519
  return value;
525
520
  }
526
521
 
@@ -0,0 +1,32 @@
1
+ import { type Event, isSealedNoopEvent as isSealedNoop } from '@workflow/world';
2
+
3
+ /**
4
+ * Sealed-position `noop` events (specVersion 7).
5
+ *
6
+ * A sealed-log backend hands each write its position before the write
7
+ * commits, so a writer that dies after claiming a position leaves a hole.
8
+ * The backend closes a provably abandoned hole by writing a `noop` event
9
+ * into it, creating a log-only row the run itself never observes. Replay steps
10
+ * over it without offering it to any consumer and without advancing the
11
+ * deterministic clock.
12
+ *
13
+ * The observability UI mirrors that treatment. A `noop` appears in event
14
+ * lists (greyed, with {@link SEALED_EVENT_MESSAGE} on hover) because it is a
15
+ * real row of the log, but it is excluded from span geometry and from
16
+ * trace-duration bounds: its `createdAt` is the *sealer's* wall clock, which
17
+ * can postdate every real event around it, and letting it stretch a span or
18
+ * the trace's known duration would chart the sealer's schedule rather than
19
+ * the run's.
20
+ */
21
+ export const SEALED_EVENT_MESSAGE =
22
+ 'No-op events are written by the backend to seal an abandoned log position';
23
+
24
+ /**
25
+ * Whether `event` is a backend-written seal for an abandoned position.
26
+ *
27
+ * Delegates to `@workflow/world` so the UI, the `node:vm` engine and the
28
+ * QuickJS engine cannot drift on what a seal is.
29
+ */
30
+ export function isSealedNoopEvent(event: Pick<Event, 'eventType'>): boolean {
31
+ return isSealedNoop(event);
32
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Event, EventType, WorkflowRun } from '@workflow/world';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
4
- import { buildTrace } from './trace-builder';
4
+ import { buildTrace, filterSpanRawEvents } from './trace-builder';
5
5
 
6
6
  const BASE_TIME = Date.parse('2026-01-01T00:00:00.000Z');
7
7
 
@@ -71,4 +71,32 @@ describe('buildTrace', () => {
71
71
  expect(otelTimeToMs(stepSpan?.endTime ?? [0, 0])).toBe(BASE_TIME + 20_000);
72
72
  expect(trace.duplicateEventIds.size).toBe(0);
73
73
  });
74
+
75
+ it('keeps sealed-position noops out of the geometry and its time bounds', () => {
76
+ const events = [
77
+ event('run_created', { at: 0 }),
78
+ event('run_started', { at: 0 }),
79
+ event('step_created', { correlationId: 'step_a', at: 1 }),
80
+ event('step_started', { correlationId: 'step_a', at: 1 }),
81
+ event('step_completed', { correlationId: 'step_a', at: 4 }),
82
+ // A hole sealed by a reader long after the run went quiet. Its
83
+ // createdAt is the SEALER's clock — letting it into the geometry would
84
+ // stretch the trace to the sealer's schedule.
85
+ event('noop', { correlationId: 'noop_5', at: 300 }),
86
+ ];
87
+
88
+ const trace = buildTrace(run, events, new Date(BASE_TIME + 400_000), {
89
+ isCompleteHistory: true,
90
+ });
91
+
92
+ // No span for the noop's correlationId, and the trace's known duration
93
+ // ends at the last real event, not at the seal.
94
+ expect(trace.spans.some((span) => span.spanId === 'noop_5')).toBe(false);
95
+ expect(trace.knownDurationMs).toBe(4000);
96
+
97
+ // The run span's raw event list still shows the seal (greyed in the UI),
98
+ // exactly like duplicate rows: real log rows, marked with the reason.
99
+ const runRaw = filterSpanRawEvents(events, 'run', 'run_1');
100
+ expect(runRaw.some((e) => e.eventType === 'noop')).toBe(true);
101
+ });
74
102
  });
@@ -23,6 +23,7 @@ import {
23
23
  } from '../components/workflow-traces/trace-span-construction';
24
24
  import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
25
25
  import { findDuplicateEventIds } from './duplicate-events';
26
+ import { isSealedNoopEvent } from './sealed-events';
26
27
  import type { Span } from './trace-types';
27
28
 
28
29
  /**
@@ -212,14 +213,18 @@ export function buildTrace(
212
213
  // Span geometry comes from what the run acted on. A repeat of a class the
213
214
  // log already records is read past by every replay, and letting one through
214
215
  // here would stretch a span to whenever a concurrent replay committed it.
215
- // The event lists still show them, marked as repeats.
216
+ // Sealed-position noops are excluded for the same reason with a different
217
+ // clock: a noop's createdAt is the sealer's wall time, which can postdate
218
+ // every real event around it, so feeding it into span grouping or the
219
+ // latest-known-time bound would chart the sealer's schedule instead of the
220
+ // run's. The event lists still show both, marked with the reason.
216
221
  const duplicateEventIds = findDuplicateEventIds(events, {
217
222
  isCompleteHistory,
218
223
  });
219
- const actedOnEvents =
220
- duplicateEventIds.size === 0
221
- ? events
222
- : events.filter((event) => !duplicateEventIds.has(event.eventId));
224
+ const actedOnEvents = events.filter(
225
+ (event) =>
226
+ !duplicateEventIds.has(event.eventId) && !isSealedNoopEvent(event)
227
+ );
223
228
 
224
229
  const groupedEvents = groupEventsByCorrelation(actedOnEvents);
225
230
  const latestKnownTime = computeLatestKnownTime(actedOnEvents, run);
package/src/lib/utils.ts CHANGED
@@ -80,7 +80,7 @@ export function formatDuration(ms: number, compact = false): string {
80
80
  * Unlike {@link formatDuration}, this keeps sub-second detail, so 1626ms
81
81
  * renders as "1.63s" rather than "2s". `fractionDigits` sets both the number of
82
82
  * decimals on the seconds component (trailing zeros trimmed, so 2000ms is "2s"
83
- * and 1500ms is "1.5s") and the rounding granularity the default of 2 rounds
83
+ * and 1500ms is "1.5s") and the rounding granularity: the default of 2 rounds
84
84
  * to 10ms; the timeline ruler passes fewer digits to match its tick step.
85
85
  *
86
86
  * - < 1s: shows whole milliseconds (e.g. "626ms")
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * The package leaves WASM sourcing to the caller; we resolve the shipped
10
10
  * `zstd.wasm` as a bundler asset (`new URL(..., import.meta.url)`, the same
11
- * pattern the trace-viewer Worker uses) and compile it once, lazily the
11
+ * pattern the trace-viewer Worker uses) and compile it once, lazily: the
12
12
  * WASM is fetched only the first time a zstd payload is actually decoded.
13
13
  */
14
14
  import { registerZstdDecoder } from '@workflow/core/serialization-format';