ui/async_slot.svelte.ts

Composable async-operation slot for Svelte 5 reactive state classes.

A state class HOLDS one or more AsyncSlots via composition — one slot per distinct async operation (e.g. list + create + revoke). Each slot tracks the status, payload, and error of its operation independently, so state classes with multiple write paths don't accumulate ad-hoc creating / updating fields beside a single shared loading / error pair.

Core surface:

- Explicit four-value statusAsyncStatus from @fuzdev/fuz_util/async.js: `'initial' | 'pending' | 'success' | 'failure'. loading: false, error: null` would be ambiguous between "never tried" and "succeeded once and now resting"; the four-value status removes the need for a per-class submitted / hydrated flag. - Owns data: T | undefined — the success payload persists across retries (stale-while-revalidate). The sentinel is undefined (not null) so null stays available as a legitimate success value for nullable Ts. Pass T = void for write-only actions whose response isn't worth keeping. - Supersession via internal AbortController — a second run() aborts the first, and superseded results are silently discarded without writing to state. Removes the "in-flight call resolves after the locator advanced" race that locator-style state classes would otherwise need to compensate for. - AbortSignal threaded to the callback — RPC clients that accept a signal (or fetch) get cancellation for free; callers can also pass an external signal via {@link RunOptions} to bind the slot's lifetime to a component / page. - preserve_error_on_retry — opt-in to keeping the previous error visible while a retry is pending (default clears at the start of each run()). - Per-slot map_error — set once in the constructor ({map_error: to_rpc_error_message}); every run() gets the right normalization without re-passing per call. - Public run() — slots are composed, not subclassed, so call sites can invoke state.list.run(...) directly.

@example

class CellsState { readonly list = new AsyncSlot<{cells: ReadonlyArray<CellJson>}>(); readonly create = new AsyncSlot<{cell: CellJson}>({map_error: to_rpc_error_message}); async fetch() { await this.list.run((signal) => this.#api.cell_list({}, {signal})); } async submit_new(input: CellCreateInput) { const result = await this.create.run(() => this.#api.cell_create(input)); if (result) await this.fetch(); } }

Declarations
#

3 declarations

view source

AsyncSlot
#

ui/async_slot.svelte.ts view source

Reactive container for a single async operation.

generics

T

default void

E

default string

status

type AsyncStatus

data

type T | undefined

error

type E | null

error_data

The raw caught value from the last failed run(), for programmatic inspection.

type unknown

initial

Convenience derived: status === 'initial'.

type boolean

readonly

loading

Convenience derived: status === 'pending'.

type boolean

readonly

succeeded

Convenience derived: status === 'success'.

type boolean

readonly

failed

Convenience derived: status === 'failure'.

type boolean

readonly

constructor

type new <T = void, E = string>(options?: AsyncSlotOptions<T, E>): AsyncSlot<T, E>

options
type AsyncSlotOptions<T, E>
default {}

run

Run an async operation. The callback receives an AbortSignal it can forward to fetch / RPC clients that support cancellation; the slot also discards superseded results internally even if the callback ignores the signal.

Supersession rule: a second run() aborts the first's signal AND silently drops its commit if it resolves anyway. So back-to-back-to-back run() calls leave only the last call's result in data.

Abort rule: a run() that throws because of its own signal (manual abort(), external options.signal, OR supersession by another run()) does NOT promote to 'failure'. Manual / external aborts revert status to the previous resolved state ('initial' if no run() has ever succeeded, 'success' otherwise). Supersession is handled by the bail-on-mismatch check, leaving the second run's 'pending' standing.

type (fn: (signal: AbortSignal) => Promise<T>, options?: RunOptions): Promise<T | undefined>

fn
type (signal: AbortSignal) => Promise<T>
options
default {}
returns Promise<T | undefined>

the resolved value on success; undefined on failure, abort, or supersession

abort

Manually abort the in-flight run, if any. Reverts status synchronously to the prior resolved state — 'initial' if no run() (or set()) has ever succeeded on this slot, 'success' otherwise. The aborted run's eventual resolution / rejection is dropped without writing to state (the run's Promise resolves to undefined).

type (reason?: unknown): void

reason?
type unknown
optional
returns void

set

Replace data directly and mark the slot 'success'. For post-mutation hydration where the calling RPC already returned the canonical row (parallels CellState.set_cell).

Aborts any in-flight run() first — without this, the in-flight callback could resolve after set() and overwrite the explicit value (the bail-on-mismatch check only fires when #controller was rotated).

type (data: T): void

data
type T
returns void

reset

Reset to 'initial', clear data / error / error_data, and abort any in-flight run. After reset() the slot looks like a fresh instance with no initial option.

type (): void

returns void

AsyncSlotOptions
#

ui/async_slot.svelte.ts view source

AsyncSlotOptions<T, E>

generics

T

E

default string

initial

Seed data and put the slot in 'success' before any run(). Useful when the page already has the resource in hand (SSR hydration, a mutation response, hand-off from a parent slot).

type T

map_error

Convert a caught throw into the error value stored in {@link AsyncSlot.error}. Default extracts Error.message (falling back to 'Request failed' for non-Error throws). Pass to_rpc_error_message to unwrap JSON-RPC data.reason codes.

type (e: unknown) => E

preserve_error_on_retry

When true, the previous error / error_data survive the start of a new run() until the next success (or another failure overwrites them). Useful for retry UX that wants to keep the failure message visible alongside an inline spinner. Default falserun() clears the error at the start so the pending state reads "no current error."

type boolean

RunOptions
#

ui/async_slot.svelte.ts view source

RunOptions

signal

External signal chained into the slot's internal controller. Aborts the in-flight run when fired (alongside automatic supersession by the next run() and manual {@link AsyncSlot.abort} calls).

type AbortSignal

Imported by
#