Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 50 additions & 19 deletions src/lib/components/event/payload-decoder.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount, type Snippet } from 'svelte';
import { type Snippet } from 'svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import Button from '$lib/holocene/button.svelte';
import { authUser } from '$lib/stores/auth-user';
import type { Memo } from '$lib/types';
import type { EventAttribute, WorkflowEvent } from '$lib/types/events';
Expand All @@ -23,33 +24,41 @@
key?: string;
onDecode?: (decodedValue: string) => void;
children: Snippet<[decodedValue: string]>;
error?: Snippet<[retry: () => Promise<string>, err: unknown]>;
loading?: Snippet<[keyedVal: string]>;
}
let { children, value, key = '', onDecode }: Props = $props();
let {
children,
value,
key = '',
onDecode,
error = errorSnip,
loading = loadingSnip,
}: Props = $props();
let keyedValue = key && value?.[key] ? value[key] : value;
let decodedValue = $state(stringifyWithBigInt(keyedValue));
let keyedValue = stringifyWithBigInt(
key && value?.[key] ? value[key] : value,
);
onMount(() => {
decodePayloads(value);
});
let decodeValuePromise = $state<Promise<string>>(decodePayloads(value));
const decodePayloads = async (
async function decodePayloads(
_value: PotentiallyDecodable | EventAttribute | WorkflowEvent | Memo,
) => {
) {
const settings = {
...$page.data.settings,
...page.data.settings,
codec: {
...$page.data.settings?.codec,
endpoint: getCodecEndpoint($page.data.settings),
passAccessToken: getCodecPassAccessToken($page.data.settings),
includeCredentials: getCodecIncludeCredentials($page.data.settings),
...page.data.settings?.codec,
endpoint: getCodecEndpoint(page.data.settings),
passAccessToken: getCodecPassAccessToken(page.data.settings),
includeCredentials: getCodecIncludeCredentials(page.data.settings),
},
};
try {
const convertedAttributes = await cloneAllPotentialPayloadsWithCodec(
_value,
$page.params.namespace,
page.params.namespace,
settings,
$authUser.accessToken,
);
Expand All @@ -61,14 +70,36 @@
if (Array.isArray(finalValue) && finalValue.length === 1) {
finalValue = finalValue[0];
}
decodedValue = stringifyWithBigInt(finalValue);
let decodedValue = stringifyWithBigInt(finalValue);
if (onDecode) {
onDecode(decodedValue);
}
return decodedValue;
} catch (e) {
console.error('Could not decode payloads');
// hmm before this just ate the error we want to throw this to get an error here
// but maybe this is leaking information to the users? But it also might be good
// to allow that? Think about this harder maybe ask app sec about if this is an okay
// design choice
Comment on lines +81 to +83
Copy link
Collaborator

@Alex-Tideman Alex-Tideman Nov 4, 2025

Choose a reason for hiding this comment

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

We always want to return the original encoded value if decoding fails

throw e;
}
};
}
</script>

{@render children(decodedValue)}
{#snippet loadingSnip(val)}
{val}
{/snippet}

{#snippet errorSnip(retry, error)}
<div>{error}</div>

<Button on:click={retry}>Retry Decoding</Button>
{/snippet}

{#await decodeValuePromise}
{@render loading?.(keyedValue)}
{:then decoded}
{@render children(decoded)}
{:catch err}
{@render error?.(() => decodePayloads(value), err)}
Copy link
Collaborator

@Alex-Tideman Alex-Tideman Nov 4, 2025

Choose a reason for hiding this comment

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

This... confuses me?

So you'd pass something like

{#snippet error(retry, err)}
  {#await retry} ...
  {:catch}
   {err}
  {/await}
{/snippet}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

retry is a function so you can call it in the error component if you like should have probably explicitly set the promise above to the decode payloads so it would retry

{/await}
Comment on lines +99 to +105
Copy link
Collaborator

Choose a reason for hiding this comment

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

@claude explain this

Copy link

@claude claude bot Nov 10, 2025

Choose a reason for hiding this comment

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

Claude finished @Alex-Tideman's task β€”β€” View job


Explanation of line 105

The code at line 105 (actually the entire block from lines 99-105) is using Svelte 5's {#await} block syntax to handle the asynchronous decoding operation. Here's what's happening:

The Pattern:

{#await decodeValuePromise}
  {@render loading?.(keyedValue)}
{:then decoded}
  {@render children(decoded)}
{:catch err}
  {@render error?.(() => decodePayloads(value), err)}
{/await}

Breaking it down:

  1. Line 99-100 (Pending state): While decodeValuePromise is still loading, it renders the loading snippet, passing the original keyedValue (the stringified unencoded value)

  2. Line 101-102 (Success state): When the promise resolves successfully, it renders the children snippet with the decoded value

  3. Line 103-104 (Error state): If decoding fails, it renders the error snippet with two parameters:

    • First param: A retry function () => decodePayloads(value) - This is an arrow function that, when called, will re-attempt the decode operation
    • Second param: The error object err

Why pass a function instead of calling it?

The key insight is that line 104 passes () => decodePayloads(value) (a function) rather than calling it immediately. This gives control to the error snippet to decide when to retry. The default errorSnip (lines 93-97) calls this function when the user clicks the "Retry Decoding" button:

{#snippet errorSnip(retry, error)}
  <div>{error}</div>
  <Button on:click={retry}>Retry Decoding</Button>
{/snippet}

This pattern allows consumers to provide their own error UI with custom retry behavior while keeping the retry logic (calling decodePayloads) encapsulated in the component.

Loading