~james/core

core/web/src/lib/action.svelte.ts -rw-r--r-- 2.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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;
};