@workflow/web-shared 4.1.0-beta.50 → 4.1.0-beta.52
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/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +8 -5
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +3 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +23 -91
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/detail-card.d.ts +3 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +2 -2
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +22 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +38 -49
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts +4 -4
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +77 -12
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/stream-viewer.d.ts +8 -7
- package/dist/components/stream-viewer.d.ts.map +1 -1
- package/dist/components/stream-viewer.js +12 -9
- package/dist/components/stream-viewer.js.map +1 -1
- package/dist/components/trace-viewer/context.d.ts +3 -2
- package/dist/components/trace-viewer/context.d.ts.map +1 -1
- package/dist/components/trace-viewer/context.js.map +1 -1
- package/dist/components/ui/inspector-theme.d.ts +81 -0
- package/dist/components/ui/inspector-theme.d.ts.map +1 -0
- package/dist/components/ui/inspector-theme.js +63 -0
- package/dist/components/ui/inspector-theme.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +3 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +111 -10
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +17 -12
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/hydration.d.ts +26 -0
- package/dist/lib/hydration.d.ts.map +1 -0
- package/dist/lib/hydration.js +98 -0
- package/dist/lib/hydration.js.map +1 -0
- package/package.json +3 -2
- package/src/components/event-list-view.tsx +15 -9
- package/src/components/index.ts +6 -5
- package/src/components/sidebar/attribute-panel.tsx +30 -129
- package/src/components/sidebar/detail-card.tsx +7 -1
- package/src/components/sidebar/entity-detail-panel.tsx +71 -57
- package/src/components/sidebar/events-list.tsx +204 -88
- package/src/components/stream-viewer.tsx +37 -21
- package/src/components/trace-viewer/context.tsx +3 -2
- package/src/components/ui/inspector-theme.ts +64 -0
- package/src/components/workflow-trace-view.tsx +210 -32
- package/src/components/workflow-traces/trace-span-construction.ts +17 -12
- package/src/index.ts +13 -0
- package/src/lib/hydration.ts +137 -0
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
4
|
-
import {
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
6
|
import { toast } from 'sonner';
|
|
6
7
|
import { ErrorBoundary } from './error-boundary';
|
|
7
8
|
import {
|
|
8
9
|
EntityDetailPanel,
|
|
10
|
+
type SelectedSpanInfo,
|
|
9
11
|
type SpanSelectionInfo,
|
|
10
12
|
} from './sidebar/entity-detail-panel';
|
|
11
13
|
import {
|
|
12
14
|
TraceViewerContextProvider,
|
|
13
15
|
TraceViewerTimeline,
|
|
16
|
+
useTraceViewer,
|
|
14
17
|
} from './trace-viewer';
|
|
15
18
|
import type { Span } from './trace-viewer/types';
|
|
16
19
|
import { Skeleton } from './ui/skeleton';
|
|
@@ -28,6 +31,8 @@ import {
|
|
|
28
31
|
import { otelTimeToMs } from './workflow-traces/trace-time-utils';
|
|
29
32
|
|
|
30
33
|
const RE_RENDER_INTERVAL_MS = 2000;
|
|
34
|
+
const DEFAULT_PANEL_WIDTH = 380;
|
|
35
|
+
const MIN_PANEL_WIDTH = 240;
|
|
31
36
|
|
|
32
37
|
type GroupedEvents = {
|
|
33
38
|
eventsByStepId: Map<string, Event[]>;
|
|
@@ -184,6 +189,95 @@ const buildTrace = (
|
|
|
184
189
|
/** Re-export SpanSelectionInfo for consumers */
|
|
185
190
|
export type { SpanSelectionInfo };
|
|
186
191
|
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Bridge: reads selected span from trace viewer context and notifies parent
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Bridge component that lives INSIDE the TraceViewerContextProvider and
|
|
198
|
+
* reads the selected span info, passing it up to the parent via callback.
|
|
199
|
+
* This allows the parent to render the detail panel OUTSIDE the context.
|
|
200
|
+
*/
|
|
201
|
+
function SelectionBridge({
|
|
202
|
+
onSelectionChange,
|
|
203
|
+
}: {
|
|
204
|
+
onSelectionChange: (info: SelectedSpanInfo | null) => void;
|
|
205
|
+
}) {
|
|
206
|
+
const { state } = useTraceViewer();
|
|
207
|
+
const { selected } = state;
|
|
208
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
209
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (selected) {
|
|
213
|
+
onSelectionChangeRef.current({
|
|
214
|
+
data: selected.span.attributes?.data,
|
|
215
|
+
resource: selected.span.attributes?.resource as string | undefined,
|
|
216
|
+
spanId: selected.span.spanId,
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
onSelectionChangeRef.current(null);
|
|
220
|
+
}
|
|
221
|
+
}, [selected]);
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Bridge component to deselect from outside the context.
|
|
228
|
+
*/
|
|
229
|
+
function DeselectBridge({ triggerDeselect }: { triggerDeselect: number }) {
|
|
230
|
+
const { dispatch } = useTraceViewer();
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (triggerDeselect > 0) {
|
|
234
|
+
dispatch({ type: 'deselect' });
|
|
235
|
+
}
|
|
236
|
+
}, [triggerDeselect, dispatch]);
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Panel chrome (header with name/duration, close button)
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
function PanelResizeHandle({
|
|
246
|
+
onResize,
|
|
247
|
+
}: {
|
|
248
|
+
onResize: (deltaX: number) => void;
|
|
249
|
+
}) {
|
|
250
|
+
const handlePointerDown = useCallback(
|
|
251
|
+
(e: React.PointerEvent) => {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
let lastX = e.clientX;
|
|
254
|
+
const onPointerMove = (moveEvent: PointerEvent) => {
|
|
255
|
+
const deltaX = lastX - moveEvent.clientX;
|
|
256
|
+
lastX = moveEvent.clientX;
|
|
257
|
+
onResize(deltaX);
|
|
258
|
+
};
|
|
259
|
+
const onPointerUp = () => {
|
|
260
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
261
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
262
|
+
};
|
|
263
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
264
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
265
|
+
},
|
|
266
|
+
[onResize]
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-blue-400/50 z-10"
|
|
272
|
+
onPointerDown={handlePointerDown}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Main component
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
187
281
|
export const WorkflowTraceViewer = ({
|
|
188
282
|
run,
|
|
189
283
|
steps,
|
|
@@ -198,6 +292,7 @@ export const WorkflowTraceViewer = ({
|
|
|
198
292
|
onResolveHook,
|
|
199
293
|
onStreamClick,
|
|
200
294
|
onSpanSelect,
|
|
295
|
+
onLoadEventData,
|
|
201
296
|
}: {
|
|
202
297
|
run: WorkflowRun;
|
|
203
298
|
steps: Step[];
|
|
@@ -221,8 +316,18 @@ export const WorkflowTraceViewer = ({
|
|
|
221
316
|
onStreamClick?: (streamId: string) => void;
|
|
222
317
|
/** Callback when a span is selected. */
|
|
223
318
|
onSpanSelect?: (info: SpanSelectionInfo) => void;
|
|
319
|
+
/** Callback to load event data for a specific event (lazy loading in sidebar) */
|
|
320
|
+
onLoadEventData?: (
|
|
321
|
+
correlationId: string,
|
|
322
|
+
eventId: string
|
|
323
|
+
) => Promise<unknown | null>;
|
|
224
324
|
}) => {
|
|
225
325
|
const [now, setNow] = useState(() => new Date());
|
|
326
|
+
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
327
|
+
null
|
|
328
|
+
);
|
|
329
|
+
const [panelWidth, setPanelWidth] = useState(DEFAULT_PANEL_WIDTH);
|
|
330
|
+
const [deselectTrigger, setDeselectTrigger] = useState(0);
|
|
226
331
|
|
|
227
332
|
useEffect(() => {
|
|
228
333
|
if (!run?.completedAt) {
|
|
@@ -250,27 +355,50 @@ export const WorkflowTraceViewer = ({
|
|
|
250
355
|
}
|
|
251
356
|
}, [error, isLoading]);
|
|
252
357
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
(info
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
358
|
+
const handleSpanSelect = useCallback(
|
|
359
|
+
(info: SpanSelectionInfo) => {
|
|
360
|
+
onSpanSelect?.(info);
|
|
361
|
+
},
|
|
362
|
+
[onSpanSelect]
|
|
363
|
+
);
|
|
260
364
|
|
|
365
|
+
const handleSelectionChange = useCallback(
|
|
366
|
+
(info: SelectedSpanInfo | null) => {
|
|
367
|
+
if (info) {
|
|
368
|
+
// Filter raw events by the selected span's correlationId (stepId/hookId)
|
|
369
|
+
// This bypasses the trace worker pipeline entirely.
|
|
370
|
+
const correlationId = info.spanId;
|
|
371
|
+
const rawEvents = correlationId
|
|
372
|
+
? events.filter((e) => e.correlationId === correlationId)
|
|
373
|
+
: [];
|
|
374
|
+
setSelectedSpan({ ...info, rawEvents });
|
|
375
|
+
} else {
|
|
376
|
+
setSelectedSpan(null);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
[events]
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const handleClose = useCallback(() => {
|
|
383
|
+
setSelectedSpan(null);
|
|
384
|
+
setDeselectTrigger((n) => n + 1);
|
|
385
|
+
}, []);
|
|
386
|
+
|
|
387
|
+
const handleResize = useCallback((deltaX: number) => {
|
|
388
|
+
setPanelWidth((w) => Math.max(MIN_PANEL_WIDTH, w + deltaX));
|
|
389
|
+
}, []);
|
|
390
|
+
|
|
391
|
+
// Get the selected span name and duration for the panel header
|
|
392
|
+
const selectedSpanName = useMemo(() => {
|
|
393
|
+
if (!selectedSpan?.data) return undefined;
|
|
394
|
+
const data = selectedSpan.data as Record<string, unknown>;
|
|
261
395
|
return (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
spanDetailError={spanDetailError}
|
|
267
|
-
spanDetailLoading={spanDetailLoading}
|
|
268
|
-
onSpanSelect={handleSpanSelect}
|
|
269
|
-
onWakeUpSleep={onWakeUpSleep}
|
|
270
|
-
onResolveHook={onResolveHook}
|
|
271
|
-
/>
|
|
396
|
+
(data.stepName as string) ??
|
|
397
|
+
(data.workflowName as string) ??
|
|
398
|
+
(data.hookId as string) ??
|
|
399
|
+
'Details'
|
|
272
400
|
);
|
|
273
|
-
};
|
|
401
|
+
}, [selectedSpan?.data]);
|
|
274
402
|
|
|
275
403
|
if (isLoading || !trace) {
|
|
276
404
|
return (
|
|
@@ -288,19 +416,69 @@ export const WorkflowTraceViewer = ({
|
|
|
288
416
|
}
|
|
289
417
|
|
|
290
418
|
return (
|
|
291
|
-
<div className="relative w-full h-full">
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
419
|
+
<div className="relative w-full h-full flex">
|
|
420
|
+
{/* Timeline (takes remaining space) */}
|
|
421
|
+
<div className="flex-1 min-w-0 relative">
|
|
422
|
+
<TraceViewerContextProvider
|
|
423
|
+
customSpanClassNameFunc={getCustomSpanClassName}
|
|
424
|
+
customSpanEventClassNameFunc={getCustomSpanEventClassName}
|
|
425
|
+
>
|
|
426
|
+
<SelectionBridge onSelectionChange={handleSelectionChange} />
|
|
427
|
+
<DeselectBridge triggerDeselect={deselectTrigger} />
|
|
428
|
+
<TraceViewerTimeline height="100%" trace={trace} />
|
|
429
|
+
</TraceViewerContextProvider>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
{/* Detail panel (rendered outside the context, as a sibling) */}
|
|
433
|
+
{selectedSpan && (
|
|
434
|
+
<div
|
|
435
|
+
className="relative border-l flex-shrink-0 flex flex-col"
|
|
436
|
+
style={{
|
|
437
|
+
width: panelWidth,
|
|
438
|
+
borderColor: 'var(--ds-gray-200)',
|
|
439
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
<PanelResizeHandle onResize={handleResize} />
|
|
443
|
+
{/* Panel header */}
|
|
444
|
+
<div
|
|
445
|
+
className="flex items-center justify-between px-3 py-2 border-b flex-shrink-0"
|
|
446
|
+
style={{ borderColor: 'var(--ds-gray-200)' }}
|
|
447
|
+
>
|
|
448
|
+
<span
|
|
449
|
+
className="text-sm font-medium truncate"
|
|
450
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
451
|
+
>
|
|
452
|
+
{selectedSpanName}
|
|
453
|
+
</span>
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
aria-label="Close panel"
|
|
457
|
+
onClick={handleClose}
|
|
458
|
+
className="ml-2 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex-shrink-0"
|
|
459
|
+
>
|
|
460
|
+
<X size={16} />
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
463
|
+
{/* Panel body */}
|
|
464
|
+
<div className="flex-1 overflow-y-auto">
|
|
465
|
+
<ErrorBoundary title="Failed to load entity details">
|
|
466
|
+
<EntityDetailPanel
|
|
467
|
+
run={run}
|
|
468
|
+
onStreamClick={onStreamClick}
|
|
469
|
+
spanDetailData={spanDetailData ?? null}
|
|
470
|
+
spanDetailError={spanDetailError}
|
|
471
|
+
spanDetailLoading={spanDetailLoading}
|
|
472
|
+
onSpanSelect={handleSpanSelect}
|
|
473
|
+
onWakeUpSleep={onWakeUpSleep}
|
|
474
|
+
onLoadEventData={onLoadEventData}
|
|
475
|
+
onResolveHook={onResolveHook}
|
|
476
|
+
selectedSpan={selectedSpan}
|
|
477
|
+
/>
|
|
478
|
+
</ErrorBoundary>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
)}
|
|
304
482
|
</div>
|
|
305
483
|
);
|
|
306
484
|
};
|
|
@@ -96,7 +96,7 @@ export function waitToSpan(
|
|
|
96
96
|
const start = dateToOtelTime(startTime);
|
|
97
97
|
const end = dateToOtelTime(endTime);
|
|
98
98
|
const duration = calculateDuration(startTime, endTime);
|
|
99
|
-
const spanEvents = convertEventsToSpanEvents(events);
|
|
99
|
+
const spanEvents = convertEventsToSpanEvents(events, false);
|
|
100
100
|
return {
|
|
101
101
|
spanId: wait.waitId,
|
|
102
102
|
name: 'sleep',
|
|
@@ -107,7 +107,7 @@ export function waitToSpan(
|
|
|
107
107
|
traceFlags: 1,
|
|
108
108
|
attributes: {
|
|
109
109
|
resource: 'sleep' as const,
|
|
110
|
-
data: wait,
|
|
110
|
+
data: wait, // wait is a plain object built from events, no non-cloneable types
|
|
111
111
|
},
|
|
112
112
|
links: [],
|
|
113
113
|
events: spanEvents,
|
|
@@ -128,19 +128,22 @@ export function stepToSpan(
|
|
|
128
128
|
const now = nowTime ?? new Date();
|
|
129
129
|
const parsedName = parseStepName(String(step.stepName));
|
|
130
130
|
|
|
131
|
-
//
|
|
131
|
+
// Only embed identification fields — not the full object with
|
|
132
|
+
// input/output/error which may contain non-cloneable types.
|
|
133
|
+
// The detail panel fetches full data separately via spanDetailData.
|
|
134
|
+
const { input: _i, output: _o, error: _e, ...stepIdentity } = step;
|
|
132
135
|
const attributes = {
|
|
133
136
|
resource: 'step' as const,
|
|
134
|
-
data:
|
|
137
|
+
data: stepIdentity,
|
|
135
138
|
};
|
|
136
139
|
|
|
137
140
|
const resource = 'step';
|
|
138
141
|
const endTime = new Date(step.completedAt ?? now);
|
|
139
142
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
const events = convertEventsToSpanEvents(stepEvents);
|
|
143
|
+
// Include ALL correlated events on the span so the sidebar detail view
|
|
144
|
+
// can display them. The timeline uses the `showVerticalLine` flag to
|
|
145
|
+
// determine which events appear as markers.
|
|
146
|
+
const events = convertEventsToSpanEvents(stepEvents, false);
|
|
144
147
|
|
|
145
148
|
// Use createdAt as span start time, with activeStartTime for when execution began
|
|
146
149
|
// This allows visualization of the "queued" period before execution
|
|
@@ -225,7 +228,7 @@ export function hookToSpan(
|
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
// Convert hook-related events to span events
|
|
228
|
-
const events = convertEventsToSpanEvents(hookEvents);
|
|
231
|
+
const events = convertEventsToSpanEvents(hookEvents, false);
|
|
229
232
|
|
|
230
233
|
// We display hooks as a minimum span size of 10 seconds, just to ensure
|
|
231
234
|
// it's clickable even if there is no
|
|
@@ -262,10 +265,12 @@ export function runToSpan(
|
|
|
262
265
|
): Span {
|
|
263
266
|
const now = nowTime ?? new Date();
|
|
264
267
|
|
|
265
|
-
//
|
|
268
|
+
// Only embed identification fields — not the full object with
|
|
269
|
+
// input/output/error which may contain non-cloneable types.
|
|
270
|
+
const { input: _i, output: _o, error: _e, ...runIdentity } = run;
|
|
266
271
|
const attributes = {
|
|
267
272
|
resource: 'run' as const,
|
|
268
|
-
data:
|
|
273
|
+
data: runIdentity,
|
|
269
274
|
};
|
|
270
275
|
|
|
271
276
|
// Use createdAt as span start time, with activeStartTime for when execution began
|
|
@@ -274,7 +279,7 @@ export function runToSpan(
|
|
|
274
279
|
const endTime = run.completedAt ?? now;
|
|
275
280
|
|
|
276
281
|
// Convert run-level events to span events
|
|
277
|
-
const events = convertEventsToSpanEvents(runEvents);
|
|
282
|
+
const events = convertEventsToSpanEvents(runEvents, false);
|
|
278
283
|
|
|
279
284
|
return {
|
|
280
285
|
spanId: String(run.runId),
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,19 @@ export {
|
|
|
20
20
|
isTerminalStatus,
|
|
21
21
|
shouldShowReenqueueButton,
|
|
22
22
|
} from './lib/event-analysis';
|
|
23
|
+
export type { Revivers, StreamRef } from './lib/hydration';
|
|
24
|
+
export {
|
|
25
|
+
CLASS_INSTANCE_REF_TYPE,
|
|
26
|
+
ClassInstanceRef,
|
|
27
|
+
extractStreamIds,
|
|
28
|
+
getWebRevivers,
|
|
29
|
+
hydrateResourceIO,
|
|
30
|
+
isClassInstanceRef,
|
|
31
|
+
isStreamId,
|
|
32
|
+
isStreamRef,
|
|
33
|
+
STREAM_REF_TYPE,
|
|
34
|
+
truncateId,
|
|
35
|
+
} from './lib/hydration';
|
|
23
36
|
export type { StreamStep } from './lib/utils';
|
|
24
37
|
export {
|
|
25
38
|
extractConversation,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-specific hydration for o11y display.
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe revivers that use `atob()` for base64 decoding (no Node.js
|
|
5
|
+
* `Buffer` dependency). Produces `ClassInstanceRef` objects and `StreamRef`
|
|
6
|
+
* objects for UI rendering.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ClassInstanceRef,
|
|
11
|
+
extractClassName,
|
|
12
|
+
hydrateResourceIO as hydrateResourceIOGeneric,
|
|
13
|
+
observabilityRevivers,
|
|
14
|
+
type Revivers,
|
|
15
|
+
} from '@workflow/core/serialization-format';
|
|
16
|
+
|
|
17
|
+
// Re-export types and utilities that consumers need
|
|
18
|
+
export {
|
|
19
|
+
CLASS_INSTANCE_REF_TYPE,
|
|
20
|
+
ClassInstanceRef,
|
|
21
|
+
extractStreamIds,
|
|
22
|
+
isClassInstanceRef,
|
|
23
|
+
isStreamId,
|
|
24
|
+
isStreamRef,
|
|
25
|
+
type Revivers,
|
|
26
|
+
STREAM_REF_TYPE,
|
|
27
|
+
type StreamRef,
|
|
28
|
+
truncateId,
|
|
29
|
+
} from '@workflow/core/serialization-format';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Browser-safe base64 decode
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
36
|
+
if (base64 === '' || base64 === '.') {
|
|
37
|
+
return new ArrayBuffer(0);
|
|
38
|
+
}
|
|
39
|
+
const binaryString = atob(base64);
|
|
40
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
41
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
42
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
43
|
+
}
|
|
44
|
+
return bytes.buffer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Web revivers (browser-safe, no Buffer dependency)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the web-specific revivers for hydrating serialized data.
|
|
53
|
+
*
|
|
54
|
+
* Uses `atob()` for base64 decoding (no Node.js Buffer dependency).
|
|
55
|
+
* All types are revived as real instances (Date, Map, Set, URL,
|
|
56
|
+
* URLSearchParams, Headers, Error, etc.).
|
|
57
|
+
*/
|
|
58
|
+
export function getWebRevivers(): Revivers {
|
|
59
|
+
function reviveArrayBuffer(value: string): ArrayBuffer {
|
|
60
|
+
return base64ToArrayBuffer(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
// O11y-specific revivers (streams, step functions → display objects).
|
|
65
|
+
// Spread FIRST so web-specific overrides below take precedence.
|
|
66
|
+
...observabilityRevivers,
|
|
67
|
+
|
|
68
|
+
// Binary types
|
|
69
|
+
ArrayBuffer: reviveArrayBuffer,
|
|
70
|
+
BigInt: (value: string) => BigInt(value),
|
|
71
|
+
BigInt64Array: (value: string) =>
|
|
72
|
+
new BigInt64Array(reviveArrayBuffer(value)),
|
|
73
|
+
BigUint64Array: (value: string) =>
|
|
74
|
+
new BigUint64Array(reviveArrayBuffer(value)),
|
|
75
|
+
Date: (value) => new Date(value),
|
|
76
|
+
Error: (value) => {
|
|
77
|
+
const error = new Error(value.message);
|
|
78
|
+
error.name = value.name;
|
|
79
|
+
error.stack = value.stack;
|
|
80
|
+
return error;
|
|
81
|
+
},
|
|
82
|
+
Float32Array: (value: string) => new Float32Array(reviveArrayBuffer(value)),
|
|
83
|
+
Float64Array: (value: string) => new Float64Array(reviveArrayBuffer(value)),
|
|
84
|
+
Int8Array: (value: string) => new Int8Array(reviveArrayBuffer(value)),
|
|
85
|
+
Int16Array: (value: string) => new Int16Array(reviveArrayBuffer(value)),
|
|
86
|
+
Int32Array: (value: string) => new Int32Array(reviveArrayBuffer(value)),
|
|
87
|
+
Map: (value) => new Map(value),
|
|
88
|
+
RegExp: (value) => new RegExp(value.source, value.flags),
|
|
89
|
+
Set: (value) => new Set(value),
|
|
90
|
+
Uint8Array: (value: string) => new Uint8Array(reviveArrayBuffer(value)),
|
|
91
|
+
Uint8ClampedArray: (value: string) =>
|
|
92
|
+
new Uint8ClampedArray(reviveArrayBuffer(value)),
|
|
93
|
+
Uint16Array: (value: string) => new Uint16Array(reviveArrayBuffer(value)),
|
|
94
|
+
Uint32Array: (value: string) => new Uint32Array(reviveArrayBuffer(value)),
|
|
95
|
+
|
|
96
|
+
Headers: (value) => new Headers(value),
|
|
97
|
+
URL: (value) => new URL(value),
|
|
98
|
+
URLSearchParams: (value) => new URLSearchParams(value === '.' ? '' : value),
|
|
99
|
+
|
|
100
|
+
// Web-specific overrides for class instances
|
|
101
|
+
Class: (value) => `<class:${extractClassName(value.classId)}>`,
|
|
102
|
+
Instance: (value) =>
|
|
103
|
+
new ClassInstanceRef(
|
|
104
|
+
extractClassName(value.classId),
|
|
105
|
+
value.classId,
|
|
106
|
+
value.data
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Pre-built web revivers (cached for performance)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
let cachedRevivers: Revivers | null = null;
|
|
116
|
+
|
|
117
|
+
function getRevivers(): Revivers {
|
|
118
|
+
if (!cachedRevivers) {
|
|
119
|
+
cachedRevivers = getWebRevivers();
|
|
120
|
+
}
|
|
121
|
+
return cachedRevivers;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Public API
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Hydrate the serialized data fields of a resource for web display.
|
|
130
|
+
*
|
|
131
|
+
* Uses browser-safe revivers (atob for base64, ClassInstanceRef for
|
|
132
|
+
* custom classes, StreamRef for streams). Call this on data received
|
|
133
|
+
* from the server before passing it to UI components.
|
|
134
|
+
*/
|
|
135
|
+
export function hydrateResourceIO<T>(resource: T): T {
|
|
136
|
+
return hydrateResourceIOGeneric(resource as any, getRevivers()) as T;
|
|
137
|
+
}
|