From fb7d660475717099fe5398a9411b5b225262ff8f Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 9 Apr 2026 15:02:53 -0600 Subject: [PATCH 01/30] refactor: replace PayloadDecoder and MetadataDecoder with unified Payload component Consolidates two divergent payload display components into a single `` component at `src/lib/components/payload.svelte`. Previously, `payload-decoder.svelte` (Svelte 5 runes, required a `children` snippet, always decoded full attribute trees) and `metadata-decoder.svelte` (Svelte 4 options API with slot syntax, decoded single payloads to truncated summary strings) served different display purposes but used inconsistent APIs and decoding paths across 65+ call sites. The new component unifies both under a single `mode` prop: - `code-block` (default): renders a CodeBlock with the decoded value, replacing all PayloadDecoder + CodeBlock snippet patterns - `summary`: truncates to 120 chars and renders a Badge, replacing all MetadataDecoder usages - `inline-truncated`: renders the compact .payload pre block used in event detail rows - `children` snippet: escape hatch for callers that need custom rendering (schedule input, timeline SVG text elements) Both old components used a single decoding path internally (cloneAllPotentialPayloadsWithCodec -> decodePayloadAttributes -> stringifyWithBigInt). MetadataDecoder previously used a separate decodeSingleReadablePayloadWithCodec path; the new component uses the same unified path for consistency. The component is written in Svelte 5 runes throughout and imports from $app/state instead of $app/stores, matching the newer direction of the codebase. Migrated files: - src/lib/components/event/event-card.svelte - src/lib/components/event/event-details-row.svelte (+ moved .payload CSS) - src/lib/components/event/event-metadata-expanded.svelte - src/lib/components/event/event-summary-row.svelte - src/lib/components/lines-and-dots/svg/group-details-text.svelte - src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte - src/lib/components/schedule/schedule-form/schedule-input-payload.svelte - src/lib/components/standalone-activities/activity-input-and-outcome.svelte - src/lib/components/workflow/client-actions/update-confirmation-modal.svelte - src/lib/components/workflow/input-and-results-payload.svelte - src/lib/components/workflow/metadata/metadata-events.svelte - src/lib/components/workflow/pending-activity/pending-activity-card.svelte - src/lib/components/workflow/workflow-json-navigator.svelte - src/lib/pages/standalone-activity-details.svelte - src/lib/pages/standalone-activity-search-attributes.svelte - src/lib/pages/workflow-memo.svelte - src/lib/pages/workflow-metadata.svelte - src/lib/pages/workflow-search-attributes.svelte All 1730 tests pass. --- src/lib/components/event/event-card.svelte | 71 +++---- .../components/event/event-details-row.svelte | 31 +-- .../event/event-metadata-expanded.svelte | 23 +-- .../components/event/event-summary-row.svelte | 25 +-- .../components/event/metadata-decoder.svelte | 69 ------- .../components/event/payload-decoder.svelte | 72 ------- .../svg/group-details-text.svelte | 48 ++--- .../svg/timeline-graph-row.svelte | 57 +++--- src/lib/components/payload.svelte | 184 ++++++++++++++++++ .../schedule-input-payload.svelte | 36 ++-- .../activity-input-and-outcome.svelte | 15 +- .../update-confirmation-modal.svelte | 51 ++--- .../workflow/input-and-results-payload.svelte | 37 ++-- .../workflow/metadata/metadata-events.svelte | 14 +- .../pending-activity-card.svelte | 37 ++-- .../workflow/workflow-json-navigator.svelte | 19 +- .../pages/standalone-activity-details.svelte | 14 +- ...andalone-activity-search-attributes.svelte | 17 +- src/lib/pages/workflow-memo.svelte | 18 +- src/lib/pages/workflow-metadata.svelte | 32 ++- .../pages/workflow-search-attributes.svelte | 17 +- 21 files changed, 413 insertions(+), 474 deletions(-) delete mode 100644 src/lib/components/event/metadata-decoder.svelte delete mode 100644 src/lib/components/event/payload-decoder.svelte create mode 100644 src/lib/components/payload.svelte diff --git a/src/lib/components/event/event-card.svelte b/src/lib/components/event/event-card.svelte index d183f3fc4a..0201ea3d8b 100644 --- a/src/lib/components/event/event-card.svelte +++ b/src/lib/components/event/event-card.svelte @@ -1,13 +1,13 @@

Summary

