@workflow/web-shared 5.0.0-beta.16 → 5.0.0-beta.18

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 (37) hide show
  1. package/dist/components/new-trace-viewer/components/span-markers.d.ts +46 -0
  2. package/dist/components/new-trace-viewer/components/span-markers.d.ts.map +1 -0
  3. package/dist/components/new-trace-viewer/components/span-markers.js +85 -0
  4. package/dist/components/new-trace-viewer/components/timeline.d.ts +3 -1
  5. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  6. package/dist/components/new-trace-viewer/components/timeline.js +34 -51
  7. package/dist/components/new-trace-viewer/components/use-row-window.d.ts +14 -0
  8. package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -1
  9. package/dist/components/new-trace-viewer/components/use-row-window.js +39 -1
  10. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  11. package/dist/components/new-trace-viewer/trace-viewer.js +87 -23
  12. package/dist/components/new-trace-viewer/utils.d.ts +15 -0
  13. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  14. package/dist/components/new-trace-viewer/utils.js +37 -31
  15. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  16. package/dist/components/sidebar/attribute-panel.js +3 -25
  17. package/dist/components/sidebar/attributes-block.d.ts +1 -1
  18. package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
  19. package/dist/components/sidebar/attributes-block.js +5 -7
  20. package/dist/components/trace-viewer/trace-viewer.module.css +4 -4
  21. package/dist/lib/hydration.d.ts.map +1 -1
  22. package/dist/lib/hydration.js +6 -1
  23. package/dist/lib/zstd-browser-decoder.d.ts +7 -0
  24. package/dist/lib/zstd-browser-decoder.d.ts.map +1 -0
  25. package/dist/lib/zstd-browser-decoder.js +41 -0
  26. package/package.json +6 -5
  27. package/src/components/new-trace-viewer/components/span-markers.tsx +173 -0
  28. package/src/components/new-trace-viewer/components/timeline.tsx +142 -95
  29. package/src/components/new-trace-viewer/components/use-row-window.ts +47 -0
  30. package/src/components/new-trace-viewer/trace-viewer.tsx +108 -23
  31. package/src/components/new-trace-viewer/utils.test.ts +150 -0
  32. package/src/components/new-trace-viewer/utils.ts +61 -34
  33. package/src/components/sidebar/attribute-panel.tsx +5 -43
  34. package/src/components/sidebar/attributes-block.tsx +9 -18
  35. package/src/components/trace-viewer/trace-viewer.module.css +4 -4
  36. package/src/lib/hydration.ts +7 -0
  37. package/src/lib/zstd-browser-decoder.ts +42 -0
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Span, SpanEvent } from './types';
3
+ import {
4
+ computeOffscreenMarkers,
5
+ computeSpanMarkers,
6
+ computeSpanSegments,
7
+ } from './utils';
8
+
9
+ /** Build a high-res timestamp tuple ([seconds, nanoseconds]) for a given ms. */
10
+ function ts(ms: number): [number, number] {
11
+ return [Math.floor(ms / 1000), (ms % 1000) * 1e6];
12
+ }
13
+
14
+ function hookSpan(opts: {
15
+ startMs: number;
16
+ endMs: number;
17
+ receivesMs: number[];
18
+ disposedMs?: number;
19
+ attrSetMs?: number[];
20
+ }): Span {
21
+ const events: SpanEvent[] = [
22
+ { name: 'hook_created', timestamp: ts(opts.startMs), attributes: {} },
23
+ ...opts.receivesMs.map((m) => ({
24
+ name: 'hook_received',
25
+ timestamp: ts(m),
26
+ attributes: {},
27
+ })),
28
+ ...(opts.attrSetMs ?? []).map((m) => ({
29
+ name: 'attr_set',
30
+ timestamp: ts(m),
31
+ attributes: {},
32
+ })),
33
+ ...(opts.disposedMs !== undefined
34
+ ? [
35
+ {
36
+ name: 'hook_disposed',
37
+ timestamp: ts(opts.disposedMs),
38
+ attributes: {},
39
+ } satisfies SpanEvent,
40
+ ]
41
+ : []),
42
+ ];
43
+
44
+ return {
45
+ name: 'hook',
46
+ kind: 0,
47
+ resource: 'hook',
48
+ library: { name: 'workflow' },
49
+ spanId: 'hook-1',
50
+ status: { code: 1 },
51
+ traceFlags: 0,
52
+ attributes: {},
53
+ links: [],
54
+ events,
55
+ startTime: ts(opts.startMs),
56
+ endTime: ts(opts.endMs),
57
+ duration: ts(opts.endMs - opts.startMs),
58
+ };
59
+ }
60
+
61
+ describe('computeSpanSegments (hook)', () => {
62
+ it('renders a single waiting segment for a hook resumed many times but not disposed', () => {
63
+ const span = hookSpan({
64
+ startMs: 0,
65
+ endMs: 100_000,
66
+ receivesMs: [1_000, 50_000, 99_000],
67
+ });
68
+
69
+ // A hook resumed N times still re-suspends after every resumption, so the
70
+ // bar must stay "waiting" for its whole life — not flip to a filled
71
+ // "received" segment after the first receive (which hid resumptions 2..N).
72
+ expect(computeSpanSegments(span)).toEqual([
73
+ { startFraction: 0, endFraction: 1, status: 'waiting' },
74
+ ]);
75
+ });
76
+
77
+ it('ends the waiting segment at disposal and appends a succeeded tail', () => {
78
+ const span = hookSpan({
79
+ startMs: 0,
80
+ endMs: 100_000,
81
+ receivesMs: [1_000, 50_000],
82
+ disposedMs: 80_000,
83
+ });
84
+
85
+ expect(computeSpanSegments(span)).toEqual([
86
+ { startFraction: 0, endFraction: 0.8, status: 'waiting' },
87
+ { startFraction: 0.8, endFraction: 1, status: 'succeeded' },
88
+ ]);
89
+ });
90
+
91
+ it('treats a never-resolved hook as fully waiting', () => {
92
+ const span = hookSpan({ startMs: 0, endMs: 100_000, receivesMs: [] });
93
+
94
+ expect(computeSpanSegments(span)).toEqual([
95
+ { startFraction: 0, endFraction: 1, status: 'waiting' },
96
+ ]);
97
+ });
98
+ });
99
+
100
+ describe('computeSpanMarkers', () => {
101
+ it('emits one marker per resumption, including those at the temporal edges', () => {
102
+ const span = hookSpan({
103
+ startMs: 0,
104
+ endMs: 100_000,
105
+ receivesMs: [1_000, 50_000, 99_000],
106
+ });
107
+
108
+ const markers = computeSpanMarkers(span);
109
+ expect(markers.map((m) => m.timeMs)).toEqual([1_000, 50_000, 99_000]);
110
+ });
111
+
112
+ it('merges hook_received and attr_set events, sorted by time', () => {
113
+ const span = hookSpan({
114
+ startMs: 0,
115
+ endMs: 100_000,
116
+ receivesMs: [50_000],
117
+ attrSetMs: [10_000, 70_000],
118
+ });
119
+
120
+ expect(computeSpanMarkers(span).map((m) => m.timeMs)).toEqual([
121
+ 10_000, 50_000, 70_000,
122
+ ]);
123
+ });
124
+
125
+ it('returns no markers when the span has no marker events', () => {
126
+ const span = hookSpan({ startMs: 0, endMs: 100_000, receivesMs: [] });
127
+ expect(computeSpanMarkers(span)).toEqual([]);
128
+ });
129
+ });
130
+
131
+ describe('computeOffscreenMarkers', () => {
132
+ const mk = (timeMs: number) => ({ timeMs });
133
+
134
+ it('partitions markers by side with the nearest one per side', () => {
135
+ const markers = [mk(5), mk(8), mk(50), mk(92), mk(99)];
136
+ // Visible window [10, 90]: 5 & 8 off left (nearest 8), 92 & 99 off right
137
+ // (nearest 92), 50 in view.
138
+ expect(computeOffscreenMarkers(markers, 10, 90)).toEqual({
139
+ left: { count: 2, nearestMs: 8 },
140
+ right: { count: 2, nearestMs: 92 },
141
+ });
142
+ });
143
+
144
+ it('returns null for a side with nothing off-screen', () => {
145
+ expect(computeOffscreenMarkers([mk(20), mk(50)], 10, 90)).toEqual({
146
+ left: null,
147
+ right: null,
148
+ });
149
+ });
150
+ });
@@ -1,5 +1,5 @@
1
- import type { Span, SpanEvent } from './types';
2
1
  import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
