type ActionState = | { kind: "idle" } | { kind: "loading" } | { kind: "ready"; value: T } | { kind: "failed"; error: string }; type Operation = (...args: Args) => Promise; const defaultError = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause)); const create = ( operation: Operation, initial: ActionState, toError: typeof defaultError ) => { let state = $state>(initial); let revision = 0; let args: Args | undefined; const run: Operation = async (...nextArgs) => { const currentRevision = ++revision; args = nextArgs; state = { kind: "loading" }; try { const value = await operation(...nextArgs); if (currentRevision !== revision) return; args = undefined; state = { kind: "ready", value }; } catch (cause) { if (currentRevision !== revision) return; args = undefined; state = { kind: "failed", error: toError(cause) }; } }; return { get data() { return state.kind === "ready" ? state.value : undefined; }, get error() { return state.kind === "failed" ? state.error : undefined; }, get loading() { return state.kind === "loading"; }, get args() { return state.kind === "loading" ? args : undefined; }, run, update(update: (current: T) => T): boolean { if (state.kind !== "ready") return false; revision++; state = { kind: "ready", value: update(state.value) }; return true; } }; }; // loads run automatically and rerun when reactive values change: // const post = createLoad(() => api.getPost(id)); // // actions only run when called: // const save = createAction(api.savePost); // await save.run(draft); // // both expose .data, .error, .loading, and .args. export const createAction = ( operation: Operation, toError: typeof defaultError = defaultError ) => create(operation, { kind: "idle" }, toError); export const createLoad = (load: Operation, toError: typeof defaultError = defaultError) => { const action = create(load, { kind: "loading" }, toError); $effect(() => void action.run()); return action; };