export interface ActionClientOptions { signal?: AbortSignal; csrfToken?: string; headers?: HeadersInit; serialize?: (input: I) => BodyInit; } export interface ActionResult { data: O; invalidated: string[]; } export class ActionClientError extends Error { constructor( public readonly status: number, public readonly errors?: Record, ) { super(`Server action failed with status ${status}.`); this.name = "ActionClientError"; } } export function createActionClient(route: string, name: string) { return async (input: I, options: ActionClientOptions = {}): Promise> => { 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; }; if (!response.ok) throw new ActionClientError(response.status, payload.errors); return { data: payload.data as O, invalidated: payload.invalidated ?? [] }; }; }