Created
October 2, 2026 17:16
-
-
Save cowboyd/6dc41dc2ac69dcca33f8ee5ed0cba48f to your computer and use it in GitHub Desktop.
one-off revoution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| action, | |
| createContext, | |
| ensure, | |
| once, | |
| type Operation, | |
| race, | |
| resource, | |
| suspend, | |
| until, | |
| useScope, | |
| withResolvers, | |
| } from "effection"; | |
| import { serveDir, type ServeDirOptions } from "@std/http"; | |
| import { toHtml } from "hast-util-to-html"; | |
| import { match, type MatchResult, type ParamData } from "path-to-regexp"; | |
| import type * as hast from "hast"; | |
| export type { JSXChild, JSXElement } from "revolution/jsx-runtime"; | |
| import type { JSXElement } from "revolution/jsx-runtime"; | |
| export interface Handler<Input, Output> { | |
| (request: Input): Operation<Output>; | |
| } | |
| export interface Middleware<Input, Output> { | |
| (request: Input, next: Handler<Input, Output>): Operation<Output>; | |
| } | |
| export interface RevolutionPlugin { | |
| html?: HTMLMiddleware | HTMLMiddleware[]; | |
| http?: HTTPMiddleware | HTTPMiddleware[]; | |
| } | |
| export type HASTHtmlNode = hast.Element & { | |
| type: "element"; | |
| tagName: "html"; | |
| }; | |
| export type HTMLMiddleware = Middleware<Request, HASTHtmlNode>; | |
| export type HTTPMiddleware = Middleware<Request, Response>; | |
| export type AppMiddleware = Middleware<Request, JSXElement> | HTTPMiddleware; | |
| export interface RevolutionOptions { | |
| app?: AppMiddleware[]; | |
| plugins?: RevolutionPlugin[]; | |
| } | |
| export interface ServerInfo { | |
| hostname: string; | |
| port: number; | |
| } | |
| export interface Revolution { | |
| start(options?: { port?: number }): Operation<ServerInfo>; | |
| } | |
| const OptionsContext = createContext<RevolutionOptions>("revolution.options"); | |
| const ParamsContext = createContext<MatchResult<ParamData>["params"]>( | |
| "revolution.params", | |
| ); | |
| const ResponseContext = createContext<(response: Response) => void>( | |
| "revolution.http-response", | |
| ); | |
| export function* useRevolutionOptions(): Operation<RevolutionOptions> { | |
| return yield* OptionsContext.expect(); | |
| } | |
| export function* useParams<T extends object>(): Operation<T> { | |
| return (yield* ParamsContext.expect()) as T; | |
| } | |
| export function route<T>( | |
| path: string, | |
| ...middlewares: Middleware<Request, T>[] | |
| ): Middleware<Request, T> { | |
| let handler = concat(...middlewares); | |
| return function* (request, next) { | |
| let result = match(path)(new URL(request.url).pathname); | |
| if (result) { | |
| // path-to-regexp uses a null-prototype object, which cannot be rendered | |
| // in Effection's diagnostic description for Context.set(). | |
| yield* ParamsContext.set({ ...result.params }); | |
| return yield* handler(request, next); | |
| } else { | |
| return yield* next(request); | |
| } | |
| }; | |
| } | |
| export function* respondNotFound(): Operation<never> { | |
| let respond = yield* ResponseContext.expect(); | |
| respond(new Response("Not Found", { status: 404, statusText: "Not Found" })); | |
| yield* suspend(); | |
| throw new Error("unreachable"); | |
| } | |
| export function serveDirMiddleware( | |
| options?: ServeDirOptions, | |
| ): HTTPMiddleware { | |
| return function* (request) { | |
| return yield* until(serveDir(request, options)); | |
| }; | |
| } | |
| export function createRevolution(options: RevolutionOptions = {}): Revolution { | |
| let handler = createApp(options); | |
| return { | |
| *start({ port } = {}) { | |
| return yield* useServer({ handler, port }); | |
| }, | |
| }; | |
| } | |
| function createApp({ | |
| app: middlewares = [], | |
| plugins = [], | |
| }: RevolutionOptions): Handler<Request, Response> { | |
| let html = concat(...plugins.flatMap((plugin) => [plugin.html ?? []].flat())); | |
| let http = concat(...plugins.flatMap((plugin) => [plugin.http ?? []].flat())); | |
| let app = concat(...middlewares.map<HTTPMiddleware>((middleware) => { | |
| return function* (request, next) { | |
| let invoke = middleware as Middleware<Request, Response | JSXElement>; | |
| let result = yield* invoke(request, next); | |
| if (result instanceof Response) { | |
| return result; | |
| } else { | |
| assertIsHTMLNode(result); | |
| let element = yield* html(request, function* () { | |
| return result; | |
| }); | |
| return serializeHtml(element); | |
| } | |
| }; | |
| })); | |
| let provideOptions: HTTPMiddleware = function* (request, next) { | |
| yield* OptionsContext.set({ app: middlewares, plugins }); | |
| return yield* next(request); | |
| }; | |
| return dispatch( | |
| concat(provideOptions, httpResponsesMiddleware(), http, app), | |
| respondNotFound, | |
| ); | |
| } | |
| function httpResponsesMiddleware(): HTTPMiddleware { | |
| return function* (request, next) { | |
| try { | |
| let response = withResolvers<Response>(); | |
| yield* ResponseContext.set(response.resolve); | |
| return yield* race([response.operation, next(request)]); | |
| } catch (error) { | |
| return new Response((error as Error).stack, { | |
| status: 500, | |
| statusText: "Internal Server Error", | |
| }); | |
| } | |
| }; | |
| } | |
| function useServer( | |
| options: { port?: number; handler: Handler<Request, Response> }, | |
| ): Operation<ServerInfo> { | |
| return resource(function* (provide) { | |
| let scope = yield* useScope(); | |
| let server: Deno.HttpServer | undefined; | |
| yield* ensure(function* () { | |
| if (server) { | |
| yield* until(server.shutdown()); | |
| } | |
| }); | |
| let handler = (request: Request) => | |
| scope.run(function* () { | |
| let response = yield* race([ | |
| aborted(request), | |
| options.handler(request), | |
| ]); | |
| let driver = getDriver(response); | |
| if (driver) { | |
| scope.run(function* () { | |
| if (driver.params) { | |
| yield* ParamsContext.set(driver.params); | |
| } | |
| yield* race([aborted(request), driver.operation()]); | |
| }); | |
| } | |
| return response; | |
| }); | |
| let info = yield* action<ServerInfo>((resolve) => { | |
| server = Deno.serve({ handler, port: options.port, onListen: resolve }); | |
| return () => {}; | |
| }); | |
| yield* provide(info); | |
| }); | |
| } | |
| function* aborted(request: Request): Operation<Response> { | |
| if (!request.signal.aborted) { | |
| yield* once(request.signal, "abort"); | |
| } | |
| return new Response("aborted"); | |
| } | |
| interface Driver { | |
| operation(): Operation<void>; | |
| params?: MatchResult<ParamData>["params"]; | |
| } | |
| const DRIVER = Symbol.for("revolution.driver"); | |
| function getDriver(response: Response): Driver | undefined { | |
| return (response as Response & { [DRIVER]?: Driver })[DRIVER]; | |
| } | |
| function concat<Input, Output>( | |
| ...middlewares: Middleware<Input, Output>[] | |
| ): Middleware<Input, Output> { | |
| return middlewares.reduceRight<Middleware<Input, Output>>( | |
| (rest, middleware) => (request, next) => | |
| middleware(request, (value) => rest(value, next)), | |
| (request, next) => next(request), | |
| ); | |
| } | |
| function dispatch<Input, Output>( | |
| middleware: Middleware<Input, Output>, | |
| terminus: Handler<Input, Output>, | |
| ): Handler<Input, Output> { | |
| return (request) => middleware(request, terminus); | |
| } | |
| function assertIsHTMLNode(value: JSXElement): asserts value is HASTHtmlNode { | |
| if ( | |
| value.type !== "element" || value.tagName.toLocaleLowerCase() !== "html" | |
| ) { | |
| throw new Error("expected an <html> element"); | |
| } | |
| } | |
| function serializeHtml(node: HASTHtmlNode): Response { | |
| return new Response(toHtml([{ type: "doctype" }, node]), { | |
| headers: { "Content-Type": "text/html; charset=utf-8;" }, | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment