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,
});
};
}
Thread
Login to reply
Replies ()
No replies yet. Be the first to leave a comment!