47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
export interface ActionClientOptions<I> {
|
|
signal?: AbortSignal;
|
|
csrfToken?: string;
|
|
headers?: HeadersInit;
|
|
serialize?: (input: I) => BodyInit;
|
|
}
|
|
|
|
export interface ActionResult<O> {
|
|
data: O;
|
|
invalidated: string[];
|
|
}
|
|
|
|
export class ActionClientError extends Error {
|
|
constructor(
|
|
public readonly status: number,
|
|
public readonly errors?: Record<string, string>,
|
|
) {
|
|
super(`Server action failed with status ${status}.`);
|
|
this.name = "ActionClientError";
|
|
}
|
|
}
|
|
|
|
export function createActionClient<I, O>(route: string, name: string) {
|
|
return async (input: I, options: ActionClientOptions<I> = {}): Promise<ActionResult<O>> => {
|
|
const headers = new Headers(options.headers);
|
|
headers.set("accept", "application/json");
|
|
headers.set("x-wrnexus-action", name);
|
|
if (options.csrfToken) headers.set("x-csrf-token", options.csrfToken);
|
|
const body = options.serialize ? options.serialize(input) : JSON.stringify(input);
|
|
if (!options.serialize) headers.set("content-type", "application/json");
|
|
const response = await fetch(route, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
signal: options.signal,
|
|
headers,
|
|
body,
|
|
});
|
|
const payload = (await response.json()) as {
|
|
data?: O;
|
|
invalidated?: string[];
|
|
errors?: Record<string, string>;
|
|
};
|
|
if (!response.ok) throw new ActionClientError(response.status, payload.errors);
|
|
return { data: payload.data as O, invalidated: payload.invalidated ?? [] };
|
|
};
|
|
}
|