2
+ import type { Span, SpanEvent } from './types';
3
3
 
4
4
  // ---------------------------------------------------------------------------
5
5
  // Root bounds
@@ -243,7 +243,7 @@ function computeStepSegmentsFromSpan(
243
243
  ]);
244
244
 
245
245
  if (marks.length === 0) {
246
- segments.push({ startFraction: 0, endFraction: 1, status: 'running' });
246
+ segments.push({ startFraction: 0, endFraction: 1, status: 'queued' });
247
247
  return segments;
248
248
  }
249
249
 
@@ -278,7 +278,7 @@ function computeStepSegmentsFromSpan(
278
278
  ? 'failed'
279
279
  : nextType === 'step_completed'
280
280
  ? 'succeeded'
281
- : 'running';
281
+ : 'retrying';
282
282
  segments.push({
283
283
  startFraction: markFrac,
284
284
  endFraction: nextFrac,
@@ -313,47 +313,20 @@ function computeHookSegmentsFromSpan(
313
313
  const segments: Segment[] = [];
314
314
  if (duration <= 0) return segments;
315
315
 
316
- const sorted = [...events]
317
- .map((e) => ({ name: e.name, time: getHighResInMs(e.timestamp) }))
318
- .sort((a, b) => a.time - b.time);
319
-
320
- const received = sorted.find((e) => e.name === 'hook_received');
321
- const disposed = sorted.find((e) => e.name === 'hook_disposed');
322
-
323
- if (!received && !disposed) {
324
- segments.push({ startFraction: 0, endFraction: 1, status: 'waiting' });
325
- return segments;
326
- }
327
-
328
- const receivedFrac = received
329
- ? timeToFraction(received.time, startMs, duration)
330
- : null;
316
+ const disposed = sortedEventMarks(events, ['hook_disposed'])[0];
331
317
  const disposedFrac = disposed
332
318
  ? timeToFraction(disposed.time, startMs, duration)
333
319
  : null;
334
320
 
335
- if (receivedFrac !== null && receivedFrac > 0.001) {
321
+ const waitingEnd = disposedFrac ?? 1;
322
+ if (waitingEnd > 0.001) {
336
323
  segments.push({
337
324
  startFraction: 0,
338
- endFraction: receivedFrac,
339
- status: 'waiting',
340
- });
341
- } else if (receivedFrac === null && disposedFrac !== null) {
342
- segments.push({
343
- startFraction: 0,
344
- endFraction: disposedFrac,
325
+ endFraction: waitingEnd,
345
326
  status: 'waiting',
346
327
  });
347
328
  }
348
329
 
349
- if (receivedFrac !== null) {
350
- segments.push({
351
- startFraction: receivedFrac,
352
- endFraction: disposedFrac ?? 1,
353
- status: 'received',
354
- });
355
- }
356
-
357
330
  if (disposedFrac !== null && disposedFrac < 0.999) {
358
331
  segments.push({
359
332
  startFraction: disposedFrac,
@@ -488,3 +461,57 @@ export function computeSpanSegments(span: Span): Segment[] {
488
461
  return [];
489
462
  }
490
463
  }
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Span markers — point-in-time events rendered as ticks on top of a bar
467
+ // ---------------------------------------------------------------------------
468
+
469
+ export interface SpanMarker {
470
+ timeMs: number;
471
+ }
472
+
473
+ // `hook_received` = a resumption; `attr_set` = attributes written mid-span.
474
+ const MARKER_EVENT_NAMES = ['hook_received', 'attr_set'];
475
+
476
+ export function computeSpanMarkers(span: Span): SpanMarker[] {
477
+ return sortedEventMarks(span.events, MARKER_EVENT_NAMES).map((mark) => ({
478
+ timeMs: mark.time,
479
+ }));
480
+ }
481
+
482
+ export interface OffscreenSide {
483
+ count: number;
484
+ /** Nearest off-screen marker — the one a reveal jumps to. */
485
+ nearestMs: number;
486
+ }
487
+
488
+ export interface OffscreenMarkers {
489
+ left: OffscreenSide | null;
490
+ right: OffscreenSide | null;
491
+ }
492
+
493
+ /** Partition markers outside `[visibleStartMs, visibleEndMs]` by side. */
494
+ export function computeOffscreenMarkers(
495
+ markers: SpanMarker[],
496
+ visibleStartMs: number,
497
+ visibleEndMs: number
498
+ ): OffscreenMarkers {
499
+ let leftCount = 0;
500
+ let rightCount = 0;
501
+ let nearestLeft = Number.NEGATIVE_INFINITY;
502
+ let nearestRight = Number.POSITIVE_INFINITY;
503
+ for (const { timeMs } of markers) {
504
+ if (timeMs < visibleStartMs) {
505
+ leftCount++;
506
+ if (timeMs > nearestLeft) nearestLeft = timeMs;
507
+ } else if (timeMs > visibleEndMs) {
508
+ rightCount++;
509
+ if (timeMs < nearestRight) nearestRight = timeMs;
510
+ }
511
+ }
512
+ return {
513
+ left: leftCount > 0 ? { count: leftCount, nearestMs: nearestLeft } : null,
514
+ right:
515
+ rightCount > 0 ? { count: rightCount, nearestMs: nearestRight } : null,
516
+ };
517
+ }
@@ -7,7 +7,6 @@ import { format } from 'date-fns';
7
7
  import type { KeyboardEvent, ReactNode } from 'react';
8
8
  import { useCallback, useContext, useMemo, useState } from 'react';
9
9
  import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
10
- import { useToast } from '../../lib/toast';
11
10
  import { extractConversation, isDoStreamStep } from '../../lib/utils';
12
11
  import { CopyButton } from '../new-trace-viewer/components/copy-button';
13
12
  import { MiddleTruncate } from '../new-trace-viewer/components/middle-truncate/middle-truncate';
@@ -616,6 +615,7 @@ const copyableBasicAttributes = new Set<AttributeKey>([
616
615
  'hookId',
617
616
  'eventId',
618
617
  'deploymentId',
618
+ 'moduleSpecifier',
619
619
  ]);
620
620
 
621
621
  export const AttributeBlock = ({
@@ -733,7 +733,6 @@ export const AttributePanel = ({
733
733
  /** Resource type of the selected span — used to show targeted loading skeletons. */
734
734
  resource?: string;
735
735
  }) => {
736
- const toast = useToast();
737
736
  // Extract workflowCoreVersion from executionContext for display
738
737
  const displayData = useMemo(() => {
739
738
  const result = { ...data };
@@ -819,17 +818,6 @@ export const AttributePanel = ({
819
818
  }),
820
819
  [displayData.stepName]
821
820
  );
822
- const handleCopyModuleSpecifier = useCallback((value: string) => {
823
- navigator.clipboard
824
- .writeText(value)
825
- .then(() => {
826
- toast.success('moduleSpecifier copied');
827
- })
828
- .catch(() => {
829
- toast.error('Failed to copy moduleSpecifier');
830
- });
831
- }, []);
832
-
833
821
  const outerDecryptCtx = useContext(DecryptClickContext);
834
822
  const decryptValue = onDecrypt
835
823
  ? {
@@ -850,16 +838,9 @@ export const AttributePanel = ({
850
838
  const displayValue = attributeToDisplayFn[
851
839
  attribute as keyof typeof attributeToDisplayFn
852
840
  ]?.(displayData[attribute as keyof typeof displayData]);
853
- const isModuleSpecifier = attribute === 'moduleSpecifier';
854
841
  const isCopyableBasicAttribute =
855
842
  copyableBasicAttributes.has(attribute as AttributeKey) &&
856
843
  typeof displayValue === 'string';
857
- const moduleSpecifierValue =
858
- typeof displayValue === 'string'
859
- ? displayValue
860
- : String(
861
- displayValue ?? displayData.moduleSpecifier ?? ''
862
- );
863
844
 
864
845
  return (
865
846
  <div
@@ -869,34 +850,15 @@ export const AttributePanel = ({
869
850
  <span className="text-label-14 text-gray-900">
870
851
  {getAttributeDisplayName(attribute)}
871
852
  </span>
872
- {isModuleSpecifier ? (
873
- <button
874
- type="button"
875
- className="min-w-0 max-w-[70%] truncate text-right text-label-13 font-mono"
876
- style={{
877
- color: 'var(--ds-gray-1000)',
878
- background: 'transparent',
879
- border: 'none',
880
- padding: 0,
881
- }}
882
- title={moduleSpecifierValue}
883
- onClick={() =>
884
- handleCopyModuleSpecifier(moduleSpecifierValue)
885
- }
886
- >
887
- {moduleSpecifierValue}
888
- </button>
889
- ) : isCopyableBasicAttribute ? (
853
+ {isCopyableBasicAttribute ? (
890
854
  <div
891
- className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-right text-[13px] font-mono"
892
- style={{
893
- color: 'var(--ds-gray-1000)',
894
- }}
855
+ className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-[13px] font-mono text-gray-1000"
895
856
  title={displayValue}
896
857
  >
897
858
  <MiddleTruncate
898
859
  value={displayValue}
899
- className="flex-1"
860
+ className="text-right"
861
+ style={{ gridTemplateColumns: 'minmax(0, 1fr)' }}
900
862
  />
901
863
  <CopyButton
902
864
  copyText={displayValue}
@@ -41,26 +41,21 @@ function AttributeRow({
41
41
  }) {
42
42
  const reserved = isReservedAttributeKey(attributeKey);
43
43
  return (
44
- <div className="flex items-center justify-between gap-3 py-1.5">
44
+ <div className="flex items-center justify-between gap-3 py-0.5">
45
45
  <span
46
- className="flex min-w-0 items-center gap-1.5 text-label-12 font-mono"
47
- style={{
48
- color: reserved ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)',
49
- }}
46
+ className="flex min-w-0 items-center gap-1.5 text-label-13 text-gray-900"
47
+ style={reserved ? { color: 'var(--ds-gray-700)' } : undefined}
50
48
  >
51
49
  <span className="truncate">{attributeKey}</span>
52
50
  {reserved && <ReservedBadge />}
53
51
  </span>
54
52
  {removed ? (
55
- <span
56
- className="shrink-0 text-label-12 italic"
57
- style={{ color: 'var(--ds-gray-700)' }}
58
- >
53
+ <span className="shrink-0 text-copy-13 italic text-gray-700">
59
54
  removed
60
55
  </span>
61
56
  ) : (
62
57
  <span
63
- className="max-w-[60%] truncate text-right text-label-12 font-mono text-gray-1000"
58
+ className="max-w-[60%] truncate text-copy-13 text-gray-1000"
64
59
  title={value}
65
60
  >
66
61
  {value}
@@ -85,7 +80,7 @@ export function sortAttributeKeys(keys: string[]): string[] {
85
80
  }
86
81
 
87
82
  /**
88
- * Collapsible card showing a run's materialized attributes as key-value
83
+ * Collapsible section showing a run's materialized attributes as key-value
89
84
  * rows. Reserved (`$`-prefixed) keys are visually de-emphasized with a
90
85
  * badge and sorted after user keys.
91
86
  */
@@ -98,12 +93,8 @@ export function RunAttributesCard({
98
93
  if (keys.length === 0) return null;
99
94
 
100
95
  return (
101
- <DetailCard
102
- summary={`Attributes (${keys.length})`}
103
- defaultOpen
104
- contentClassName="mb-0"
105
- >
106
- <div className="flex flex-col divide-y divide-gray-alpha-400">
96
+ <DetailCard summary="Attributes" defaultOpen contentClassName="mb-4">
97
+ <div className="flex flex-col">
107
98
  {keys.map((key) => (
108
99
  <AttributeRow attributeKey={key} key={key} value={attributes[key]} />
109
100
  ))}
@@ -156,7 +147,7 @@ export function AttrSetEventBlock({ data }: { data: unknown }) {
156
147
 
157
148
  return (
158
149
  <div className="flex flex-col px-3 py-1">
159
- <div className="flex flex-col divide-y divide-gray-alpha-400">
150
+ <div className="flex flex-col">
160
151
  {data.changes.map((change, index) => (
161
152
  <AttributeRow
162
153
  attributeKey={change.key}
@@ -1314,10 +1314,10 @@
1314
1314
  .segQueued {
1315
1315
  background: repeating-linear-gradient(
1316
1316
  45deg,
1317
- var(--ds-gray-alpha-200),
1318
- var(--ds-gray-alpha-200) 6px,
1319
- var(--ds-gray-alpha-100) 6px,
1320
- var(--ds-gray-alpha-100) 12px
1317
+ var(--ds-gray-200),
1318
+ var(--ds-gray-200) 6px,
1319
+ var(--ds-gray-100) 6px,
1320
+ var(--ds-gray-100) 12px
1321
1321
  );
1322
1322
  }
1323
1323
 
@@ -476,6 +476,13 @@ export async function hydrateResourceIOWithKey<T>(
476
476
  '@workflow/core/serialization-format'
477
477
  );
478
478
  const { importKey } = await import('@workflow/core/encryption');
479
+ // Payloads may be zstd-compressed (the Web DecompressionStream has no zstd);
480
+ // register the WASM-backed browser decoder before hydrating. Idempotent and
481
+ // lazy — the WASM is only compiled when a zstd payload is actually decoded.
482
+ const { ensureZstdDecoderRegistered } = await import(
483
+ './zstd-browser-decoder.js'
484
+ );
485
+ ensureZstdDecoderRegistered();
479
486
  const cryptoKey = await importKey(key);
480
487
  const revivers = getRevivers();
481
488
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Browser zstd decoder for the o11y read path.
3
+ *
4
+ * The Web `DecompressionStream` has no zstd support, so `@workflow/core`'s
5
+ * `hydrateDataWithKey` delegates zstd inflation to a decoder registered via
6
+ * `registerZstdDecoder`. This module supplies that decoder, backed by the
7
+ * `@tootallnate/zstd-wasm` single-file WASM decoder.
8
+ *
9
+ * The package leaves WASM sourcing to the caller; we resolve the shipped
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
12
+ * WASM is fetched only the first time a zstd payload is actually decoded.
13
+ */
14
+ import { registerZstdDecoder } from '@workflow/core/serialization-format';
15
+
16
+ let registered = false;
17
+ let modulePromise: Promise<WebAssembly.Module> | undefined;
18
+
19
+ function loadWasmModule(): Promise<WebAssembly.Module> {
20
+ if (!modulePromise) {
21
+ const url = new URL('@tootallnate/zstd-wasm/zstd.wasm', import.meta.url);
22
+ modulePromise = fetch(url)
23
+ .then((res) => res.arrayBuffer())
24
+ .then((bytes) => WebAssembly.compile(bytes));
25
+ }
26
+ return modulePromise;
27
+ }
28
+
29
+ /**
30
+ * Register the browser zstd decoder with `@workflow/core` (idempotent).
31
+ * Call this before hydrating payloads that may be zstd-compressed; the
32
+ * actual WASM compile + decode happens lazily on first use.
33
+ */
34
+ export function ensureZstdDecoderRegistered(): void {
35
+ if (registered) return;
36
+ registered = true;
37
+ registerZstdDecoder(async (payload) => {
38
+ const { decompressBytes } = await import('@tootallnate/zstd-wasm');
39
+ const wasmModule = await loadWasmModule();
40
+ return decompressBytes(wasmModule, payload);
41
+ });
42
+ }