- - - {decodedValue} - - + + {#snippet children(decodedValue)} + + {decodedValue} + + {/snippet} +
diff --git a/src/lib/components/event/event-summary-row.svelte b/src/lib/components/event/event-summary-row.svelte index 9c9417b833..611f4b56b7 100644 --- a/src/lib/components/event/event-summary-row.svelte +++ b/src/lib/components/event/event-summary-row.svelte @@ -4,6 +4,7 @@ import { page } from '$app/state'; + import Payload from '$lib/components/payload.svelte'; import { timestamp } from '$lib/components/timestamp.svelte'; import Badge from '$lib/holocene/badge.svelte'; import Copyable from '$lib/holocene/copyable/index.svelte'; @@ -42,7 +43,6 @@ import EventDetailsFull from './event-details-full.svelte'; import EventDetailsRow from './event-details-row.svelte'; import EventLink from './event-link.svelte'; - import MetadataDecoder from './metadata-decoder.svelte'; interface Props { event: IterableEvent; @@ -326,21 +326,16 @@ {/if} {#if currentEvent?.userMetadata?.summary} - - {#if decodedValue} -
-

Summary

- - {decodedValue} - -
- {/if} -
+

Summary

+ + {/if} {#if currentEvent?.links?.length} - import { page } from '$app/stores'; - - import type { Payload } from '$lib/types'; - import { decodeSingleReadablePayloadWithCodec } from '$lib/utilities/decode-payload'; - import { - getCodecEndpoint, - getCodecIncludeCredentials, - getCodecPassAccessToken, - } from '$lib/utilities/get-codec'; - - export let value: Payload | undefined = undefined; - export let fallback: string = ''; - export let prefix: string = ''; - export let onDecode: (decodedValue: string) => void | undefined = undefined; - - const maxLength = 120; - - let decodedValue = ''; - - $: endpoint = getCodecEndpoint($page.data.settings); - $: passAccessToken = getCodecPassAccessToken($page.data.settings); - $: includeCredentials = getCodecIncludeCredentials($page.data.settings); - $: settings = { - ...$page.data.settings, - codec: { - ...$page.data.settings?.codec, - endpoint, - passAccessToken, - includeCredentials, - }, - }; - - const setPrefix = (metadata: string) => { - if (prefix) { - metadata = `${prefix} • ${metadata}`; - if (metadata.length < maxLength) return metadata; - return metadata.slice(0, maxLength) + '...'; - } - return metadata; - }; - - $: decodePayload = async (_value: Payload | undefined) => { - if (!_value) return fallback; - if (decodedValue) return decodedValue; - - const metadata = await decodeSingleReadablePayloadWithCodec( - _value, - settings, - ); - - if (typeof metadata === 'string') { - decodedValue = setPrefix(metadata); - if (onDecode) { - onDecode(decodedValue); - } - return decodedValue; - } - - decodedValue = fallback; - return fallback; - }; - - -{#await decodePayload(value) then metadata} - -{:catch} - -{/await} diff --git a/src/lib/components/event/payload-decoder.svelte b/src/lib/components/event/payload-decoder.svelte deleted file mode 100644 index 9161469c1a..0000000000 --- a/src/lib/components/event/payload-decoder.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - -{@render children(decodedValue)} diff --git a/src/lib/components/lines-and-dots/svg/group-details-text.svelte b/src/lib/components/lines-and-dots/svg/group-details-text.svelte index 3684152789..abda78cb3d 100644 --- a/src/lib/components/lines-and-dots/svg/group-details-text.svelte +++ b/src/lib/components/lines-and-dots/svg/group-details-text.svelte @@ -1,7 +1,6 @@ + +{#if children} + {@render children(decodedValue)} +{:else if mode === 'code-block'} + +{:else if mode === 'summary'} + + {decodedValue || fallback} + +{:else if mode === 'inline-truncated'} +
+ +
{decodedValue.slice(0, truncateAt)}
+
+
+{/if} + + diff --git a/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte b/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte index 0ba8dab179..1e5019236a 100644 --- a/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte +++ b/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte @@ -1,8 +1,8 @@
- - -
- -
-
-
+ + {#snippet children(_decodedValue)} + +
+ +
+
+ {/snippet} +
diff --git a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte index 8c6ffc8ed6..76595789a6 100644 --- a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte +++ b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte @@ -1,11 +1,10 @@ - -
-

Summary

- - {#snippet children(decodedValue)} - - {decodedValue} - - {/snippet} - -
diff --git a/src/lib/components/payload.svelte b/src/lib/components/payload.svelte index 87f44540e7..ad3909af32 100644 --- a/src/lib/components/payload.svelte +++ b/src/lib/components/payload.svelte @@ -162,23 +162,11 @@ {decodedValue || fallback} {:else if mode === 'inline-truncated'} -
+
{decodedValue.slice(0, truncateAt)}
{/if} - - From 5a5794f4924863d8971ffab486bd482aea39d742 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Mon, 13 Apr 2026 11:50:46 -0600 Subject: [PATCH 03/30] refactor: move Payload component into payload/ directory Moves payload.svelte into src/lib/components/payload/ and adds a USAGE.md report documenting all 31 call sites grouped by feature area. Updates all 17 import paths accordingly. --- src/lib/components/event/event-card.svelte | 2 +- .../components/event/event-details-row.svelte | 2 +- .../components/event/event-summary-row.svelte | 2 +- .../svg/group-details-text.svelte | 2 +- .../svg/timeline-graph-row.svelte | 2 +- src/lib/components/payload/USAGE.md | 121 ++++++++++++++++++ .../components/{ => payload}/payload.svelte | 0 .../schedule-input-payload.svelte | 2 +- .../activity-input-and-outcome.svelte | 2 +- .../update-confirmation-modal.svelte | 2 +- .../workflow/input-and-results-payload.svelte | 2 +- .../workflow/metadata/metadata-events.svelte | 2 +- .../pending-activity-card.svelte | 2 +- .../workflow/workflow-json-navigator.svelte | 2 +- .../pages/standalone-activity-details.svelte | 2 +- ...andalone-activity-search-attributes.svelte | 2 +- src/lib/pages/workflow-memo.svelte | 2 +- src/lib/pages/workflow-metadata.svelte | 2 +- .../pages/workflow-search-attributes.svelte | 2 +- 19 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 src/lib/components/payload/USAGE.md rename src/lib/components/{ => payload}/payload.svelte (100%) diff --git a/src/lib/components/event/event-card.svelte b/src/lib/components/event/event-card.svelte index 0201ea3d8b..9c4f9c36fe 100644 --- a/src/lib/components/event/event-card.svelte +++ b/src/lib/components/event/event-card.svelte @@ -1,7 +1,7 @@ + + diff --git a/src/lib/components/payload/payload-decoder.svelte b/src/lib/components/payload/payload-decoder.svelte new file mode 100644 index 0000000000..390899bb3d --- /dev/null +++ b/src/lib/components/payload/payload-decoder.svelte @@ -0,0 +1,62 @@ + + +{@render children(decodedValue)} diff --git a/src/lib/components/payload/payload-inline.svelte b/src/lib/components/payload/payload-inline.svelte new file mode 100644 index 0000000000..2c24ec4634 --- /dev/null +++ b/src/lib/components/payload/payload-inline.svelte @@ -0,0 +1,72 @@ + + +
+ +
{decodedValue.slice(0, truncateAt)}
+
+
diff --git a/src/lib/components/payload/payload-summary.svelte b/src/lib/components/payload/payload-summary.svelte new file mode 100644 index 0000000000..a143b687de --- /dev/null +++ b/src/lib/components/payload/payload-summary.svelte @@ -0,0 +1,63 @@ + + +{#if children} + {@render children(decodedValue)} +{:else if className} + {decodedValue || fallback} +{:else} + {decodedValue || fallback} +{/if} diff --git a/src/lib/components/payload/payload.svelte b/src/lib/components/payload/payload.svelte deleted file mode 100644 index ad3909af32..0000000000 --- a/src/lib/components/payload/payload.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - -{#if children} - {@render children(decodedValue)} -{:else if mode === 'code-block'} - -{:else if mode === 'summary'} - - {decodedValue || fallback} - -{:else if mode === 'inline-truncated'} -
- -
{decodedValue.slice(0, truncateAt)}
-
-
-{/if} diff --git a/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte b/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte index 6eb815d39a..a8087a64f8 100644 --- a/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte +++ b/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte @@ -1,7 +1,7 @@
- + {#snippet children(_decodedValue)} {/snippet} - +
diff --git a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte index fe988d1315..19250cd260 100644 --- a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte +++ b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte @@ -1,5 +1,5 @@ diff --git a/src/lib/components/payload/payload-decoder.svelte b/src/lib/components/payload/payload-decoder.svelte index dca373bc3d..3a7b5dcd44 100644 --- a/src/lib/components/payload/payload-decoder.svelte +++ b/src/lib/components/payload/payload-decoder.svelte @@ -1,5 +1,5 @@ diff --git a/src/lib/components/payload/payload-inline.svelte b/src/lib/components/payload/payload-inline.svelte index 99b3410639..63da284051 100644 --- a/src/lib/components/payload/payload-inline.svelte +++ b/src/lib/components/payload/payload-inline.svelte @@ -1,6 +1,4 @@ diff --git a/src/lib/components/payload/payload-summary.svelte b/src/lib/components/payload/payload-summary.svelte index 23e8765b2e..807db2208b 100644 --- a/src/lib/components/payload/payload-summary.svelte +++ b/src/lib/components/payload/payload-summary.svelte @@ -1,5 +1,5 @@ From 1265db8b9819b3de34e993274fa9b59a48cbb527 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 21 Apr 2026 09:43:49 -0600 Subject: [PATCH 10/30] add workflow with user metadata --- package.json | 3 +- scripts/workflows.ts | 13 -------- .../workflow/metadata/metadata-events.svelte | 7 ++--- temporal/client.ts | 24 +++++++++++--- temporal/{workers.ts => worker.ts} | 5 +++ temporal/workflows.ts | 31 +++++++++++++++++++ 6 files changed, 59 insertions(+), 24 deletions(-) delete mode 100644 scripts/workflows.ts rename temporal/{workers.ts => worker.ts} (95%) diff --git a/package.json b/package.json index a07d3df26f..a07036f665 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "stylelint": "stylelint \"src/**/*.{css,postcss,svelte}\"", "stylelint:fix": "stylelint --fix \"src/**/*.{css,postcss,svelte}\"", "generate:locales": "esno scripts/generate-locales.ts", - "workflows": "esno scripts/workflows.ts", + "worker": "esno temporal/worker.ts", + "workflows": "esno temporal/client.ts", "audit:tailwind": "esno scripts/audit-tailwind-colors", "audit:holocene-props": "esno scripts/generate-holocene-props.ts", "validate:versions": "./scripts/validate-versions.sh" diff --git a/scripts/workflows.ts b/scripts/workflows.ts deleted file mode 100644 index 418304f9d1..0000000000 --- a/scripts/workflows.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { connect, startWorkflows } from '../temporal/client'; -import { runWorkerUntil } from '../temporal/workers'; - -async function main() { - const client = await connect(); - const results = startWorkflows(client); - await runWorkerUntil(results); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/src/lib/components/workflow/metadata/metadata-events.svelte b/src/lib/components/workflow/metadata/metadata-events.svelte index d98ce3819f..695c1a2e25 100644 --- a/src/lib/components/workflow/metadata/metadata-events.svelte +++ b/src/lib/components/workflow/metadata/metadata-events.svelte @@ -31,11 +31,8 @@ - {#snippet children(decodedValue)} - {decodedValue} - {/snippet} - + class="text-sm" + />
diff --git a/temporal/client.ts b/temporal/client.ts index 1f69d0b40e..0d386de1f1 100644 --- a/temporal/client.ts +++ b/temporal/client.ts @@ -5,6 +5,7 @@ import { BlockingWorkflow, CompletedWorkflow, RunningWorkflow, + UserMetadataWorkflow, Workflow, } from './workflows'; @@ -29,9 +30,9 @@ export const connect = async () => { const workflows: WorkflowHandle[] = []; -export const startWorkflows = async ( - client: Client, -): Promise<(string | number | void)[]> => { +export const startWorkflows = async (): Promise<(string | number | void)[]> => { + const client = await connect(); + const wf1 = await client.workflow.start(Workflow, { taskQueue: 'e2e-1', args: ['Plain text input 1'], @@ -56,9 +57,17 @@ export const startWorkflows = async ( workflowId: 'running-workflow', }); - workflows.push(wf1, wf2, wf3, wf4); + const wf5 = await client.workflow.start(UserMetadataWorkflow, { + taskQueue: 'e2e-1', + args: ['Plain text input 5'], + workflowId: 'user-metadata-workflow', + staticSummary: '# Summary\n **this is the summary**', + staticDetails: '# Details\n **these are the details**', + }); - return Promise.all([wf1.result(), wf3.result()]); + workflows.push(wf1, wf2, wf3, wf4, wf5); + + return Promise.all([wf1.result(), wf3.result(), wf5.result()]); }; export const stopWorkflows = (): Promise => { @@ -71,3 +80,8 @@ export const stopWorkflows = (): Promise => { }), ); }; + +startWorkflows().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/temporal/workers.ts b/temporal/worker.ts similarity index 95% rename from temporal/workers.ts rename to temporal/worker.ts index 93583f821f..0c09c938af 100644 --- a/temporal/workers.ts +++ b/temporal/worker.ts @@ -66,3 +66,8 @@ export const stopWorker = async (): Promise => { }); } }; + +runWorker().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/temporal/workflows.ts b/temporal/workflows.ts index e27d5b757f..db4e5656c1 100644 --- a/temporal/workflows.ts +++ b/temporal/workflows.ts @@ -14,6 +14,7 @@ const { echo: LocalActivity } = workflow.proxyLocalActivities< const isBlockedQuery = workflow.defineQuery('is-blocked'); const unblockSignal = workflow.defineSignal('unblock'); +const proceedSignal = workflow.defineSignal('proceed'); const { double } = workflow.proxyActivities({ startToCloseTimeout: '1 hour', @@ -63,3 +64,33 @@ export async function CompletedWorkflow( export async function RunningWorkflow(): Promise { return await workflow.sleep('10 days'); } + +export async function UserMetadataWorkflow(input: string): Promise { + let signalReceived = false; + + workflow.setHandler(proceedSignal, () => void (signalReceived = true)); + + workflow.setCurrentDetails( + `# Paused at checkpoint.\n Send 'proceed' signal to continue, or workflow will auto-proceed after 10 minutes. Input: ${input}`, + ); + + await workflow.condition(() => signalReceived, '10 minutes', { + summary: 'Sleeping for 10 minutes', + }); + + const currentDetails = workflow.getCurrentDetails(); + console.log(`Current details: ${currentDetails}`); + + workflow.setCurrentDetails( + signalReceived + ? 'Received proceed signal, continuing execution' + : 'Timed out after 10 minutes, continuing execution', + ); + + return await Activity.executeWithOptions( + { + summary: '# This is the summary', + }, + [input], + ); +} From f38770f2ee6a62026ac14765230f91f4f9350127 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 21 Apr 2026 15:38:04 -0600 Subject: [PATCH 11/30] remove unnecessary props from payload code block --- src/lib/components/event/event-card.svelte | 26 +-- .../payload}/decode-payload-value.ts | 0 .../payload/payload-code-block.svelte | 25 +-- .../components/payload/payload-decoder.svelte | 2 +- .../components/payload/payload-inline.svelte | 2 +- .../workflow/input-and-results-payload.svelte | 2 - .../pending-activity-card.svelte | 4 - .../workflow/workflow-json-navigator.svelte | 7 +- ...andalone-activity-search-attributes.svelte | 2 - src/lib/pages/workflow-memo.svelte | 7 +- src/lib/pages/workflow-metadata.svelte | 9 +- .../pages/workflow-search-attributes.svelte | 2 - src/lib/utilities/decode-payload-report.md | 194 ------------------ 13 files changed, 17 insertions(+), 265 deletions(-) rename src/lib/{utilities => components/payload}/decode-payload-value.ts (100%) delete mode 100644 src/lib/utilities/decode-payload-report.md diff --git a/src/lib/components/event/event-card.svelte b/src/lib/components/event/event-card.svelte index e63419597c..95f1d0659d 100644 --- a/src/lib/components/event/event-card.svelte +++ b/src/lib/components/event/event-card.svelte @@ -4,6 +4,7 @@ import PayloadCodeBlock from '$lib/components/payload/payload-code-block.svelte'; import PayloadSummary from '$lib/components/payload/payload-summary.svelte'; import Timestamp from '$lib/components/timestamp.svelte'; + import CodeBlock from '$lib/holocene/code-block.svelte'; import Copyable from '$lib/holocene/copyable/index.svelte'; import Link from '$lib/holocene/link.svelte'; import { translate } from '$lib/i18n/translate'; @@ -179,28 +180,15 @@ {format(key)}

{#if value?.payloads} - + {:else if key === 'searchAttributes'} {:else} - + {/if} {#if stackTrace} @@ -208,13 +196,7 @@

{translate('workflows.call-stack-tab')}

- + {/if} {/snippet} diff --git a/src/lib/utilities/decode-payload-value.ts b/src/lib/components/payload/decode-payload-value.ts similarity index 100% rename from src/lib/utilities/decode-payload-value.ts rename to src/lib/components/payload/decode-payload-value.ts diff --git a/src/lib/components/payload/payload-code-block.svelte b/src/lib/components/payload/payload-code-block.svelte index 90c0501e9c..5e9c95a722 100644 --- a/src/lib/components/payload/payload-code-block.svelte +++ b/src/lib/components/payload/payload-code-block.svelte @@ -1,33 +1,24 @@ - -{#if typeof value === 'object'} - {#if value?.payloads} - - {:else if key === 'searchAttributes'} - - {:else} - - {/if} -{:else if linkType !== 'none'} - -{:else} - {value} -{/if} diff --git a/src/lib/components/payload/payload-code-block.svelte b/src/lib/components/payload/payload-code-block.svelte index 5e9c95a722..65b8a17ca4 100644 --- a/src/lib/components/payload/payload-code-block.svelte +++ b/src/lib/components/payload/payload-code-block.svelte @@ -1,45 +1,35 @@ - + + {#snippet children(decodedValue)} +
+ {#each decodedValue as data (data)} + + {/each} +
+ {/snippet} +
diff --git a/src/lib/components/payload/payload-decoder.svelte b/src/lib/components/payload/payload-decoder.svelte index 0f2f718f76..8812364dd2 100644 --- a/src/lib/components/payload/payload-decoder.svelte +++ b/src/lib/components/payload/payload-decoder.svelte @@ -2,33 +2,52 @@ import { type Snippet } from 'svelte'; import { - type DecodableValue, - decodePayloadValue, - getInitialPayloadValue, - } from './decode-payload-value'; + decodeEventAttributes, + decodePayloadAndParseDataToJSON, + decodePayloadsAndParseDataToJSON, + isRawPayload, + isRawPayloads, + type PayloadContainingObject, + type PotentiallyDecodable, + } from '$lib/utilities/decode-payload'; + import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int'; + + export const decodePayloadValue = async ( + value: PotentiallyDecodable | PayloadContainingObject, + ): Promise => { + if (isRawPayload(value)) { + const decodedPayloadData = await decodePayloadAndParseDataToJSON(value); + const stringified = stringifyWithBigInt(decodedPayloadData); + onDecode?.([stringified]); + return [stringified]; + } else if (isRawPayloads(value)) { + const parsedPayloadsData = await decodePayloadsAndParseDataToJSON(value); + const stringified = parsedPayloadsData.map((data) => + stringifyWithBigInt(data), + ); + onDecode?.(stringified); + return stringified; + } else { + const decoded = await decodeEventAttributes(value); + console.log(decoded); + const stringified = stringifyWithBigInt(decoded); + onDecode?.([stringified]); + return [stringified]; + } + }; interface Props { - value: DecodableValue; - fieldName?: string; - onDecode?: (decodedValue: string) => void; - children: Snippet<[decodedValue: string]>; + value: PotentiallyDecodable | PayloadContainingObject; + onDecode?: (decodedPayloads: string[]) => void; + children: Snippet<[decodedPayloads: string[]]>; + loading?: Snippet; } - let { value, fieldName = '', onDecode, children }: Props = $props(); - - let decodedValue = $state(getInitialPayloadValue(value, fieldName)); - - $effect(() => { - if (!value) return; - decodePayloadValue(value, fieldName) - .then((result) => { - decodedValue = result; - onDecode?.(result); - }) - .catch(() => { - console.error('Could not decode payloads'); - }); - }); + let { value, onDecode, children, loading }: Props = $props(); -{@render children(decodedValue)} +{#await decodePayloadValue(value)} + {@render loading?.()} +{:then decodedValue} + {@render children(decodedValue)} +{/await} diff --git a/src/lib/components/payload/payload-inline.svelte b/src/lib/components/payload/payload-inline.svelte index b4ea5a871b..a7ae23fffc 100644 --- a/src/lib/components/payload/payload-inline.svelte +++ b/src/lib/components/payload/payload-inline.svelte @@ -1,42 +1,25 @@ -
- -
{decodedValue.slice(0, truncateAt)}
-
-
+ + {#snippet children(decodedValue)} +
+ +
{decodedValue.slice(0, truncateAt)}
+
+
+ {/snippet} +
diff --git a/src/lib/components/payload/payload-summary.svelte b/src/lib/components/payload/payload-summary.svelte index 807db2208b..e8e15f6f86 100644 --- a/src/lib/components/payload/payload-summary.svelte +++ b/src/lib/components/payload/payload-summary.svelte @@ -2,12 +2,11 @@ import { type Snippet } from 'svelte'; import Badge from '$lib/holocene/badge.svelte'; - import type { Payload as RawPayload } from '$lib/types'; - import type { Payload } from '$lib/types/events'; - import { decodeUserMetadata } from '$lib/utilities/decode-payload'; + import type { Payload } from '$lib/types'; + import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload'; interface Props { - value: RawPayload | Payload | null | undefined; + value: Payload | null | undefined; fallback?: string; prefix?: string; maxSummaryLength?: number; @@ -40,7 +39,7 @@ decodedValue = fallback; return; } - decodeUserMetadata(value) + decodePayloadAndParseDataToJSON(value) .then((result) => { if (typeof result === 'string' && result) { decodedValue = applyPrefix(result); diff --git a/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte b/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte index a8087a64f8..cbda4c872c 100644 --- a/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte +++ b/src/lib/components/schedule/schedule-form/schedule-input-payload.svelte @@ -10,14 +10,14 @@ type PayloadInputEncoding, } from '$lib/models/payload-encoding'; import type { Payloads } from '$lib/types'; - import { atob } from '$lib/utilities/atob'; + import { base64ParsePayloadMetadata } from '$lib/utilities/decode-payload'; interface Props { input: string; editInput: boolean; encoding: Writable; messageType: string; - payloads: Payloads; + payloads: Payloads | undefined; showEditActions?: boolean; } @@ -35,15 +35,19 @@ let initialMessageType = $state(''); let loading = $state(true); - const setInitialInput = (decodedValue: string): void => { - initialInput = decodedValue; + const setInitialInput = (decodedValue: string[]): void => { + initialInput = decodedValue[0]; input = initialInput; - const currentEncoding = atob( - String(payloads?.payloads[0]?.metadata?.encoding ?? 'json/plain'), - ); - const currentMessageType = payloads?.payloads[0]?.metadata?.messageType - ? atob(String(payloads?.payloads[0]?.metadata?.messageType)) - : ''; + let currentEncoding: PayloadInputEncoding = 'json/plain'; + let currentMessageType = ''; + + if (payloads) { + const parsedMetadata = base64ParsePayloadMetadata(payloads); + + currentEncoding = + (parsedMetadata[0]?.encoding as PayloadInputEncoding) ?? 'json/plain'; + currentMessageType = parsedMetadata[0]?.messageType ?? ''; + } if (isPayloadInputEncodingType(currentEncoding)) { $encoding = currentEncoding; @@ -69,11 +73,7 @@
- + {#snippet children(_decodedValue)} @@ -11,7 +10,7 @@ {#snippet titleSnippet()}

diff --git a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte index 19250cd260..748729bba9 100644 --- a/src/lib/components/standalone-activities/activity-input-and-outcome.svelte +++ b/src/lib/components/standalone-activities/activity-input-and-outcome.svelte @@ -16,14 +16,14 @@
Input
- +
Result
{#if has(outcome, 'failure')} {:else if has(outcome, 'result')} - + {:else} {/if} diff --git a/src/lib/components/workflow/input-and-results-payload.svelte b/src/lib/components/workflow/input-and-results-payload.svelte index f4bbf42ba9..9e341f24c4 100644 --- a/src/lib/components/workflow/input-and-results-payload.svelte +++ b/src/lib/components/workflow/input-and-results-payload.svelte @@ -2,62 +2,24 @@ import type { Snippet } from 'svelte'; import PayloadCodeBlock from '$lib/components/payload/payload-code-block.svelte'; - import PayloadDecoder from '$lib/components/payload/payload-decoder.svelte'; import CodeBlock from '$lib/holocene/code-block.svelte'; - import { translate } from '$lib/i18n/translate'; - import type { Payload as RawPayload } from '$lib/types'; - import type { PotentiallyDecodable } from '$lib/utilities/decode-payload'; - import { - parseWithBigInt, - stringifyWithBigInt, - } from '$lib/utilities/parse-with-big-int'; + import type { Payloads } from '$lib/types'; + import type { CompletionEventAttributes } from '$lib/utilities/get-started-completed-and-task-failed-events'; type Props = { title: string; titleSnippet?: Snippet; - content?: string; + content: Payloads | CompletionEventAttributes; isPending?: boolean; }; let { title, titleSnippet = defaultTitleSnippet, - content = '', + content, isPending = false, }: Props = $props(); const MAX_HEIGHT = 300; - - const parseContent = (c: string): PotentiallyDecodable | undefined => { - try { - return parseWithBigInt(c); - } catch { - return undefined; - } - }; - - const parsePayloads = (c: string): unknown[] => { - try { - const data = parseWithBigInt(c); - return Array.isArray(data) ? data : [data]; - } catch { - return []; - } - }; - - const getPayloads = ( - value: PotentiallyDecodable | undefined, - ): RawPayload[] => { - try { - const payloads = value?.payloads; - return Array.isArray(payloads) ? payloads : []; - } catch { - return []; - } - }; - - const parsedContent = $derived(parseContent(content)); - const payloads = $derived(getPayloads(parsedContent)); - const payloadsSize = $derived(payloads.length); {#snippet defaultTitleSnippet()} @@ -69,36 +31,7 @@
{@render titleSnippet()} {#if content} - {#if payloadsSize > 0} - - {#snippet children(decodedValue)} - {#if payloadsSize > 1} - {#each parsePayloads(decodedValue) as decodedContent (decodedContent)} - - {/each} - {:else} - - {/if} - {/snippet} - - {:else} - - {/if} + {:else} {#key activity.attempt} - + {/key}
{/snippet} diff --git a/src/lib/components/workflow/workflow-json-navigator.svelte b/src/lib/components/workflow/workflow-json-navigator.svelte index 4406c72e3c..89812b016e 100644 --- a/src/lib/components/workflow/workflow-json-navigator.svelte +++ b/src/lib/components/workflow/workflow-json-navigator.svelte @@ -92,9 +92,7 @@
{#if $decodeEventHistory && events.length > 0} - {#key [index, $decodeEventHistory]} - - {/key} + {:else} {#key index} { @@ -14,14 +14,18 @@ if (!userMetadata) return metadata; if (userMetadata.summary) { - const summary = await decodeUserMetadata(userMetadata.summary); + const summary = await decodePayloadAndParseDataToJSON( + userMetadata.summary, + ); if (typeof summary === 'string') { metadata.summary = summary; } } if (userMetadata.details) { - const details = await decodeUserMetadata(userMetadata.details); + const details = await decodePayloadAndParseDataToJSON( + userMetadata.details, + ); if (typeof details === 'string') { metadata.details = details; diff --git a/src/lib/pages/standalone-activity-search-attributes.svelte b/src/lib/pages/standalone-activity-search-attributes.svelte index 5cbbb06154..3e15c5bce5 100644 --- a/src/lib/pages/standalone-activity-search-attributes.svelte +++ b/src/lib/pages/standalone-activity-search-attributes.svelte @@ -8,10 +8,7 @@
{#if searchAttributes} - + {:else}

{translate('events.empty-search-attributes')}

{/if} diff --git a/src/lib/pages/workflow-memo.svelte b/src/lib/pages/workflow-memo.svelte index 11650f80c4..25735cd9ec 100644 --- a/src/lib/pages/workflow-memo.svelte +++ b/src/lib/pages/workflow-memo.svelte @@ -9,7 +9,7 @@

{translate('common.memo')}

{#if workflow?.memo} - + {:else}

{translate('events.empty-memo-attributes')}

{/if} diff --git a/src/lib/pages/workflow-metadata.svelte b/src/lib/pages/workflow-metadata.svelte index 11ee0fcb1c..b0573b3a65 100644 --- a/src/lib/pages/workflow-metadata.svelte +++ b/src/lib/pages/workflow-metadata.svelte @@ -12,17 +12,14 @@

{translate('events.attribute-group.search-attributes')}

- +
{/if} {#if workflow?.memo}

{translate('common.memo')}

- +
{/if}
diff --git a/src/lib/pages/workflow-search-attributes.svelte b/src/lib/pages/workflow-search-attributes.svelte index 6d295dcba5..47dd50c6ad 100644 --- a/src/lib/pages/workflow-search-attributes.svelte +++ b/src/lib/pages/workflow-search-attributes.svelte @@ -11,10 +11,7 @@ {translate('events.attribute-group.search-attributes')}

{#if workflow?.searchAttributes} - + {:else}

{translate('events.empty-search-attributes')}

{/if} diff --git a/src/lib/services/data-encoder.ts b/src/lib/services/data-encoder.ts index 96b8fd62c3..279dbf2ebb 100644 --- a/src/lib/services/data-encoder.ts +++ b/src/lib/services/data-encoder.ts @@ -5,6 +5,7 @@ import { setLastDataEncoderFailure, setLastDataEncoderSuccess, } from '$lib/stores/data-encoder-config'; +import type { Payloads } from '$lib/types'; import type { NetworkError } from '$lib/types/global'; import { getAccessToken, getIdToken } from '$lib/utilities/core-provider'; import { @@ -23,7 +24,7 @@ export async function codeServerRequest({ }: { type: 'decode' | 'encode'; payloads: PotentialPayloads; -}): Promise { +}): Promise { const settings = page.data.settings; const namespace = page.params.namespace; const endpoint = getCodecEndpoint(settings); @@ -107,7 +108,7 @@ export async function decodePayloadsWithCodec({ payloads, }: { payloads: PotentialPayloads; -}): Promise { +}): Promise { return codeServerRequest({ type: 'decode', payloads }); } @@ -115,6 +116,6 @@ export async function encodePayloadsWithCodec({ payloads, }: { payloads: PotentialPayloads; -}): Promise { +}): Promise { return codeServerRequest({ type: 'encode', payloads }); } diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts index e83203fb84..e43c0dfab2 100644 --- a/src/lib/services/workflow-service.ts +++ b/src/lib/services/workflow-service.ts @@ -52,7 +52,7 @@ import type { } from '$lib/types/workflows'; import { decodeEventAttributesForExport, - decodeUserMetadata, + decodePayloadAndParseDataToJSON, type PotentiallyDecodable, } from '$lib/utilities/decode-payload'; import { @@ -843,7 +843,9 @@ export const fetchInitialValuesForStartWorkflow = async ({ let summary = ''; if (workflow.summary) { - const decodedSummary = await decodeUserMetadata(workflow.summary); + const decodedSummary = await decodePayloadAndParseDataToJSON( + workflow.summary, + ); if (typeof decodedSummary === 'string') { summary = decodedSummary; } @@ -851,7 +853,9 @@ export const fetchInitialValuesForStartWorkflow = async ({ let details = ''; if (workflow.details) { - const decodedDetails = await decodeUserMetadata(workflow.details); + const decodedDetails = await decodePayloadAndParseDataToJSON( + workflow.details, + ); if (typeof decodedDetails === 'string') { details = decodedDetails; } diff --git a/src/lib/types/workflows.ts b/src/lib/types/workflows.ts index f3c5cf2906..25bead4e72 100644 --- a/src/lib/types/workflows.ts +++ b/src/lib/types/workflows.ts @@ -1,5 +1,6 @@ import type { Memo, + Payload, Payloads, PendingWorkflowTaskInfo, Priority, @@ -11,7 +12,6 @@ import type { Callback } from '$lib/types/nexus'; import type { VersioningInfo } from './deployments'; import type { - Payload, PendingActivity, PendingActivityInfo, PendingChildren, diff --git a/src/lib/utilities/decode-local-activity.ts b/src/lib/utilities/decode-local-activity.ts index d797a14a79..efbcd3c8f6 100644 --- a/src/lib/utilities/decode-local-activity.ts +++ b/src/lib/utilities/decode-local-activity.ts @@ -1,5 +1,6 @@ import type { EventGroup } from '$lib/models/event-groups/event-groups'; -import type { IterableEvent, Payload, WorkflowEvent } from '$lib/types/events'; +import type { Payload } from '$lib/types'; +import type { IterableEvent, WorkflowEvent } from '$lib/types/events'; import { decodeEventAttributes, diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts index 716ff80bd1..c7cd90aca7 100644 --- a/src/lib/utilities/decode-payload.ts +++ b/src/lib/utilities/decode-payload.ts @@ -1,7 +1,7 @@ import { decodePayloadsWithCodec as callCodecEndpoint } from '$lib/services/data-encoder'; import type { DownloadEventHistorySetting } from '$lib/stores/events'; -import type { Memo, Payloads, Payload as RawPayload } from '$lib/types'; -import type { EventAttribute, Payload, WorkflowEvent } from '$lib/types/events'; +import type { Memo, Payload, Payloads } from '$lib/types'; +import type { EventAttribute, WorkflowEvent } from '$lib/types/events'; import type { Optional, Replace } from '$lib/types/global'; import { atob } from './atob'; @@ -18,6 +18,20 @@ export type DecodeFunctions = { decodeAttributes?: typeof parsePayloadAttributes; }; +export type PayloadContainingObject = { + [K in keyof T]: + | Payload + | Payloads + | (T[K] extends object ? PayloadContainingObject : T[K]); +}; + +export type ParsedMetadata = { [key: string]: string }; + +export type ParsedPayload = { + data: unknown; + metadata: ParsedMetadata; +}; + /** * Decoding TL;DR * Decoding includes either 1 or 2 phases - "parse" and "decode" @@ -56,6 +70,22 @@ const parseBase64ObjectValues = ( return parsed; }; +export function base64ParsePayloadMetadata(payload: Payload): ParsedMetadata; +export function base64ParsePayloadMetadata( + payloads: Payloads, +): ParsedMetadata[]; +export function base64ParsePayloadMetadata( + payloadOrPayloads: Payload | Payloads, +): ParsedMetadata | ParsedMetadata[] { + if (isRawPayload(payloadOrPayloads)) { + return parseBase64ObjectValues(payloadOrPayloads.metadata); + } + + return payloadOrPayloads.payloads.map((payload) => + parseBase64ObjectValues(payload.metadata), + ); +} + /** * Phase 1 — synchronous, no network. * @@ -183,9 +213,9 @@ const decodePayloadsWithRemoteCodecAndParseRawPayloadToJSON = async ( * decoded payloads without further Base64-decoding. Use this when the * caller needs the full Payload shape (e.g. for re-serialisation on export). */ -export const decodePayloadsWithRemoteCodec = async ( +const decodePayloadsWithRemoteCodec = async ( payloads: unknown[], -): Promise => { +): Promise => { const awaitData = await callCodecEndpoint({ payloads: { payloads } }); return awaitData?.payloads ?? []; }; @@ -198,31 +228,12 @@ const keyIs = (key: string, ...validKeys: string[]) => { return false; }; -/** - * Phase 2 — async, requires a configured codec endpoint. - * - * Decodes a single user-metadata payload (`summary` or `details` field on a - * workflow) through the remote codec server. Returns the decoded string value - * or the original Payload object; returns an empty string on error. - */ -export const decodeUserMetadata = async ( - payload: RawPayload | Payload, -): Promise => { - try { - const data = await decodePayloadsWithRemoteCodec([payload]); - const result = data[0]; - return parseRawPayloadToJSON(result) || ''; - } catch { - return ''; - } -}; - /** * Returns `true` when `payload` has exactly the `{ metadata, data }` shape of * a raw Temporal Payload object. Used to distinguish a bare Payload from a * map of payloads. */ -export const isRawPayload = (payload: unknown): boolean => { +export const isRawPayload = (payload: unknown): payload is Payload => { if (!isObject(payload)) return false; const keys = Object.keys(payload); return ( @@ -230,6 +241,43 @@ export const isRawPayload = (payload: unknown): boolean => { ); }; +/** + * Returns `true` when `payloads` has exactly the `{ payloads: Payloads[] }` shape of + * a raw Temporal Payloads proto. + */ +export const isRawPayloads = (payloads: unknown): payloads is Payloads => { + if (!isObject(payloads)) return false; + return ( + has(payloads, 'payloads') && + Array.isArray(payloads.payloads) && + payloads.payloads.every(isRawPayload) + ); +}; + +export const decodePayloadAndParseDataToJSON = async ( + payload: Payload | null | undefined, +): Promise => { + const decoded = await decodePayloadsWithRemoteCodec(toArray(payload)); + + if (!decoded || !decoded[0]) { + return null; + } + + return parseRawPayloadToJSON(decoded[0]); +}; + +export const decodePayloadsAndParseDataToJSON = async ( + payloads: Payloads | null | undefined, +): Promise => { + const decoded = await decodePayloadsWithRemoteCodec(payloads.payloads); + + if (!decoded || !decoded[0]) { + return [null]; + } + + return decoded.map((payload) => parseRawPayloadToJSON(payload)); +}; + /** * Phase 2 internal implementation shared by {@link decodeEventAttributes} and * {@link decodeEventAttributesForExport}. @@ -262,9 +310,8 @@ const decodeEventAttributesInternal = async ( if (anyAttributes) { // Now that we can have single Payload that is not an array (Nexus) if (isRawPayload(clone)) { - const data = toArray(clone as Payload); - const decoded = await decode(data, returnDataOnly); - return decoded?.[0] || clone; + const decoded = await decode(toArray(clone), returnDataOnly); + return decoded?.[0] ?? clone; } for (const key of Object.keys(clone)) { @@ -305,10 +352,9 @@ export const decodeEventAttributes = ( | WorkflowEvent | Memo | null, - decodeSetting: DownloadEventHistorySetting = 'readable', ): Promise< PotentiallyDecodable | EventAttribute | WorkflowEvent | Memo | null -> => decodeEventAttributesInternal(anyAttributes, decodeSetting, true); +> => decodeEventAttributesInternal(anyAttributes, 'readable', true); /** * Phase 2 only — async, requires a configured codec endpoint. diff --git a/src/lib/utilities/export-history.ts b/src/lib/utilities/export-history.ts index 7254070177..38171cd658 100644 --- a/src/lib/utilities/export-history.ts +++ b/src/lib/utilities/export-history.ts @@ -18,7 +18,8 @@ const decodePayloads = async ( decodeSetting, ); - return parsePayloadAttributes(convertedAttributes, false); + return convertedAttributes; + // return parsePayloadAttributes(convertedAttributes, false); } catch { return event; } diff --git a/src/lib/utilities/get-started-completed-and-task-failed-events.ts b/src/lib/utilities/get-started-completed-and-task-failed-events.ts index 3a0e945985..d9aa7d81b6 100644 --- a/src/lib/utilities/get-started-completed-and-task-failed-events.ts +++ b/src/lib/utilities/get-started-completed-and-task-failed-events.ts @@ -1,3 +1,4 @@ +import type { Payloads } from '$lib/types'; import type { WorkflowEvent, WorkflowExecutionCanceledEvent, @@ -13,11 +14,10 @@ import { isWorkflowExecutionCompletedEvent, isWorkflowExecutionContinuedAsNewEvent, } from './is-event-type'; -import { stringifyWithBigInt } from './parse-with-big-int'; export type WorkflowInputAndResults = { - input: string; - results: string; + input: Payloads; + results: Payloads | CompletionEventAttributes; contAsNew: boolean; }; @@ -29,6 +29,8 @@ type CompletionEvent = | WorkflowExecutionCanceledEvent | WorkflowExecutionTerminatedEvent; +export type CompletionEventAttributes = CompletionEvent['attributes']; + const completedEventTypes = [ 'WorkflowExecutionFailed', 'WorkflowExecutionCompleted', @@ -67,8 +69,8 @@ const getEventResult = (event: CompletionEvent) => { export const getWorkflowStartedCompletedAndTaskFailedEvents = ( eventHistory: WorkflowEvent[], ): WorkflowInputAndResults => { - let input: string; - let results: string; + let input: Payloads; + let results: Payloads | CompletionEventAttributes; let contAsNew = false; let workflowStartedEvent: WorkflowExecutionStartedEvent; @@ -85,15 +87,14 @@ export const getWorkflowStartedCompletedAndTaskFailedEvents = ( } if (workflowStartedEvent) { - input = stringifyWithBigInt( + input = workflowStartedEvent?.workflowExecutionStartedEventAttributes?.input ?? - null, - ); + null; } if (workflowCompletedEvent) { contAsNew = isWorkflowExecutionContinuedAsNewEvent(workflowCompletedEvent); - results = stringifyWithBigInt(getEventResult(workflowCompletedEvent)); + results = getEventResult(workflowCompletedEvent); } return { diff --git a/src/routes/(app)/nexus/[id]/+layout.ts b/src/routes/(app)/nexus/[id]/+layout.ts index 2543ea9662..0b35f69991 100644 --- a/src/routes/(app)/nexus/[id]/+layout.ts +++ b/src/routes/(app)/nexus/[id]/+layout.ts @@ -1,5 +1,5 @@ import { fetchNexusEndpoint } from '$lib/services/nexus-service.js'; -import { decodeUserMetadata } from '$lib/utilities/decode-payload.js'; +import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload.js'; export const load = async ({ params, fetch, parent }) => { const { id } = params; @@ -11,7 +11,7 @@ export const load = async ({ params, fetch, parent }) => { let description = ''; try { if (endpoint?.spec?.description?.data) { - const decodedDescription = await decodeUserMetadata( + const decodedDescription = await decodePayloadAndParseDataToJSON( endpoint.spec.description, ); From cb006533ce5e62b5c80b81558b0788541a2c1057 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 23 Apr 2026 13:13:26 -0600 Subject: [PATCH 17/30] fix remaining type errors --- .../components/event/event-details-row.svelte | 9 ++-- .../update-confirmation-modal.svelte | 2 +- src/lib/services/workflow-service.ts | 44 ++++++++++++------- src/lib/utilities/decode-local-activity.ts | 7 +-- src/lib/utilities/decode-payload.ts | 17 +++++-- .../get-single-attribute-for-event.ts | 17 +++---- 6 files changed, 59 insertions(+), 37 deletions(-) diff --git a/src/lib/components/event/event-details-row.svelte b/src/lib/components/event/event-details-row.svelte index d912cecede..8f408ef16b 100644 --- a/src/lib/components/event/event-details-row.svelte +++ b/src/lib/components/event/event-details-row.svelte @@ -5,7 +5,8 @@ import Badge from '$lib/holocene/badge.svelte'; import Copyable from '$lib/holocene/copyable/index.svelte'; import { translate } from '$lib/i18n/translate'; - import type { Payloads } from '$lib/types'; + import type { Payload, Payloads } from '$lib/types'; + import { isRawPayload, isRawPayloads } from '$lib/utilities/decode-payload'; import { format } from '$lib/utilities/format-camel-case'; import type { CombinedAttributes } from '$lib/utilities/format-event-attributes'; import { displayLinkType } from '$lib/utilities/get-single-attribute-for-event'; @@ -13,7 +14,7 @@ import EventDetailsLink from './event-details-link.svelte'; export let key: string; - export let value: string | Record | Payloads; + export let value: string | Payload | Payloads | Record; export let attributes: CombinedAttributes; export let showKey = true; @@ -29,13 +30,13 @@ {format(key)}

{/if} - {#if typeof value === 'object'} + {#if isRawPayload(value) || isRawPayloads(value)}
- {:else if linkType !== 'none'} + {:else if typeof value === 'string' && linkType !== 'none'} {#if success?.payloads?.[0] && success.payloads[0].data} - + {/if} {/if} diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts index e43c0dfab2..ae6358b18b 100644 --- a/src/lib/services/workflow-service.ts +++ b/src/lib/services/workflow-service.ts @@ -51,7 +51,6 @@ import type { WorkflowIdentifier, } from '$lib/types/workflows'; import { - decodeEventAttributesForExport, decodePayloadAndParseDataToJSON, type PotentiallyDecodable, } from '$lib/utilities/decode-payload'; @@ -837,9 +836,10 @@ export const fetchInitialValuesForStartWorkflow = async ({ const firstEvent = await fetchInitialEvent(params); const startEvent = firstEvent as WorkflowExecutionStartedEvent; - const convertedAttributes = (await decodeEventAttributesForExport( - startEvent?.attributes?.input, - )) as PotentiallyDecodable; + const decodedInput = await decodePayloadAndParseDataToJSON( + startEvent.attributes.input[0], + false, + ); // only single payloads are supported starting a workflow; let summary = ''; if (workflow.summary) { @@ -860,19 +860,29 @@ export const fetchInitialValuesForStartWorkflow = async ({ details = decodedDetails; } } - const input = convertedAttributes?.payloads - ? stringifyWithBigInt(convertedAttributes.payloads[0]?.data) - : ''; - const encoding = - convertedAttributes?.payloads && - isPayloadInputEncodingType( - convertedAttributes.payloads[0]?.metadata?.encoding, - ) - ? convertedAttributes.payloads[0]?.metadata?.encoding - : 'json/plain'; - const messageType = convertedAttributes?.payloads - ? convertedAttributes.payloads[0]?.metadata?.messageType - : ''; + + let input = ''; + let encoding: PayloadInputEncoding = 'json/plain'; + let messageType = ''; + + if (decodedInput) { + if (decodedInput.data) { + input = stringifyWithBigInt(decodedInput.data); + } + + if (decodedInput.metadata) { + if ( + decodedInput.metadata.encoding && + isPayloadInputEncodingType(decodedInput.metadata.encoding) + ) { + encoding = decodedInput.metadata.encoding; + } + + if (decodedInput.metadata.messageType) { + messageType = decodedInput.metadata.messageType; + } + } + } return { input, diff --git a/src/lib/utilities/decode-local-activity.ts b/src/lib/utilities/decode-local-activity.ts index efbcd3c8f6..2664c8e1dc 100644 --- a/src/lib/utilities/decode-local-activity.ts +++ b/src/lib/utilities/decode-local-activity.ts @@ -31,6 +31,7 @@ export type LocalActivityDecodeOptions = { export const decodeLocalActivity = async ( event: IterableEvent, ): Promise => { + console.log(event); if (!('eventType' in event) || !isLocalActivityMarkerEvent(event)) { return undefined; } @@ -38,10 +39,10 @@ export const decodeLocalActivity = async ( try { const convertedAttributes = await decodeEventAttributes(event.attributes); - const payloads = (event.markerRecordedEventAttributes?.details?.data - ?.payloads || + const payloads = + event.markerRecordedEventAttributes?.details?.data?.payloads || event.markerRecordedEventAttributes?.details?.type?.payloads || - []) as unknown as Payload[]; + []; if (!payloads?.length) return undefined; diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts index c7cd90aca7..d78ab3a7bd 100644 --- a/src/lib/utilities/decode-payload.ts +++ b/src/lib/utilities/decode-payload.ts @@ -10,6 +10,7 @@ import { isObject } from './is'; import { parseWithBigInt } from './parse-with-big-int'; export type PotentiallyDecodable = + | Payload | Payloads | Record; @@ -254,17 +255,25 @@ export const isRawPayloads = (payloads: unknown): payloads is Payloads => { ); }; -export const decodePayloadAndParseDataToJSON = async ( +export async function decodePayloadAndParseDataToJSON( + payload: Payload, +): Promise; +export async function decodePayloadAndParseDataToJSON( + payload: Payload, + returnDataOnly: false, +): Promise; +export async function decodePayloadAndParseDataToJSON( payload: Payload | null | undefined, -): Promise => { + returnDataOnly: boolean = true, +): Promise { const decoded = await decodePayloadsWithRemoteCodec(toArray(payload)); if (!decoded || !decoded[0]) { return null; } - return parseRawPayloadToJSON(decoded[0]); -}; + return parseRawPayloadToJSON(decoded[0], returnDataOnly); +} export const decodePayloadsAndParseDataToJSON = async ( payloads: Payloads | null | undefined, diff --git a/src/lib/utilities/get-single-attribute-for-event.ts b/src/lib/utilities/get-single-attribute-for-event.ts index 1fc59c2062..a79a8ed992 100644 --- a/src/lib/utilities/get-single-attribute-for-event.ts +++ b/src/lib/utilities/get-single-attribute-for-event.ts @@ -1,14 +1,17 @@ import { isEvent } from '$lib/models/event-history'; -import type { Payloads } from '$lib/types'; +import type { Payload, Payloads } from '$lib/types'; import type { PendingActivity, PendingNexusOperation, WorkflowEvent, } from '$lib/types/events'; -import type { Payload } from '$lib/types/events'; import { capitalize } from '$lib/utilities/format-camel-case'; -import { isRawPayload, parseRawPayloadToJSON } from './decode-payload'; +import { + isRawPayload, + isRawPayloads, + parseRawPayloadToJSON, +} from './decode-payload'; import type { CombinedAttributes } from './format-event-attributes'; import { has } from './has'; import { isObject } from './is'; @@ -23,7 +26,7 @@ import { export type SummaryAttribute = { key: string; - value: string | Record | Payloads; + value: string | Payload | Payloads | Record; }; const emptyAttribute: SummaryAttribute = { key: '', value: '' }; @@ -226,16 +229,14 @@ export const formatSummaryValue = ( value: unknown, ): SummaryAttribute => { if (typeof value === 'object') { - if (isRawPayload(value)) { + if (isRawPayload(value) || isRawPayloads(value)) { return { key, value }; } const [firstKey] = Object.keys(value); if (!firstKey) { return { key, value: {} }; } - if (firstKey === 'payloads') { - return { key, value }; - } + return { key: key + capitalize(firstKey), value: value[firstKey] }; } else { return { key, value: value.toString() }; From 51a5c4409d98355666062861ff931eecd6822ae6 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 23 Apr 2026 13:34:39 -0600 Subject: [PATCH 18/30] fix tests --- .../get-single-attribute-for-event.ts | 6 +- ...d-completed-and-task-failed-events.test.ts | 126 +++++++++++++++--- 2 files changed, 114 insertions(+), 18 deletions(-) diff --git a/src/lib/utilities/get-single-attribute-for-event.ts b/src/lib/utilities/get-single-attribute-for-event.ts index a79a8ed992..4f5bbbcc6e 100644 --- a/src/lib/utilities/get-single-attribute-for-event.ts +++ b/src/lib/utilities/get-single-attribute-for-event.ts @@ -229,14 +229,16 @@ export const formatSummaryValue = ( value: unknown, ): SummaryAttribute => { if (typeof value === 'object') { - if (isRawPayload(value) || isRawPayloads(value)) { + if (isRawPayload(value)) { return { key, value }; } const [firstKey] = Object.keys(value); if (!firstKey) { return { key, value: {} }; } - + if (firstKey === 'payloads') { + return { key, value }; + } return { key: key + capitalize(firstKey), value: value[firstKey] }; } else { return { key, value: value.toString() }; diff --git a/src/lib/utilities/get-started-completed-and-task-failed-events.test.ts b/src/lib/utilities/get-started-completed-and-task-failed-events.test.ts index d67e1b0484..1c78706f04 100644 --- a/src/lib/utilities/get-started-completed-and-task-failed-events.test.ts +++ b/src/lib/utilities/get-started-completed-and-task-failed-events.test.ts @@ -115,7 +115,7 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { expect( getWorkflowStartedCompletedAndTaskFailedEvents([workflowStartedEvent]) .input, - ).toBe('{"payloads":null}'); + ).toEqual({ payloads: null }); }); it('should get the correct input for a completed event history', () => { @@ -123,7 +123,14 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { completedEventHistory, ); expect(input).toMatchInlineSnapshot( - '"{"payloads":["1656707328774263000","canary"]}"', + ` + { + "payloads": [ + "1656707328774263000", + "canary", + ], + } + `, ); }); @@ -131,14 +138,21 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { const { results } = getWorkflowStartedCompletedAndTaskFailedEvents( completedEventHistory, ); - expect(results).toMatchInlineSnapshot('"null"'); + expect(results).toMatchInlineSnapshot('null'); }); it('should get the correct input for a cancelled event history', () => { const { input } = getWorkflowStartedCompletedAndTaskFailedEvents(canceledEventHistory); expect(input).toMatchInlineSnapshot( - '"{"payloads":[1656706850149404400,480000000000]}"', + ` + { + "payloads": [ + 1656706850149404400, + 480000000000, + ], + } + `, ); }); @@ -146,21 +160,51 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { const { results } = getWorkflowStartedCompletedAndTaskFailedEvents(canceledEventHistory); expect(results).toMatchInlineSnapshot( - '"{"type":"workflowExecutionCanceledEventAttributes","workflowTaskCompletedEventId":"11","details":null}"', + ` + { + "details": null, + "type": "workflowExecutionCanceledEventAttributes", + "workflowTaskCompletedEventId": "11", + } + `, ); }); it('should get the correct input for a failed event history', () => { const { input } = getWorkflowStartedCompletedAndTaskFailedEvents(failedEventHistory); - expect(input).toMatchInlineSnapshot('"{"payloads":[1656706968987842000]}"'); + expect(input).toMatchInlineSnapshot(` + { + "payloads": [ + 1656706968987842000, + ], + } + `); }); it('should get the correct result for a failed event history', () => { const { results } = getWorkflowStartedCompletedAndTaskFailedEvents(failedEventHistory); expect(results).toMatchInlineSnapshot( - '"{"type":"workflowExecutionFailedEventAttributes","failure":{"message":"failing on attempt 2","source":"GoSDK","stackTrace":"","cause":null,"applicationFailureInfo":{"type":"","nonRetryable":false,"details":null}},"retryState":"InProgress","workflowTaskCompletedEventId":"4","newExecutionRunId":"15e13ed4-880a-4557-96c6-0116e3d07b8d"}"', + ` + { + "failure": { + "applicationFailureInfo": { + "details": null, + "nonRetryable": false, + "type": "", + }, + "cause": null, + "message": "failing on attempt 2", + "source": "GoSDK", + "stackTrace": "", + }, + "newExecutionRunId": "15e13ed4-880a-4557-96c6-0116e3d07b8d", + "retryState": "InProgress", + "type": "workflowExecutionFailedEventAttributes", + "workflowTaskCompletedEventId": "4", + } + `, ); }); @@ -168,7 +212,14 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { const { input } = getWorkflowStartedCompletedAndTaskFailedEvents(runningEventHistory); expect(input).toMatchInlineSnapshot( - '"{"payloads":[1656707029044596700,"canary"]}"', + ` + { + "payloads": [ + 1656707029044596700, + "canary", + ], + } + `, ); }); @@ -183,7 +234,16 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { terminatedEventHistory, ); expect(input).toMatchInlineSnapshot( - '"{"payloads":[1656706488881881600,"temporal.fixture.terminated.workflow.id","3cbbf515-36da-43b9-a1f3-18a7ec031ddd","canary"]}"', + ` + { + "payloads": [ + 1656706488881881600, + "temporal.fixture.terminated.workflow.id", + "3cbbf515-36da-43b9-a1f3-18a7ec031ddd", + "canary", + ], + } + `, ); }); @@ -192,7 +252,14 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { terminatedEventHistory, ); expect(results).toMatchInlineSnapshot( - '"{"type":"workflowExecutionTerminatedEventAttributes","reason":"reset canary","details":null,"identity":"history-service"}"', + ` + { + "details": null, + "identity": "history-service", + "reason": "reset canary", + "type": "workflowExecutionTerminatedEventAttributes", + } + `, ); }); @@ -200,7 +267,14 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { const { input } = getWorkflowStartedCompletedAndTaskFailedEvents(timedOutEventHistory); expect(input).toMatchInlineSnapshot( - '"{"payloads":[1656683778738403300,"canary"]}"', + ` + { + "payloads": [ + 1656683778738403300, + "canary", + ], + } + `, ); }); @@ -208,7 +282,13 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { const { results } = getWorkflowStartedCompletedAndTaskFailedEvents(timedOutEventHistory); expect(results).toMatchInlineSnapshot( - '"{"type":"workflowExecutionTimedOutEventAttributes","retryState":"RetryPolicyNotSet","newExecutionRunId":""}"', + ` + { + "newExecutionRunId": "", + "retryState": "RetryPolicyNotSet", + "type": "workflowExecutionTimedOutEventAttributes", + } + `, ); }); @@ -222,14 +302,28 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { const { input } = getWorkflowStartedCompletedAndTaskFailedEvents( continuedAsNewEventHistory, ); - expect(input).toMatchInlineSnapshot('"{"payloads":[3,2]}"'); + expect(input).toMatchInlineSnapshot(` + { + "payloads": [ + 3, + 2, + ], + } + `); }); it('should get the correct result for a continuedAsNew event history', () => { const { results } = getWorkflowStartedCompletedAndTaskFailedEvents( continuedAsNewEventHistory, ); - expect(results).toMatchInlineSnapshot('"{"payloads":[4,1]}"'); + expect(results).toMatchInlineSnapshot(` + { + "payloads": [ + 4, + 1, + ], + } + `); }); it('should set contAsNew to true for a continuedAsNew event history', () => { @@ -261,7 +355,7 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { ]; const { results } = getWorkflowStartedCompletedAndTaskFailedEvents(history); - expect(results).toBe('null'); + expect(results).toBe(null); }); it('should work as expected with a WorkflowCompletedEvent with a payloads', () => { @@ -286,6 +380,6 @@ describe('getWorkflowStartedCompletedAndTaskFailedEvents', () => { ]; const { results } = getWorkflowStartedCompletedAndTaskFailedEvents(history); - expect(results).toBe('{"payloads":"result"}'); + expect(results).toEqual({ payloads: 'result' }); }); }); From 6fd76a0758d8d57fa5c1975fa07e9456f4de535f Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 23 Apr 2026 13:55:12 -0600 Subject: [PATCH 19/30] rm inspect --- src/lib/components/event/event-card.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/components/event/event-card.svelte b/src/lib/components/event/event-card.svelte index eaaf037b49..e33cc5954b 100644 --- a/src/lib/components/event/event-card.svelte +++ b/src/lib/components/event/event-card.svelte @@ -52,7 +52,6 @@ typeof value === 'object' && Object.keys(value).length > 0, ), ); - $inspect(payloadFields); const linkFields = $derived( fields.filter( ([key, _value]) => displayLinkType(key, attributes) !== 'none', From 1d27eb454b204dae6bffa9cab0ec5f48948e815c Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 23 Apr 2026 14:06:49 -0600 Subject: [PATCH 20/30] rm console.log --- src/lib/components/payload/payload-decoder.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/components/payload/payload-decoder.svelte b/src/lib/components/payload/payload-decoder.svelte index 8812364dd2..7a4382249a 100644 --- a/src/lib/components/payload/payload-decoder.svelte +++ b/src/lib/components/payload/payload-decoder.svelte @@ -29,7 +29,6 @@ return stringified; } else { const decoded = await decodeEventAttributes(value); - console.log(decoded); const stringified = stringifyWithBigInt(decoded); onDecode?.([stringified]); return [stringified]; From 099adf0a0258ff9893d488f32a186deeb91dc9d3 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Thu, 7 May 2026 09:35:21 -0600 Subject: [PATCH 21/30] fix CR feedback --- src/lib/components/payload/payload-inline.svelte | 5 ++++- src/lib/services/workflow-service.ts | 2 +- src/lib/utilities/decode-local-activity.ts | 1 - src/lib/utilities/decode-payload.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/components/payload/payload-inline.svelte b/src/lib/components/payload/payload-inline.svelte index a7ae23fffc..19a220f6b0 100644 --- a/src/lib/components/payload/payload-inline.svelte +++ b/src/lib/components/payload/payload-inline.svelte @@ -18,7 +18,10 @@ class="overflow-hidden border border-subtle bg-code-block px-1 py-0.5 font-mono text-xs text-primary {className}" > -
{decodedValue.slice(0, truncateAt)}
+
{(decodedValue[0] ?? '').slice(
+            0,
+            truncateAt,
+          )}
{/snippet} diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts index ae6358b18b..5ebd61cb9d 100644 --- a/src/lib/services/workflow-service.ts +++ b/src/lib/services/workflow-service.ts @@ -837,7 +837,7 @@ export const fetchInitialValuesForStartWorkflow = async ({ const startEvent = firstEvent as WorkflowExecutionStartedEvent; const decodedInput = await decodePayloadAndParseDataToJSON( - startEvent.attributes.input[0], + startEvent.attributes.input?.payloads[0], false, ); // only single payloads are supported starting a workflow; diff --git a/src/lib/utilities/decode-local-activity.ts b/src/lib/utilities/decode-local-activity.ts index 2664c8e1dc..c9dfea089a 100644 --- a/src/lib/utilities/decode-local-activity.ts +++ b/src/lib/utilities/decode-local-activity.ts @@ -31,7 +31,6 @@ export type LocalActivityDecodeOptions = { export const decodeLocalActivity = async ( event: IterableEvent, ): Promise => { - console.log(event); if (!('eventType' in event) || !isLocalActivityMarkerEvent(event)) { return undefined; } diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts index d78ab3a7bd..a6f0547b00 100644 --- a/src/lib/utilities/decode-payload.ts +++ b/src/lib/utilities/decode-payload.ts @@ -278,7 +278,7 @@ export async function decodePayloadAndParseDataToJSON( export const decodePayloadsAndParseDataToJSON = async ( payloads: Payloads | null | undefined, ): Promise => { - const decoded = await decodePayloadsWithRemoteCodec(payloads.payloads); + const decoded = await decodePayloadsWithRemoteCodec(payloads?.payloads); if (!decoded || !decoded[0]) { return [null]; From 50675c0b011ba15b90157c216550823aa8368638 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Tue, 12 May 2026 08:54:44 -0600 Subject: [PATCH 22/30] remove bad key --- src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte b/src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte index 5789df8329..625b3c9ce6 100644 --- a/src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte +++ b/src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte @@ -217,7 +217,7 @@ pointer-events="all" /> {/if} - {#each points as x, index (x)} + {#each points as x, index} {@const nextPoint = points[index + 1]} {@const showText = textIndex === index} {#if nextPoint} From e505b06b3844a68a89382a1a8473d97ef2132b28 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 09:16:21 -0600 Subject: [PATCH 23/30] fix export --- src/lib/utilities/export-history.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/utilities/export-history.ts b/src/lib/utilities/export-history.ts index 38171cd658..7254070177 100644 --- a/src/lib/utilities/export-history.ts +++ b/src/lib/utilities/export-history.ts @@ -18,8 +18,7 @@ const decodePayloads = async ( decodeSetting, ); - return convertedAttributes; - // return parsePayloadAttributes(convertedAttributes, false); + return parsePayloadAttributes(convertedAttributes, false); } catch { return event; } From 025222d5884303e4220c524495014f3747042e2c Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 09:18:42 -0600 Subject: [PATCH 24/30] remove unused type --- src/lib/components/event/event-details-row.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/event/event-details-row.svelte b/src/lib/components/event/event-details-row.svelte index 8f408ef16b..fcaafa6428 100644 --- a/src/lib/components/event/event-details-row.svelte +++ b/src/lib/components/event/event-details-row.svelte @@ -14,7 +14,7 @@ import EventDetailsLink from './event-details-link.svelte'; export let key: string; - export let value: string | Payload | Payloads | Record; + export let value: string | Payload | Payloads; export let attributes: CombinedAttributes; export let showKey = true; From 82d3a2651da7c3ac6f97a4d019c0fd74a5bccf57 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 09:19:32 -0600 Subject: [PATCH 25/30] cleanup unused stuff --- src/lib/components/payload/IMPROVEMENTS.md | 413 ------------------ .../payload/decode-payload-value.ts | 44 -- .../components/payload/payload-decoder.svelte | 2 +- 3 files changed, 1 insertion(+), 458 deletions(-) delete mode 100644 src/lib/components/payload/IMPROVEMENTS.md delete mode 100644 src/lib/components/payload/decode-payload-value.ts diff --git a/src/lib/components/payload/IMPROVEMENTS.md b/src/lib/components/payload/IMPROVEMENTS.md deleted file mode 100644 index de302da22c..0000000000 --- a/src/lib/components/payload/IMPROVEMENTS.md +++ /dev/null @@ -1,413 +0,0 @@ -# Payload Component Improvements - -## Background - -The four payload components (`PayloadCodeBlock`, `PayloadDecoder`, `PayloadInline`, `PayloadSummary`) share a common design problem: their `value` prop accepts `DecodableValue` — a 7-way union that includes `PotentiallyDecodable | EventAttribute | WorkflowEvent | Memo | RawPayload | null | undefined` — and rely on a `fieldName` prop to extract the actual payload data from wrapped objects. - -This document identifies where that complexity comes from, what can be simplified, and the concrete interface changes needed. - ---- - -## The `fieldName` Problem - -### Why it exists - -`fieldName` exists because call sites wrap payload data in intermediate objects and then tell the component which key to extract: - -```svelte - - -``` - -This pattern was inherited from the monolithic `` component. It routes through `decodeEventAttributes`, which walks event attribute objects and decodes nested payload fields. Wrapping in `{ searchAttributes: x }` mimics an event attribute shape so the decode function knows which fields to decode. - -### What fieldName actually does - -In `decode-payload-value.ts`: - -```typescript -// Step 1: run full decode pipeline on the whole object -const convertedAttributes = await decodeEventAttributes(value); -const decodedAttributes = parsePayloadAttributes(convertedAttributes) as object; - -// Step 2: optionally extract a field from the decoded result -const keyExists = fieldName && decodedAttributes?.[fieldName]; -let finalValue = keyExists ? decodedAttributes[fieldName] : decodedAttributes; - -// Step 3: unwrap single-element arrays -if (Array.isArray(finalValue) && finalValue.length === 1) { - finalValue = finalValue[0]; -} -``` - -The decode runs on the full wrapped object regardless. `fieldName` is only used _after_ decoding to pluck one field out of the result. - -### The fix: callers pass the value directly - -If callers extract the payload before passing, the component can skip the wrap/unwrap dance entirely: - -```svelte - - - - - -``` - -This requires a decode function that can handle `Payload | Payloads | null | undefined` without needing to know the field name. The decode pipeline supports this already — the wrapper pattern was never required, only habitual. - -### Exceptions - -Two call sites have legitimate fieldName use that requires different handling: - -| Call site | fieldName | Why | -| ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `input-and-results-payload.svelte` | `"payloads"` | Receives a pre-parsed object `{ payloads: [...] }` from a JSON parse step; extracting `.payloads` before passing would require restructuring the surrounding logic | -| `schedule-input-payload.svelte` | `"payloads"` | Same: receives a `Payloads` proto object, passes whole thing to decode and extracts `.payloads` post-decode | - -For these two, the alternative is to extract `.payloads` before passing: `value={payloads.payloads}` — straightforward but requires verifying the surrounding null checks. - ---- - -## Proposed Interface Changes - -### `PayloadCodeBlock` - -```typescript -// Current -interface Props { - value: DecodableValue; // 7-way union - fieldName?: string; // post-decode field extraction - maxHeight?: number; - copyIconTitle?: string; - copySuccessIconTitle?: string; - testId?: string; - language?: EditorLanguage; - onDecode?: (decodedValue: string) => void; -} - -// Proposed -interface Props { - value: Payload | Payloads | null | undefined; - maxHeight?: number; - copyIconTitle?: string; - copySuccessIconTitle?: string; - testId?: string; - language?: EditorLanguage; - onDecode?: (decodedValue: string) => void; -} -``` - -Callers that currently pass full `WorkflowEvent` objects (e.g. `workflow-json-navigator.svelte`) are not using `fieldName` and would need either a separate code path or to keep accepting the broader type for that use case only. - -### `PayloadDecoder` - -```typescript -// Current -interface Props { - value: DecodableValue; - fieldName?: string; - onDecode?: (decodedValue: string) => void; - children: Snippet<[decodedValue: string]>; -} - -// Proposed -interface Props { - value: Payload | Payloads | null | undefined; - onDecode?: (decodedValue: string) => void; - children: Snippet<[decodedValue: string]>; -} -``` - -### `PayloadInline` - -```typescript -// Current -interface Props { - value: DecodableValue; - fieldName?: string; - truncateAt?: number; - class?: string; -} - -// Proposed -interface Props { - value: Payload | Payloads | null | undefined; - truncateAt?: number; - class?: string; -} -``` - -### `PayloadSummary` - -This component already uses a narrower type (`RawPayload | Payload`) and a different decode function (`decodeUserMetadata`). Its interface is close to correct. The only improvement is renaming the type alias for clarity: - -```typescript -// no functional change needed — already correct -interface Props { - value: Payload | null | undefined; // rename RawPayload → Payload for consistency - fallback?: string; - prefix?: string; - maxSummaryLength?: number; - class?: string; - onDecode?: (decodedValue: string) => void; - children?: Snippet<[decodedValue: string]>; -} -``` - ---- - -## Required Decode Utility Changes - -### New function: `decodePayloads` - -The current `decodePayloadValue` in `decode-payload-value.ts` routes through `decodeEventAttributes`, which is designed for event attribute trees. A simpler function for the `Payload | Payloads` case: - -```typescript -export const decodePayloads = async ( - value: Payload | Payloads | null | undefined, -): Promise => { - if (!value) return stringifyWithBigInt(value); - // Normalize to array - const payloads = Array.isArray((value as Payloads).payloads) - ? (value as Payloads).payloads - : [value as Payload]; - // Phase 2: send through codec - const decoded = await decodeWithCodec(payloads); - // Phase 1: base64 parse - const parsed = decoded.map(parseRawPayloadToJSON); - return stringifyWithBigInt(parsed.length === 1 ? parsed[0] : parsed); -}; -``` - -This is shorter, type-safe, and does not rely on the `fieldName` extraction step. - -### Keep `decodePayloadValue` for WorkflowEvent inputs - -The workflow JSON navigator passes a full `HistoryEvent` (WorkflowEvent) to `PayloadCodeBlock`. That decode path still needs `decodeEventAttributes`. Options: - -1. Keep a separate `PayloadEventBlock` component for full event rendering (already a distinct use case — the only call site is the JSON navigator). -2. Accept `DecodableValue` in `PayloadCodeBlock` but only when `fieldName` is not needed, and narrow at the type level by making `fieldName` a required prop when passing a complex type — enforced via discriminated props. - -Option 1 is simpler. - ---- - -## Call Site Changes Required - -| File | Current | Change | -| ---------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------- | -| `workflow-search-attributes.svelte` | `value={{ searchAttributes: x }} fieldName="searchAttributes"` | `value={workflow.searchAttributes}` | -| `workflow-memo.svelte` | `value={{ memo: x }} fieldName="memo"` | `value={workflow.memo}` | -| `workflow-metadata.svelte` | Same as above ×2 | Same as above ×2 | -| `standalone-activity-search-attributes.svelte` | `value={{ searchAttributes: x }} fieldName="searchAttributes"` | `value={searchAttributes}` | -| `standalone-activity-details.svelte` | No fieldName; value is already `Payloads` | No change | -| `update-confirmation-modal.svelte` | `value={success.payloads[0]}` (single `Payload`) | No change | -| `event-details-row.svelte` | `value={x} fieldName="payloads"` | `value={x?.payloads}` — extract at call site | -| `input-and-results-payload.svelte` | `value={parsedContent} fieldName="payloads"` | `value={parsedContent?.payloads}` | -| `schedule-input-payload.svelte` | `value={payloads} fieldName="payloads"` | `value={payloads?.payloads}` | -| `workflow-json-navigator.svelte` | `value={rawEvent}` (full `WorkflowEvent`) | Move to `PayloadEventBlock` or keep separate | - ---- - -## Prioritized Work - -1. **Add `decodePayloads` utility** in `decode-payload-value.ts` — the new decode path for `Payload | Payloads`. -2. **Update `PayloadCodeBlock`, `PayloadDecoder`, `PayloadInline`** to use the new utility and remove `fieldName`. -3. **Update call sites** — 8 files, all mechanical changes (remove wrapper, remove `fieldName` prop, adjust value expression). -4. **Extract `PayloadEventBlock`** for the workflow JSON navigator use case, or document it as the one legitimate exception. -5. **Remove `fieldName` from `decode-payload-value.ts` API** and simplify `decodePayloadValue` signature. - -Steps 1–3 can be done in a single pass. Step 4 is optional but recommended for type safety. - -## Make decoding ASYNC/AWAIT and include Retries - -### Current state - -The decode utilities (`decodeEventAttributes`, `decodeUserMetadata`, `decodePayloadValue`) are already `async` functions that return Promises. However, the components cannot use `await` directly inside `$effect` — returning a Promise from `$effect` is treated as a cleanup function, not a suspension. So all four components fall back to chained `.then().catch()`: - -```typescript -$effect(() => { - if (!value) return; - decodePayloadValue(value, fieldName) - .then((result) => { - decodedValue = result; - onDecode?.(result); - }) - .catch(() => { - console.error('Could not decode payloads'); - }); -}); -``` - -Additionally, `data-encoder.ts` `codeServerRequest` makes a single `fetch` with no timeout and no retry. On failure it silently returns the original (undecoded) payloads, so the UI shows encoded base64 without any error indication. - ---- - -### Problem 1: Race conditions on reactive updates - -When `value` changes while a decode is in progress, the `$effect` re-runs and fires a second decode. Both are in flight simultaneously. The later one to _resolve_ (not necessarily the one started later) wins. This can cause the UI to flash the wrong decoded value. - -#### Fix: AbortController + `$effect` cleanup - -`$effect` supports a cleanup function — whatever it returns is called before the next run. An `AbortController` cancels the stale request: - -```typescript -$effect(() => { - if (!value) return; - const controller = new AbortController(); - - (async () => { - try { - const result = await decodePayloadValue(value, fieldName); - if (!controller.signal.aborted) { - decodedValue = result; - onDecode?.(result); - } - } catch { - if (!controller.signal.aborted) { - console.error('Could not decode payloads'); - } - } - })(); - - return () => controller.abort(); -}); -``` - -The async IIFE gives us clean `await` syntax inside `$effect`. The `controller.abort()` cleanup fires when `value` changes or the component unmounts, so stale responses are dropped. - -To propagate the signal into the codec fetch, `codeServerRequest` in `data-encoder.ts` would accept an optional `signal` and forward it to `fetch`: - -```typescript -const decoderResponse = fetch(endpoint + `/${type}`, { - ...requestOptions, - signal, -}); -``` - ---- - -### Problem 2: No timeout on codec server requests - -`codeServerRequest` makes an unbounded `fetch`. If the codec server hangs, the UI waits indefinitely. The component shows the initial (undecoded) value forever with no feedback. - -#### Fix: Timeout via `AbortSignal.timeout` (or `AbortSignal.any`) - -```typescript -// Option A — native timeout signal (supported in modern browsers) -const signal = AbortSignal.timeout(5000); - -// Option B — compose with the cancellation signal from above -const timeoutSignal = AbortSignal.timeout(5000); -const signal = AbortSignal.any([controller.signal, timeoutSignal]); -``` - -`AbortSignal.any` (available in browsers since 2023) merges multiple signals — the fetch is aborted as soon as either the component unmounts OR the 5 second timeout fires. - -In `codeServerRequest`, a `TimeoutError` should be handled distinctly from a network error so callers can differentiate transient failures from permanent ones. - ---- - -### Problem 3: No retry on transient codec failures - -`codeServerRequest` catches all errors and returns the original payloads on `decode` failures. This means a temporary codec server blip (e.g. a cold-start 502) permanently shows encoded data for that page load. There is no attempt to recover. - -#### Fix: Exponential backoff retry in `codeServerRequest` - -Adding retry at the `codeServerRequest` level benefits every decode path (components, export, event history) without changes to callers. - -```typescript -async function codeServerRequest( - options: CodecRequestOptions, - signal?: AbortSignal, - maxAttempts = 3, -): Promise { - let attempt = 0; - while (attempt < maxAttempts) { - try { - return await fetchCodecEndpoint(options, signal); - } catch (err) { - attempt++; - if (attempt >= maxAttempts || signal?.aborted) throw err; - await delay(250 * 2 ** attempt, signal); // 500ms, 1000ms - } - } - throw new Error('unreachable'); -} - -function delay(ms: number, signal?: AbortSignal): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(resolve, ms); - signal?.addEventListener('abort', () => { - clearTimeout(timer); - reject(signal.reason); - }); - }); -} -``` - -Retry should only apply to transient errors (network failure, 5xx). A 4xx (bad request, auth failure) should not be retried. - ---- - -### Problem 4: No loading state in components - -Because the initial render uses `getInitialPayloadValue` (which returns the raw base64 JSON), the user sees undecoded data until the async decode resolves. There is no loading indicator, so it is not clear whether decoding is in progress or has failed. - -#### Fix: Explicit `isDecoding` state - -```typescript -let isDecoding = $state(false); -let decodedValue = $state(getInitialPayloadValue(value, fieldName)); - -$effect(() => { - if (!value) return; - const controller = new AbortController(); - isDecoding = true; - - (async () => { - try { - const result = await decodePayloadValue( - value, - fieldName, - controller.signal, - ); - if (!controller.signal.aborted) { - decodedValue = result; - isDecoding = false; - onDecode?.(result); - } - } catch { - if (!controller.signal.aborted) { - isDecoding = false; - } - } - })(); - - return () => { - controller.abort(); - isDecoding = false; - }; -}); -``` - -`PayloadCodeBlock` could pass `isDecoding` to `` as a loading prop; `PayloadInline` could render a placeholder; `PayloadSummary` could keep the `fallback` value visible during decode. - ---- - -### Summary of changes - -| Layer | Change | Benefit | -| ---------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------ | -| `data-encoder.ts` `codeServerRequest` | Accept `signal`, add retry with exponential backoff, handle timeout distinctly | All callers get resilient fetching without changes | -| `decode-payload-value.ts` `decodePayloadValue` | Forward `signal` to `decodeEventAttributes` | Enables cancellation from components | -| All four payload components | Use async IIFE + `AbortController` in `$effect`, track `isDecoding` state | No race conditions, no stale updates, loading feedback | - -The retry and timeout changes are additive — existing callers that don't pass a `signal` continue to work without modification. diff --git a/src/lib/components/payload/decode-payload-value.ts b/src/lib/components/payload/decode-payload-value.ts deleted file mode 100644 index 97d0317792..0000000000 --- a/src/lib/components/payload/decode-payload-value.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Memo, Payload as RawPayload } from '$lib/types'; -import type { EventAttribute, WorkflowEvent } from '$lib/types/events'; -import { - decodeEventAttributes, - parsePayloadAttributes, - type PotentiallyDecodable, -} from '$lib/utilities/decode-payload'; -import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int'; - -export type DecodableValue = - | PotentiallyDecodable - | EventAttribute - | WorkflowEvent - | Memo - | RawPayload - | null - | undefined; - -export const getInitialPayloadValue = ( - value: DecodableValue, - fieldName: string, -): string => { - if (!value) return stringifyWithBigInt(value); - const keyedValue = fieldName && value?.[fieldName] ? value[fieldName] : value; - return stringifyWithBigInt(keyedValue); -}; - -export const decodePayloadValue = async ( - value: DecodableValue, - fieldName: string, -): Promise => { - const convertedAttributes = await decodeEventAttributes( - value as PotentiallyDecodable | EventAttribute | WorkflowEvent | Memo, - ); - const decodedAttributes = parsePayloadAttributes( - convertedAttributes, - ) as object; - const keyExists = fieldName && decodedAttributes?.[fieldName]; - let finalValue = keyExists ? decodedAttributes[fieldName] : decodedAttributes; - if (Array.isArray(finalValue) && finalValue.length === 1) { - finalValue = finalValue[0]; - } - return stringifyWithBigInt(finalValue); -}; diff --git a/src/lib/components/payload/payload-decoder.svelte b/src/lib/components/payload/payload-decoder.svelte index 7a4382249a..fcb3051ef5 100644 --- a/src/lib/components/payload/payload-decoder.svelte +++ b/src/lib/components/payload/payload-decoder.svelte @@ -12,7 +12,7 @@ } from '$lib/utilities/decode-payload'; import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int'; - export const decodePayloadValue = async ( + const decodePayloadValue = async ( value: PotentiallyDecodable | PayloadContainingObject, ): Promise => { if (isRawPayload(value)) { From a1a00d75c0bbd653378fa49663aad0faf0aa807a Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 09:20:31 -0600 Subject: [PATCH 26/30] fix possibly duplicate key --- src/lib/components/payload/payload-code-block.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/payload/payload-code-block.svelte b/src/lib/components/payload/payload-code-block.svelte index 65b8a17ca4..69462b020f 100644 --- a/src/lib/components/payload/payload-code-block.svelte +++ b/src/lib/components/payload/payload-code-block.svelte @@ -20,7 +20,7 @@ {#snippet children(decodedValue)}
- {#each decodedValue as data (data)} + {#each decodedValue as data, index (index)} Date: Wed, 13 May 2026 09:21:31 -0600 Subject: [PATCH 27/30] add language prop to PayloadCodeBlock --- src/lib/components/payload/payload-code-block.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/components/payload/payload-code-block.svelte b/src/lib/components/payload/payload-code-block.svelte index 69462b020f..410a46f7d3 100644 --- a/src/lib/components/payload/payload-code-block.svelte +++ b/src/lib/components/payload/payload-code-block.svelte @@ -12,9 +12,10 @@ value: PotentiallyDecodable | PayloadContainingObject; maxHeight?: number; testId?: string; + language?: 'text' | 'json'; } - let { value, maxHeight, testId }: Props = $props(); + let { value, maxHeight, testId, language = 'json' }: Props = $props(); @@ -27,7 +28,7 @@ copyIconTitle={translate('common.copy-icon-title')} copySuccessIconTitle={translate('common.copy-success-icon-title')} {testId} - language="json" + {language} /> {/each}
From 7d70f2d5897f83a5d8a88f6dd0639a5a7856015c Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 09:23:13 -0600 Subject: [PATCH 28/30] add copyIconTitle and copySuccessIconTitle to codeblock instance --- src/lib/components/event/event-card.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/components/event/event-card.svelte b/src/lib/components/event/event-card.svelte index e33cc5954b..e5046b27c5 100644 --- a/src/lib/components/event/event-card.svelte +++ b/src/lib/components/event/event-card.svelte @@ -192,7 +192,13 @@

{translate('workflows.call-stack-tab')}

- + {/if} {/snippet} From 820fb40891c738a5aefaeb1386dbe3ed8bdeb1ff Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 10:48:13 -0600 Subject: [PATCH 29/30] refactor(payload): fix reactivity and race condition in payload-summary Replace $effect+$state async pattern with $derived promise + $effect cleanup to ensure all reactive deps (value, fallback, prefix, maxSummaryLength) are tracked synchronously. Use $effect cleanup function to cancel stale promise callbacks on re-run, preventing race conditions when value changes rapidly. Remove unused onDecode prop. --- .../components/payload/payload-summary.svelte | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/lib/components/payload/payload-summary.svelte b/src/lib/components/payload/payload-summary.svelte index e8e15f6f86..ed3255ef5f 100644 --- a/src/lib/components/payload/payload-summary.svelte +++ b/src/lib/components/payload/payload-summary.svelte @@ -11,7 +11,6 @@ prefix?: string; maxSummaryLength?: number; class?: string; - onDecode?: (decodedValue: string) => void; children?: Snippet<[decodedValue: string]>; } @@ -21,36 +20,43 @@ prefix = '', maxSummaryLength = 120, class: className = '', - onDecode, children, }: Props = $props(); let decodedValue = $state(fallback); - const applyPrefix = (text: string): string => { - if (!prefix) return text; - const prefixed = `${prefix} • ${text}`; - if (prefixed.length <= maxSummaryLength) return prefixed; - return prefixed.slice(0, maxSummaryLength) + '...'; - }; + const decodePromise = $derived( + (() => { + const _value = value; + const _fallback = fallback; + const _prefix = prefix; + const _max = maxSummaryLength; + + if (!_value) return Promise.resolve(_fallback); + + return decodePayloadAndParseDataToJSON(_value).then((result) => { + if (typeof result !== 'string' || !result) return _fallback; + if (!_prefix) return result; + const prefixed = `${_prefix} • ${result}`; + return prefixed.length <= _max + ? prefixed + : prefixed.slice(0, _max) + '...'; + }); + })(), + ); $effect(() => { - if (!value) { - decodedValue = fallback; - return; - } - decodePayloadAndParseDataToJSON(value) + let cancelled = false; + decodePromise .then((result) => { - if (typeof result === 'string' && result) { - decodedValue = applyPrefix(result); - onDecode?.(decodedValue); - } else { - decodedValue = fallback; - } + if (!cancelled) decodedValue = result; }) .catch(() => { - decodedValue = fallback; + if (!cancelled) decodedValue = fallback; }); + return () => { + cancelled = true; + }; }); From 869b9bc173acef79e5ec44c52ca1238f478a2fd8 Mon Sep 17 00:00:00 2001 From: Ross Edfort Date: Wed, 13 May 2026 11:15:05 -0600 Subject: [PATCH 30/30] early return if no payloads in decode utils --- src/lib/utilities/decode-payload.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts index a6f0547b00..d623c596ff 100644 --- a/src/lib/utilities/decode-payload.ts +++ b/src/lib/utilities/decode-payload.ts @@ -266,6 +266,7 @@ export async function decodePayloadAndParseDataToJSON( payload: Payload | null | undefined, returnDataOnly: boolean = true, ): Promise { + if (!payload) return null; const decoded = await decodePayloadsWithRemoteCodec(toArray(payload)); if (!decoded || !decoded[0]) { @@ -278,6 +279,7 @@ export async function decodePayloadAndParseDataToJSON( export const decodePayloadsAndParseDataToJSON = async ( payloads: Payloads | null | undefined, ): Promise => { + if (!payloads) return null; const decoded = await decodePayloadsWithRemoteCodec(payloads?.payloads); if (!decoded || !decoded[0]) {