import { packBody, packJson, packer, unpackBody, usePackers, unpackJson,} from "./pack.ts";
import type { Packers } from "./pack.ts";import type { AnyRpc, Rpc } from "./rpc.ts";import type { Stack } from "./stack.ts";import type { Parser, ParserFunction, ParserInput, ParserOutput,} from "./parser.ts";import type { TypedResponse } from "./http.ts";
export interface HttpErrorInit { status?: number; expose?: unknown; detail?: Record<string, unknown>;}
export class HttpError extends Error { status: number; expose?: unknown; detail: Record<string, unknown>;
constructor(message: string, init?: HttpErrorInit) { super(message); this.status = init?.status || 500; this.expose = init?.expose; this.detail = init?.detail || {}; }}
usePackers({ httpError: packer({ check: (v) => v instanceof HttpError, pack: (v: HttpError) => ({ status: v.status, message: v.message, expose: v.expose, }), unpack: (raw, whenDone) => { const u = (raw as { status: number; message: string }); const err = new HttpError(u.message, { status: u.status }); whenDone((parsed) => { err.expose = parsed.expose; }); return err; }, }),});
export interface Socket<Send = unknown, Message = unknown> { raw: WebSocket; send: (data: Send) => void; close: (code?: number, reason?: string) => void; on(type: "open", cb: SocketListener<"open">): void; on(type: "close", cb: SocketListener<"close">): void; on(type: "message", cb: SocketListener<"message", Message>): void; on(type: "error", cb: SocketListener<"error">): void; off( type?: "open" | "close" | "message" | "error", cb?: (ev: Event) => void | Promise<void>, ): void;}
export type AnySocket = Socket<any, any>;
export type SocketListener< Type extends "open" | "close" | "message" | "error", Message = unknown,> = (ev: ( Type extends "open" ? Event : Type extends "close" ? CloseEvent : Type extends "message" ? MessageEvent & { message: Message } : Type extends "error" ? Event | ErrorEvent : never)) => void | Promise<void>;
export interface SocketInit<Message extends Parser | null = null> { message?: Message; packers?: Packers | null;}
const decoder = new TextDecoder();
export function wrapWebSocket< Send = unknown, Message extends Parser | null = null,>( raw: WebSocket, init?: SocketInit<Message>,): Socket<Send, Message extends Parser ? ParserOutput<Message> : unknown> { const listeners = { open: new Set<(ev: Event) => unknown>(), close: new Set<(ev: CloseEvent) => unknown>(), message: new Map<unknown, (ev: MessageEvent) => unknown>(), error: new Set<(ev: Event | ErrorEvent) => unknown>(), };
return { raw, send: data => { raw.send(packJson(data, init?.packers)); }, close: (code, reason) => { raw.close(code, reason); }, on: (type, cb) => { if (type !== "message") { listeners[type].add(cb as (ev: Event) => unknown); raw.addEventListener(type, cb as (ev: Event) => unknown); return; }
const messageListener = async (ev: MessageEvent) => { const data = ev.data; if ( typeof data !== "string" && !ArrayBuffer.isView(data) && !(data instanceof Blob) ) { throw new Error(`Invalid data received: ${data}`); } let message: any = unpackJson(( typeof data === "string" ? data : ArrayBuffer.isView(data) ? decoder.decode(data) : await data.text() )); if (init?.message) { const parse: ParserFunction = ( typeof init.message === "function" ? init.message : init.message.parse ); message = await parse(message); }
Object.assign(ev, { message }); (cb as (ev: Event) => void)(ev); };
listeners.message.set(cb, messageListener); raw.addEventListener(type, messageListener); }, off: (type, cb) => { if (!cb) { const turnOff = (t: Exclude<typeof type, undefined>) => { for (const listener of listeners[t].values()) { raw.removeEventListener(t, listener as (ev: Event) => unknown); } listeners[t].clear(); }; if (!type) { for (const k of Object.keys(listeners)) { turnOff(k as keyof typeof listeners); } return; } turnOff(type); return; }
if (!type) { throw new Error("If a callback is specified, the event type must also be specified"); }
const listener = ( type === "message" ? listeners[type].get(cb) as (ev: Event) => unknown : listeners[type].has(cb) ? cb : undefined ); if (listener) { listeners[type].delete(cb); raw.removeEventListener(type, listener); } }, };}
export type Client<T = unknown> = ( T extends Stack<infer R> ? Client<R> : T extends Rpc< infer R, any, any, infer Q, infer M, infer U > ? Endpoint<R, Q, M, U> : T extends Record<never, never> ? UnionToIntersection<{ [K in keyof T]: ExpandPath<K, Client<T[K]>> }[keyof T]> : unknown);
export interface Endpoint< Resp, Query, Message, Upgrade,> { (x: EndpointArg<Query, Message, Upgrade>): ( Upgrade extends true ? ( Resp extends Socket<infer S, infer M> ? Socket<M, S> : never ) : Promise< Resp extends TypedResponse<infer T> ? T : Resp extends Response ? unknown : Resp > );}
export type EndpointArg< Query, Message, Upgrade,> = Clean<{ path?: string; query: ParserInput<Query>; message: Upgrade extends true ? never : ParserInput<Message>; packers?: Packers; upgrade: Upgrade extends true ? true : never;}>;
interface CustomFetchArg { path?: string; query?: Record<string, string | string[]>; message?: unknown; packers?: Packers; upgrade?: boolean;}
export function client<T extends Stack | AnyRpc>( base = "", packers?: Packers,): Client<T> { const customFetch = (path: string, x: CustomFetchArg = {}) => { const url = new URL(path, window.location.origin); if (x.query) { for (const [k, v] of Object.entries(x.query)) { if (Array.isArray(v)) { for (const v2 of v) { url.searchParams.append(k, v2); } } else { url.searchParams.append(k, v); } } } if (x.upgrade) { if (url.protocol === "http:") { url.protocol = "ws:"; } else { url.protocol = "wss:"; } const raw = new WebSocket(url.href, "json"); return wrapWebSocket(raw, { packers: x.packers }); } return (async () => { let body: BodyInit | null = null; let mime = ""; if (x.message) { const pb = packBody(x.message, x.packers); body = pb.body; mime = pb.mime; } const method = body === null ? "GET" : "POST"; const res = await fetch(url.href, { method, headers: mime ? { "content-type": mime } : {}, body, }); let resBody: unknown = undefined; if (res.body) { resBody = await unpackBody(res, x.packers); } if (!res.ok) { const detail = { body: resBody }; let message: string; let status: number; let expose: unknown; if (resBody instanceof HttpError) { message = resBody.message; status = resBody.status; expose = resBody.expose; } else if (typeof resBody === "string") { message = resBody; status = res.status; expose = undefined; } else { message = res.statusText; status = res.status; expose = undefined; } throw new HttpError(message, { status, expose, detail }); } return resBody; })(); };
const proxy = (path: string, packers?: Packers): unknown => { return new Proxy((x: CustomFetchArg) => customFetch(path, { ...x, packers: { ...packers, ...x.packers }, }), { get(_, property) { if (typeof property !== "string") { throw new TypeError("Symbol segments can't be used on the client"); } const append = property.split("/").filter(p => !!p).join("/"); return proxy(path.endsWith("/") ? path + append : path + "/" + append); } }); };
return proxy(base, packers) as Client<T>;}
type ExpandPath<K, T> = ( K extends `*` | `:${string}` ? { [x: string]: T } : K extends `:${string}/${infer P2}` ? { [x: string]: ExpandPath<P2, T> } : K extends `/${infer P}` | `${infer P}/` | `${infer P}/*` ? ExpandPath<P, T> : K extends `${infer P1}/${infer P2}` ? { [x in P1]: ExpandPath<P2, T> } : K extends string ? { [x in K]: T } : never);
type Clean< T, Required = { [K in keyof T as ( T[K] extends never ? never : undefined extends T[K] ? never : K )]: T[K]; }, Optional = { [K in keyof T as ( K extends keyof Required ? never : T[K] extends never ? never : K )]?: T[K]; },> = Required & Optional;
type UnionToIntersection<U> = ( U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? { [K in keyof I]: I[K] } : never