Skip to content

DT-3751 - download external payloads#3345

Merged
rossedfort merged 54 commits into
mainfrom
DT-3751-external-payload-codeblocks
May 15, 2026
Merged

DT-3751 - download external payloads#3345
rossedfort merged 54 commits into
mainfrom
DT-3751-external-payload-codeblocks

Conversation

@rossedfort
Copy link
Copy Markdown
Contributor

@rossedfort rossedfort commented Apr 24, 2026

Description & motivation 💭

Adds download functionality for externally stored payloads (note: only for payloads that display in a Code Block). Other payload touch points will be addressed in a following PR.

WIP
This PR is a work in progress. Currently the download button shows, but the download functionality doesn't work.

Screenshots (if applicable) 📸

Generic error (when codec server not configured or download fails)

Screenshot 2026-05-05 at 11 22 48 AM

Error when codec server is configured, but no /download endpoint

Screenshot 2026-05-05 at 11 23 21 AM

Disable download button and show tooltip when codec server not configured

Screenshot 2026-05-05 at 11 23 43 AM

Sample download w/ semantic filename

Screenshot 2026-05-12 at 10 54 00 AM

Event History Row w/ External Payload

Screenshot 2026-05-12 at 12 30 52 PM

Design Considerations 🎨

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Check out this branch and run pnpm i and pnpm build:server and pnpm dev
  2. Clone Justin's fork of sdk-go which contains an external payload workflow and codec server
  3. Follow the instructions in the notion doc titled: External Storage Bug Bash 04/17
  4. Set the AWS_PROFILE and S3_BUCKET environment variables
  5. In your local version of Justin's fork of temporalio/sdk-go, run go run demos/worker/main.go, and in a separate shell session, run go run demos/codecserver/main.go
  6. Run temporal workflow start --task-queue "echo-task-queue" --type "GenerateDocument" --workflow-id "generate-doc" --input '1000000' to run a workflow with an externally stored payload.

Checklists

Draft Checklist

Merge Checklist

Issue(s) closed

Docs

Any docs updates needed?

…load component

Consolidates two divergent payload display components into a single
`<Payload />` 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.
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.
… components

Replace the 14-prop multi-mode payload.svelte with four single-purpose
components: PayloadCodeBlock, PayloadSummary, PayloadDecoder, and
PayloadInline. Each component carries only the props relevant to its
render mode, eliminating dead props at call sites.

Also updates the decode paths to use the renamed decode-payload.ts API
(decodeEventAttributes, parsePayloadAttributes) following the #3302
cleanup.
Replace decodeEventAttributes with decodeUserMetadata — the correct
decode path for a single raw Payload (Phase 2 codec only, no attribute
tree walking). Also add onDecode callback prop, called after a
successful decode, consistent with PayloadCodeBlock.
…lue utility

Remove 3-way duplication of getInitialValue/onMount decode logic from
payload-code-block, payload-decoder, and payload-inline by extracting
getInitialPayloadValue and decodePayloadValue into
src/lib/utilities/decode-payload-value.ts.
Components now re-decode when value or fieldName props change after
mount, fixing the reactivity gap that caused stale decoded content
when parent components updated their payload bindings.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment May 15, 2026 4:07pm

Request Review

downloadLoading = true;
let data: Payloads | undefined = undefined;
try {
data = await downloadExternalPayloadWithCodec(payload);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ 'data.payloads' is possibly 'null' or 'undefined'.

</div>
<div class="flex w-full flex-col gap-4 xl:w-1/3">
<ScheduleInput
{scheduleId}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Type 'IPayloads | null | undefined' is not assignable to type 'IPayloads'.

<ScheduleInput
{scheduleId}
input={schedule?.schedule?.action?.startWorkflow?.input}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Type 'IScheduleSpec | null | undefined' is not assignable to type 'IScheduleSpec'.

): Promise<unknown[]> => {
if (!payloads) return null;
returnDataOnly: boolean = true,
): Promise<unknown[]> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Argument of type 'IPayload[] | null | undefined' is not assignable to parameter of type 'unknown[]'.

Comment thread src/lib/components/payload/payload-code-block.svelte
Comment thread src/lib/components/schedule/schedule-form/schedule-input-payload.svelte Outdated
{@render children(decodedValue)}
{:then decodeResult}
{@render children(decodeResult)}
{/await}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we could really easily put the error state in here and pass it a retry snippet as well so we can get per request errors instead of the "last used" errors

{testId}
language="json"
/>
{/if}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there's a lot of stuff going on with these files that could be weird or error, maybe we should throw down an error boundary somewhere just in case to prevent app crashes

payload.metadata.messageType ===
'temporal.api.sdk.v1.ExternalStorageReference'
);
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just spitballing on these, I wonder if instead of using typeGuards (which are a typesystem escape hatch) we could use a discriminated union on like a data container so you could easily identify which phase we're in for decoding.

Just thinking out loud don't need to act on it.

@rossedfort rossedfort merged commit 41fd4f1 into main May 15, 2026
17 checks passed
@rossedfort rossedfort deleted the DT-3751-external-payload-codeblocks branch May 15, 2026 18:24
tegan-temporal pushed a commit that referenced this pull request May 18, 2026
* refactor: replace PayloadDecoder and MetadataDecoder with unified Payload component

Consolidates two divergent payload display components into a single
`<Payload />` 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.

* remove unused component, fix css

* 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.

* docs: add Payload component improvement suggestions

* docs: add decode-payload usage report and refactoring recommendations

* refactor(payload): split Payload component into focused mode-specific components

Replace the 14-prop multi-mode payload.svelte with four single-purpose
components: PayloadCodeBlock, PayloadSummary, PayloadDecoder, and
PayloadInline. Each component carries only the props relevant to its
render mode, eliminating dead props at call sites.

Also updates the decode paths to use the renamed decode-payload.ts API
(decodeEventAttributes, parsePayloadAttributes) following the #3302
cleanup.

* fix(payload-summary): use decodeUserMetadata and add onDecode prop

Replace decodeEventAttributes with decodeUserMetadata — the correct
decode path for a single raw Payload (Phase 2 codec only, no attribute
tree walking). Also add onDecode callback prop, called after a
successful decode, consistent with PayloadCodeBlock.

* refactor(payload): extract shared decode logic into decode-payload-value utility

Remove 3-way duplication of getInitialValue/onMount decode logic from
payload-code-block, payload-decoder, and payload-inline by extracting
getInitialPayloadValue and decodePayloadValue into
src/lib/utilities/decode-payload-value.ts.

* fix(payload): replace onMount with \$effect for reactive value updates

Components now re-decode when value or fieldName props change after
mount, fixing the reactivity gap that caused stale decoded content
when parent components updated their payload bindings.

* add workflow with user metadata

* remove unnecessary props from payload code block

* fix summary display

* add improvements

* fix heading levels

* add multi input workflow

* get rid of fieldName prop and fix most of the call sites

* fix remaining type errors

* fix tests

* rm inspect

* rm console.log

* WIP - download external payloads

* add support for downloading externally stored payloads

* add some error handling to downloads

* add support for filename

* fix CR feedback

* remove bad key

* fix export

* remove unused type

* cleanup unused stuff

* fix possibly duplicate key

* add language prop to PayloadCodeBlock

* add copyIconTitle and copySuccessIconTitle to codeblock instance

* 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.

* early return if no payloads in decode utils

* add type guards

* support inline external payloads

* fix download error and payload codeblock types

* fix schedule input

* rm console.log

* fix tests and strict mode

* fix spec

* fix typo

* add some optional chaining
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants