import { untrack } from "svelte";
type ActionState<T> =
| { kind: "idle" }
| { kind: "loading" }
| { kind: "ready"; value: T }
| { kind: "failed"; error: string; cause: unknown };
type Operation<T, Args extends unknown[] = []> = (...args: Args) => Promise<T>;
const defaultError = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause));
const create = <T, Args extends unknown[] = []>(
operation: Operation<T, Args>,
initial: ActionState<T>,
toError: typeof defaultError
) => {
let state = $state<ActionState<T>>(initial);
let revision = 0;
let args: Args | undefined;
const run: Operation<void, Args> = async (...nextArgs) => {
const currentRevision = ++revision;
args = nextArgs;
if (untrack(() => state.kind) !== "loading") 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), cause };
}
};
return {
get data() {
return state.kind === "ready" ? state.value : undefined;
},
get error() {
return state.kind === "failed" ? state.error : undefined;
},
get cause() {
return state.kind === "failed" ? state.cause : 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 = <T, Args extends unknown[] = []>(
operation: Operation<T, Args>,
toError: typeof defaultError = defaultError
) => create(operation, { kind: "idle" }, toError);
export const createLoad = <T>(load: Operation<T>, toError: typeof defaultError = defaultError) => {
const action = create(load, { kind: "loading" }, toError);
$effect(() => void action.run());
return action;
};