Thread

🛡️
import { ServerSentEventGenerator } from "@starfederation/datastar-sdk/web"; import type { BunRequest } from "bun"; import type { RouterTypes } from "bun"; type Mutation<Args extends any[], Result = any> = ( stream: ServerSentEventGenerator, ...args: Args ) => Promise<Result> | Result; // Extract args excluding the first stream parameter type MutationArgs<T> = T extends Mutation<infer Args> ? Args : never; type MutationAnchor<Args extends any[]> = (...args: Args) => string; // Type to extract mutation anchors from a mutations object export type InferAnchors<M extends Record<string, Mutation<any[]>>> = { [K in keyof M]: MutationAnchor<MutationArgs<M[K]>>; }; // Internal registry that preserves individual mutation types type InternalMutations<M extends Record<string, Mutation<any[]>>> = { [K in keyof M]: { mutation: M[K]; anchor: MutationAnchor<MutationArgs<M[K]>>; }; }; /** Helper to create a mutations object with proper type inference */ export function mutations<const M extends Record<string, Mutation<any[]>>>( mutationsObj: M, ): M { return mutationsObj; } /** A helper method that renders a component to a html string and wraps it in a Document component */ export function page< M extends Record<string, Mutation<any[]>>, R extends string = string, >( component: (props: { req: BunRequest<R>; mutations: { [K in keyof M]: MutationAnchor<MutationArgs<M[K]>>; }; }) => JSX.Element, mutations?: M, ): RouterTypes.RouteHandler<R> { // Create internal mutations with preserved types const internal = {} as InternalMutations<M>; const mutationAnchors = {} as { [K in keyof M]: MutationAnchor<MutationArgs<M[K]>>; }; // Build internal registry preserving individual function types (only if mutations provided) if (mutations) { for (const key in mutations) { const mutation = mutations[key]; const anchor = ((...args: MutationArgs<M[typeof key]>) => { const headers = JSON.stringify({ "X-Mutation-Name": key, "X-Mutation-Args": JSON.stringify(args), }); return `@post(location.href, {headers: ${headers}})`; }) as MutationAnchor<MutationArgs<M[typeof key]>>; internal[key] = { mutation, anchor, }; mutationAnchors[key] = anchor; } } return async (req: BunRequest<R>) => { switch (req.method) { // GET request renders the component case "GET": const html = await component({ req, mutations: mutationAnchors }); return new Response(html.toString(), { headers: { "Content-Type": "text/html", }, }); // POST request handles mutations case "POST": if (!mutations) return new Response("No mutations defined for this page", { status: 405, }); const mutationName = req.headers.get("X-Mutation-Name"); if (!mutationName) return new Response("Missing mutation name", { status: 400 }); // Find the mutation by name - now type-safe const foundKey = mutationName as keyof M; const foundMutation = internal[foundKey]?.mutation; if (!foundMutation) return new Response("Unknown mutation", { status: 404 }); // Parse arguments from headers const argsHeader = req.headers.get("X-Mutation-Args"); let args: Parameters<M[typeof foundKey]>; if (argsHeader) { try { args = JSON.parse(argsHeader); } catch { return new Response("Invalid mutation arguments", { status: 400 }); } } else { args = [] as unknown as Parameters<M[typeof foundKey]>; } // Execute the mutation with SSE stream - now perfectly typed return ServerSentEventGenerator.stream(async (stream) => { try { const result = await foundMutation(stream, ...args); // Patch success signal with result stream.patchSignals( JSON.stringify({ mutations: { [String(foundKey)]: { result, error: null, }, }, }), ); } catch (error) { console.error("Mutation error:", error); // Patch error signal const errorMessage = error instanceof Error ? error.message : "Unknown error"; stream.patchSignals( JSON.stringify({ mutations: { [String(foundKey)]: { result: null, error: errorMessage, }, }, }), ); } }); } return new Response("Method not allowed", { status: 405, }); }; }

Replies (0)

No replies yet. Be the first to leave a comment!