64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
export interface ServerCallOptions {
|
|
endpoint?: string;
|
|
signal?: AbortSignal;
|
|
headers?: HeadersInit;
|
|
csrfToken?: string;
|
|
}
|
|
export class WrnServerCallError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly code: string,
|
|
readonly status: number,
|
|
readonly details?: unknown,
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
function csrfFromCookie(): string | undefined {
|
|
if (typeof document === "undefined") return undefined;
|
|
const raw = /(?:^|;\s*)wire-csrf=([^;]+)/.exec(document.cookie)?.[1];
|
|
return raw ? decodeURIComponent(raw) : undefined;
|
|
}
|
|
|
|
export async function callServerFunction<TInput extends unknown[], TOutput>(
|
|
component: string,
|
|
functionName: string,
|
|
args: TInput,
|
|
options: ServerCallOptions = {},
|
|
): Promise<TOutput> {
|
|
const csrfToken = options.csrfToken ?? csrfFromCookie();
|
|
const response = await fetch(options.endpoint ?? "/__wrnexus/rpc", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
signal: options.signal,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
...(csrfToken ? { "x-csrf-token": csrfToken } : {}),
|
|
...options.headers,
|
|
},
|
|
body: JSON.stringify({ component, function: functionName, args }),
|
|
});
|
|
const payload = (await response.json().catch(() => null)) as any;
|
|
if (!response.ok || !payload?.ok)
|
|
throw new WrnServerCallError(
|
|
payload?.error?.message ?? `Server call failed (${response.status})`,
|
|
payload?.error?.code ?? "WRN-RPC-FAILED",
|
|
response.status,
|
|
payload?.error?.details,
|
|
);
|
|
return payload.value as TOutput;
|
|
}
|
|
|
|
export function createServerProxy<T extends Record<string, (...args: any[]) => Promise<any>>>(
|
|
component: string,
|
|
options: ServerCallOptions = {},
|
|
): T {
|
|
return new Proxy({} as T, {
|
|
get:
|
|
(_target, property) =>
|
|
(...args: unknown[]) =>
|
|
callServerFunction(component, String(property), args, options),
|
|
});
|
|
}
|