@rhythmjs/router 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,15 +9,15 @@ Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches r
9
9
  ```ts
10
10
  import { Rhythm } from "@rhythmjs/rhythm";
11
11
  import { RhythmRouter } from "@rhythmjs/router";
12
- import { toFetchHandler } from "@rhythmjs/router/adapters/bun";
13
- import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
12
+ import { serve } from "@rhythmjs/router/serve";
13
+ import type { RhythmHttpContext } from "@rhythmjs/router/context";
14
14
 
15
15
  const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
16
16
  ctx.json({ id: ctx.params.id });
17
17
  });
18
18
 
19
- const app = new Rhythm<RhythmHttpContext>().use(usersRouter.routes());
20
- Bun.serve({ fetch: toFetchHandler(app) });
19
+ const app = new Rhythm<RhythmHttpContext>().use(usersRouter.middleware());
20
+ serve(app, { port: 3000 });
21
21
  ```
22
22
 
23
23
  A fuller runnable version, including nested prefixes and a fallback route, is at [`examples/router`](../../examples/router).
@@ -37,18 +37,118 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
37
37
  - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
38
38
  - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmRouter` (prefixes compose).
39
39
  - `.routes()` — this router as a plain middleware, for mounting into a `Rhythm` app via `.use()`; the router's only way onto a server. Note: mounting a _router_ into a _router_ must use `.use(child)`, not `.use(child.routes())` — an opaque middleware can't have the parent's prefix applied to its routes.
40
- - `ctx.json/.text/.html(body, status?)`, `ctx.error(status, message?)`, `ctx.redirect(url, status?)` — response helpers built into the context by the adapters (`createHttpContext` in `adapters/context`).
40
+ - `ctx.json/.text/.html(body, status?)`, `ctx.error(status, message?)`, `ctx.redirect(url, status?)` — response helpers built into the context by the adapters (`createHttpContext` in `@rhythmjs/router/context`).
41
41
  - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
42
42
 
43
- ## Runtime adapters
43
+ ## Serving
44
44
 
45
- Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic; only turning it into an actual server touches a specific runtime.
45
+ Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic. Two primitives turn an app into a server, both built on [srvx](https://srvx.h3.dev):
46
46
 
47
- - **`@rhythmjs/router/adapters/web-std`** — `toFetchHandler(app)`, the runtime-neutral Web-standard `(Request) => Promise<Response>` adapter every fetch-based runtime can use.
48
- - **`@rhythmjs/router/adapters/bun`** — re-exports `toFetchHandler` from `web-std`, for `Bun.serve({ fetch: toFetchHandler(app) })`.
49
- - **`@rhythmjs/router/adapters/deno`** — re-exports the same `toFetchHandler`, for `Deno.serve(toFetchHandler(app))`. `Deno.serve()` accepts the identical handler shape `Bun.serve()` does, so no conversion is needed.
50
- - **`@rhythmjs/router/adapters/node`** — `toNodeHandler(app, options?)`, for `http.createServer(toNodeHandler(app)).listen(port)`. Node's `IncomingMessage`/`ServerResponse` aren't Web-standard, so this one does real conversion:
51
- - The request body is read eagerly into a buffer, up to `options.bodyLimit` (default 1mb). This guarantees the socket is always fully drained before the handler runs, even if the handler never reads `ctx.request`'s body — otherwise, on a keep-alive connection, unconsumed bytes left on the socket would stall the next request on it. A body over the limit gets a `413` and the connection is closed rather than kept alive.
52
- - Pass `{ bodyLimit: false }` to opt out of buffering — `ctx.request`'s body becomes a live stream over the raw connection instead, with no size limit, for uploads or proxying where materializing the whole body in memory isn't acceptable. This reintroduces the keep-alive caveat: if the handler doesn't read the body, unconsumed bytes are left on the socket.
53
- - The resulting `Response` is streamed back to the client via `Readable.fromWeb(...).pipe(res)`.
54
- - Errors thrown anywhere in the middleware chain are caught, logged via `console.error`, and answered with a 500 (or the connection is destroyed if headers were already sent) — without this, an unhandled rejection would crash the whole Node process.
47
+ - **`serve(app, options)`** (`@rhythmjs/router/serve`) — starts a server on Node, Bun, or Deno with one identical call; srvx picks the runtime implementation via conditional exports. On Node, requests are lazy: method, url, headers, and body materialize only when middleware touches them.
48
+ - **`toFetchHandler(app)`** (`@rhythmjs/router/fetch`) — the universal `(Request) => Promise<Response>` handler, for platforms that invoke you per request instead of letting you own a listener.
49
+
50
+ `serve()` accepts every srvx `ServerOptions` field except `fetch`: `port`, `hostname`, `tls` (HTTPS/HTTP2), `maxRequestBodySize` (an over-limit body read is answered with `413`), `reusePort`, `gracefulShutdown`, plus the three extension points below. Errors thrown in the middleware chain are answered with `500` (or the error's own `status`) without crashing the process; override the mapping with `options.error`.
51
+
52
+ ### Extending: CORS, WebSockets, and similar
53
+
54
+ - **`middleware`** — srvx middlewares (`(request, next) => Response`) run around the whole app, the natural place for CORS, logging, or auth gates:
55
+
56
+ ```ts
57
+ serve(app, {
58
+ middleware: [
59
+ async (request, next) => {
60
+ if (request.method === "OPTIONS")
61
+ return new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } });
62
+ const response = await next();
63
+ response.headers.set("access-control-allow-origin", "*");
64
+ return response;
65
+ },
66
+ ],
67
+ });
68
+ ```
69
+
70
+ - **`plugins`** — a plugin receives the live srvx `Server`, the hook for anything below fetch, WebSockets included ([crossws](https://crossws.h3.dev) attaches here; on Node the raw server is at `server.node.server`):
71
+
72
+ ```ts
73
+ serve(app, { plugins: [(server) => wireWebSockets(server)] });
74
+ ```
75
+
76
+ - **`error`** — replace the default error-to-response mapping.
77
+
78
+ ## Deploying per runtime
79
+
80
+ One app definition; each runtime has its own adapter under `@rhythmjs/router/adapters/*`, hono-style: import from your target's adapter and export what the platform expects. Runtimes that speak web-standard `Request`/`Response` natively (Bun, Deno, Vercel) can equally use `toFetchHandler(app)` from `@rhythmjs/router/fetch` — their adapters are aliases for it.
81
+
82
+ Every adapter maps errors thrown in the middleware chain to a response (`500`, or the error's own `status`) instead of crashing, and accepts `{ onError }` to replace that mapping: `handle(app, { onError: (error) => new Response("down", { status: 503 }) })`. Platform metadata (Cloudflare `env`/`ctx`, the Netlify context) is readable in middleware via `getRuntime(ctx.request)` from `@rhythmjs/router/context`.
83
+
84
+ **Node** — `getRequestListener` returns a listener for your own `node:http` server:
85
+
86
+ ```ts
87
+ import { createServer } from "node:http";
88
+ import { getRequestListener } from "@rhythmjs/router/adapters/node";
89
+
90
+ createServer(getRequestListener(app, { maxRequestBodySize: 1024 * 1024 })).listen(3000);
91
+ ```
92
+
93
+ (Or skip the adapter entirely and use `serve(app, { port: 3000 })`.)
94
+
95
+ **Bun**
96
+
97
+ ```ts
98
+ import { handle } from "@rhythmjs/router/adapters/bun";
99
+
100
+ Bun.serve({ port: 3000, fetch: handle(app) });
101
+ ```
102
+
103
+ **Deno**
104
+
105
+ ```ts
106
+ import { handle } from "@rhythmjs/router/adapters/deno";
107
+
108
+ Deno.serve({ port: 3000 }, handle(app));
109
+ ```
110
+
111
+ **Vercel** — in a catch-all route file:
112
+
113
+ ```ts
114
+ import { handle } from "@rhythmjs/router/adapters/vercel";
115
+
116
+ const handler = handle(app);
117
+ export const GET = handler;
118
+ export const POST = handler; // …and the other methods you serve
119
+ ```
120
+
121
+ **Cloudflare Workers** — `env` and `ctx` are exposed to middleware as `ctx.request.runtime.cloudflare`:
122
+
123
+ ```ts
124
+ import { handle } from "@rhythmjs/router/adapters/cloudflare";
125
+
126
+ export default { fetch: handle(app) };
127
+ ```
128
+
129
+ **AWS Lambda** — API Gateway v1/v2 events, translated by srvx:
130
+
131
+ ```ts
132
+ import { handle } from "@rhythmjs/router/adapters/aws-lambda";
133
+
134
+ export const lambda = handle(app);
135
+ ```
136
+
137
+ **Netlify Edge Functions** — the Netlify context is exposed as `ctx.request.runtime.netlify`:
138
+
139
+ ```ts
140
+ import { handle } from "@rhythmjs/router/adapters/netlify";
141
+
142
+ export default handle(app);
143
+ export const config = { path: "/*" };
144
+ ```
145
+
146
+ **Service workers**
147
+
148
+ ```ts
149
+ import { handle } from "@rhythmjs/router/adapters/service-worker";
150
+
151
+ addEventListener("fetch", handle(app));
152
+ ```
153
+
154
+ Any other fetch-based runtime works with the raw primitive: `toFetchHandler(app)` from `@rhythmjs/router/fetch`.
@@ -0,0 +1,8 @@
1
+ import { t as RhythmHttpContext } from "../context-ChHlG-q0.js";
2
+ import { t as AdapterOptions } from "../base--TyruAfY.js";
3
+ import { AWSLambdaHandler } from "srvx/aws-lambda";
4
+ import { Rhythm } from "@rhythmjs/rhythm";
5
+ //#region src/adapters/aws-lambda.d.ts
6
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: AdapterOptions): AWSLambdaHandler;
7
+ //#endregion
8
+ export type { AWSLambdaHandler, AdapterOptions };
@@ -0,0 +1,9 @@
1
+ import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ import { toLambdaHandler } from "srvx/aws-lambda";
3
+ //#region src/adapters/aws-lambda.ts
4
+ const fetchHandle = createFetchAdapter();
5
+ function handle(app, options = {}) {
6
+ return toLambdaHandler({ fetch: fetchHandle(app, options) });
7
+ }
8
+ //#endregion
9
+ export { handle };
@@ -1,2 +1,5 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
2
+ //#region src/adapters/bun.d.ts
3
+ export declare const handle: FetchAdapter;
4
+ //#endregion
5
+ export type { AdapterOptions };
@@ -1,2 +1,5 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ //#region src/adapters/bun.ts
3
+ const handle = createFetchAdapter();
4
+ //#endregion
5
+ export { handle };
@@ -0,0 +1,5 @@
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
2
+ //#region src/adapters/cloudflare.d.ts
3
+ export declare const handle: FetchAdapter<[env?: unknown, context?: unknown]>;
4
+ //#endregion
5
+ export type { AdapterOptions };
@@ -0,0 +1,13 @@
1
+ import { n as setRuntime, t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ //#region src/adapters/cloudflare.ts
3
+ const handle = createFetchAdapter((request, env, context) => {
4
+ if (env !== void 0 || context !== void 0) setRuntime(request, {
5
+ name: "cloudflare",
6
+ cloudflare: {
7
+ env,
8
+ context
9
+ }
10
+ });
11
+ });
12
+ //#endregion
13
+ export { handle };
@@ -1,2 +1,5 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
2
+ //#region src/adapters/deno.d.ts
3
+ export declare const handle: FetchAdapter;
4
+ //#endregion
5
+ export type { AdapterOptions };
@@ -1,2 +1,5 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ //#region src/adapters/deno.ts
3
+ const handle = createFetchAdapter();
4
+ //#endregion
5
+ export { handle };
@@ -0,0 +1,5 @@
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
2
+ //#region src/adapters/netlify.d.ts
3
+ export declare const handle: FetchAdapter<[context?: unknown]>;
4
+ //#endregion
5
+ export type { AdapterOptions };
@@ -0,0 +1,10 @@
1
+ import { n as setRuntime, t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ //#region src/adapters/netlify.ts
3
+ const handle = createFetchAdapter((request, context) => {
4
+ if (context !== void 0) setRuntime(request, {
5
+ name: "netlify",
6
+ netlify: { context }
7
+ });
8
+ });
9
+ //#endregion
10
+ export { handle };
@@ -1,12 +1,11 @@
1
- import { t as RhythmHttpContext } from "../context-B0711qPV.js";
1
+ import { t as RhythmHttpContext } from "../context-ChHlG-q0.js";
2
+ import { t as AdapterOptions } from "../base--TyruAfY.js";
3
+ import { NodeHttp1Handler, NodeHttp2Handler } from "srvx";
2
4
  import { Rhythm } from "@rhythmjs/rhythm";
3
- import { IncomingMessage, ServerResponse } from "node:http";
4
5
  //#region src/adapters/node.d.ts
5
- export declare class RhythmBodyTooLargeError extends Error {
6
- constructor(limit: number);
6
+ export interface NodeHandlerOptions extends AdapterOptions {
7
+ maxRequestBodySize?: number;
7
8
  }
8
- export interface RhythmNodeHandlerOptions {
9
- bodyLimit?: number | false;
10
- }
11
- export declare function toNodeHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmNodeHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
9
+ export type NodeHandler = NodeHttp1Handler & NodeHttp2Handler;
10
+ export declare function getRequestListener<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: NodeHandlerOptions): NodeHandler;
12
11
  //#endregion
@@ -1,135 +1,9 @@
1
- import { createHttpContext, toResponse } from "./context.js";
2
- import { Readable } from "node:stream";
3
- import { Buffer } from "node:buffer";
1
+ import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ import { toNodeHandler } from "srvx/node";
4
3
  //#region src/adapters/node.ts
5
- const DEFAULT_BODY_LIMIT = 1048576;
6
- var RhythmBodyTooLargeError = class extends Error {
7
- constructor(limit) {
8
- super(`request body exceeds the ${limit} byte limit`);
9
- this.name = "RhythmBodyTooLargeError";
10
- }
11
- };
12
- function readRawBody(req, limit) {
13
- return new Promise((resolve, reject) => {
14
- const chunks = [];
15
- let received = 0;
16
- let tooLarge = false;
17
- let settled = false;
18
- req.on("data", (chunk) => {
19
- received += chunk.length;
20
- if (received > limit) {
21
- if (!tooLarge) {
22
- tooLarge = true;
23
- settled = true;
24
- reject(new RhythmBodyTooLargeError(limit));
25
- }
26
- return;
27
- }
28
- chunks.push(chunk);
29
- });
30
- req.on("end", () => {
31
- if (settled) return;
32
- settled = true;
33
- resolve(Buffer.concat(chunks));
34
- });
35
- req.on("error", (err) => {
36
- if (settled) return;
37
- settled = true;
38
- reject(err);
39
- });
40
- req.on("aborted", () => {
41
- if (settled) return;
42
- settled = true;
43
- reject(/* @__PURE__ */ new Error("request aborted"));
44
- });
45
- });
46
- }
47
- async function toWebRequest(req, bodyLimit) {
48
- const method = req.method ?? "GET";
49
- const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
50
- const headers = [];
51
- for (const key in req.headers) {
52
- const value = req.headers[key];
53
- if (value === void 0) continue;
54
- if (Array.isArray(value)) for (const entry of value) headers.push([key, entry]);
55
- else headers.push([key, value]);
56
- }
57
- if (!(method !== "GET" && method !== "HEAD")) return new Request(url, {
58
- method,
59
- headers
60
- });
61
- if (bodyLimit === false) return new Request(url, {
62
- method,
63
- headers,
64
- body: Readable.toWeb(req),
65
- duplex: "half"
66
- });
67
- const body = await readRawBody(req, bodyLimit);
68
- return new Request(url, {
69
- method,
70
- headers,
71
- body
72
- });
73
- }
74
- function toNodeHeaders(headers) {
75
- const result = {};
76
- const setCookie = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [];
77
- if (setCookie.length > 0) result["set-cookie"] = setCookie;
78
- for (const [key, value] of headers) {
79
- if (key === "set-cookie") continue;
80
- result[key] = value;
81
- }
82
- return result;
83
- }
84
- async function writeWebResponse(response, res) {
85
- res.writeHead(response.status, toNodeHeaders(response.headers));
86
- if (!response.body) {
87
- res.end();
88
- return;
89
- }
90
- await new Promise((resolve, reject) => {
91
- const nodeStream = Readable.fromWeb(response.body);
92
- nodeStream.on("error", reject);
93
- res.on("finish", resolve);
94
- nodeStream.pipe(res);
95
- });
96
- }
97
- function writeDirectResponse(response, res) {
98
- const { body, status } = response;
99
- const headers = toNodeHeaders(response.headers);
100
- let payload;
101
- if (typeof body === "string") payload = body;
102
- else if (body instanceof Uint8Array) payload = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
103
- else if (body instanceof ArrayBuffer) payload = Buffer.from(body);
104
- if (!("content-length" in headers) && status !== 204 && status !== 304) headers["content-length"] = payload === void 0 ? "0" : String(typeof payload === "string" ? Buffer.byteLength(payload) : payload.byteLength);
105
- if (response.statusText !== void 0) res.writeHead(status, response.statusText, headers);
106
- else res.writeHead(status, headers);
107
- res.end(payload);
108
- }
109
- function hasDirectBody(body) {
110
- return body === null || typeof body === "string" || body instanceof Uint8Array || body instanceof ArrayBuffer;
111
- }
112
- function toNodeHandler(app, options = {}) {
113
- const run = app.callback();
114
- const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
115
- return async (req, res) => {
116
- try {
117
- const request = await toWebRequest(req, bodyLimit);
118
- const ctx = await run(createHttpContext(request));
119
- if (hasDirectBody(ctx.response.body)) writeDirectResponse(ctx.response, res);
120
- else await writeWebResponse(toResponse(ctx.response), res);
121
- } catch (err) {
122
- if (!(err instanceof RhythmBodyTooLargeError)) console.error(err);
123
- if (!res.headersSent) {
124
- const status = err instanceof RhythmBodyTooLargeError ? 413 : 500;
125
- const message = err instanceof RhythmBodyTooLargeError ? err.message : "Internal Server Error";
126
- const headers = { "content-type": "text/plain; charset=utf-8" };
127
- if (err instanceof RhythmBodyTooLargeError) headers.connection = "close";
128
- res.writeHead(status, headers);
129
- res.end(message);
130
- } else res.destroy(err instanceof Error ? err : new Error(String(err)));
131
- }
132
- };
4
+ const fetchHandle = createFetchAdapter();
5
+ function getRequestListener(app, options = {}) {
6
+ return toNodeHandler(fetchHandle(app, options), options);
133
7
  }
134
8
  //#endregion
135
- export { RhythmBodyTooLargeError, toNodeHandler };
9
+ export { getRequestListener };
@@ -0,0 +1,11 @@
1
+ import { t as RhythmHttpContext } from "../context-ChHlG-q0.js";
2
+ import { t as AdapterOptions } from "../base--TyruAfY.js";
3
+ import { Rhythm } from "@rhythmjs/rhythm";
4
+ //#region src/adapters/service-worker.d.ts
5
+ interface FetchEventLike {
6
+ readonly request: Request;
7
+ respondWith(response: Response | Promise<Response>): void;
8
+ }
9
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: AdapterOptions): (event: FetchEventLike) => void;
10
+ //#endregion
11
+ export type { AdapterOptions };
@@ -0,0 +1,11 @@
1
+ import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ //#region src/adapters/service-worker.ts
3
+ const fetchHandle = createFetchAdapter();
4
+ function handle(app, options = {}) {
5
+ const handler = fetchHandle(app, options);
6
+ return (event) => {
7
+ event.respondWith(handler(event.request));
8
+ };
9
+ }
10
+ //#endregion
11
+ export { handle };
@@ -0,0 +1,5 @@
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
2
+ //#region src/adapters/vercel.d.ts
3
+ export declare const handle: FetchAdapter;
4
+ //#endregion
5
+ export type { AdapterOptions };
@@ -0,0 +1,5 @@
1
+ import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
2
+ //#region src/adapters/vercel.ts
3
+ const handle = createFetchAdapter();
4
+ //#endregion
5
+ export { handle };
@@ -0,0 +1,10 @@
1
+ import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
+ import { ErrorHandler, ServerRuntimeContext } from "srvx";
3
+ import { Rhythm } from "@rhythmjs/rhythm";
4
+ //#region src/adapters/base.d.ts
5
+ interface AdapterOptions {
6
+ onError?: ErrorHandler;
7
+ }
8
+ type FetchAdapter<TExtra extends unknown[] = []> = <TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: AdapterOptions) => (request: Request, ...extra: TExtra) => Promise<Response>;
9
+ //#endregion
10
+ export { FetchAdapter as n, AdapterOptions as t };
@@ -0,0 +1,25 @@
1
+ import { toFetchHandler } from "./fetch.js";
2
+ import { errorToResponse } from "./serve.js";
3
+ //#region src/adapters/base.ts
4
+ function setRuntime(request, runtime) {
5
+ Object.defineProperty(request, "runtime", {
6
+ value: runtime,
7
+ configurable: true
8
+ });
9
+ }
10
+ function createFetchAdapter(enrich) {
11
+ return (app, options = {}) => {
12
+ const handler = toFetchHandler(app);
13
+ const onError = options.onError ?? errorToResponse;
14
+ return async (request, ...extra) => {
15
+ try {
16
+ enrich?.(request, ...extra);
17
+ return await handler(request);
18
+ } catch (error) {
19
+ return onError(error);
20
+ }
21
+ };
22
+ };
23
+ }
24
+ //#endregion
25
+ export { setRuntime as n, createFetchAdapter as t };
@@ -1,4 +1,7 @@
1
- //#region src/adapters/context.d.ts
1
+ import { ServerRuntimeContext } from "srvx";
2
+ //#region src/context.d.ts
3
+ type RhythmRuntime = ServerRuntimeContext;
4
+ declare function getRuntime(request: Request): RhythmRuntime | undefined;
2
5
  type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
3
6
  declare class RhythmResponse {
4
7
  status: number;
@@ -18,4 +21,4 @@ interface RhythmHttpContext {
18
21
  declare function createHttpContext(request: Request): RhythmHttpContext;
19
22
  declare function toResponse(response: RhythmResponse): Response;
20
23
  //#endregion
21
- export { toResponse as a, createHttpContext as i, RhythmResponse as n, RhythmResponseBody as r, RhythmHttpContext as t };
24
+ export { createHttpContext as a, RhythmRuntime as i, RhythmResponse as n, getRuntime as o, RhythmResponseBody as r, toResponse as s, RhythmHttpContext as t };
@@ -0,0 +1,2 @@
1
+ import { a as createHttpContext, i as RhythmRuntime, n as RhythmResponse, o as getRuntime, r as RhythmResponseBody, s as toResponse, t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
+ export { RhythmHttpContext, RhythmResponse, RhythmResponseBody, RhythmRuntime, createHttpContext, getRuntime, toResponse };
@@ -1,4 +1,7 @@
1
- //#region src/adapters/context.ts
1
+ //#region src/context.ts
2
+ function getRuntime(request) {
3
+ return request.runtime;
4
+ }
2
5
  var RhythmResponse = class {
3
6
  status = 200;
4
7
  statusText = void 0;
@@ -63,4 +66,4 @@ function toResponse(response) {
63
66
  });
64
67
  }
65
68
  //#endregion
66
- export { RhythmResponse, createHttpContext, toResponse };
69
+ export { RhythmResponse, createHttpContext, getRuntime, toResponse };
@@ -1,5 +1,5 @@
1
- import { t as RhythmHttpContext } from "../context-B0711qPV.js";
1
+ import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
2
  import { Rhythm } from "@rhythmjs/rhythm";
3
- //#region src/adapters/web-std.d.ts
3
+ //#region src/fetch.d.ts
4
4
  export declare function toFetchHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
5
  //#endregion
package/dist/fetch.js ADDED
@@ -0,0 +1,16 @@
1
+ import { createHttpContext } from "./context.js";
2
+ import { FastResponse } from "srvx";
3
+ //#region src/fetch.ts
4
+ function toFetchHandler(app) {
5
+ const run = app.callback();
6
+ return async (request) => {
7
+ const response = (await run(createHttpContext(request))).response;
8
+ return new FastResponse(response.body, {
9
+ status: response.status,
10
+ statusText: response.statusText,
11
+ headers: response.headers
12
+ });
13
+ };
14
+ }
15
+ //#endregion
16
+ export { toFetchHandler };
@@ -1,4 +1,4 @@
1
- import { t as RhythmHttpContext } from "./context-B0711qPV.js";
1
+ import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
2
  import { DeriveMiddleware, Middleware } from "@rhythmjs/rhythm/types";
3
3
  //#region src/rhythm-router.d.ts
4
4
  export interface RhythmRouterContext {
@@ -0,0 +1,9 @@
1
+ import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
+ import { ErrorHandler, Server, ServerMiddleware, ServerOptions, ServerPlugin } from "srvx";
3
+ import { Rhythm } from "@rhythmjs/rhythm";
4
+ //#region src/serve.d.ts
5
+ export type RhythmServeOptions = Omit<ServerOptions, "fetch">;
6
+ export declare const errorToResponse: ErrorHandler;
7
+ export declare function serve<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmServeOptions): Server;
8
+ //#endregion
9
+ export type { ErrorHandler, Server, ServerMiddleware, ServerPlugin };
package/dist/serve.js ADDED
@@ -0,0 +1,21 @@
1
+ import { toFetchHandler } from "./fetch.js";
2
+ import { serve as serve$1 } from "srvx";
3
+ //#region src/serve.ts
4
+ const errorToResponse = (error) => {
5
+ const status = error.status ?? error.statusCode ?? 500;
6
+ const message = status >= 500 ? "Internal Server Error" : error instanceof Error ? error.message : String(error);
7
+ if (status >= 500) console.error(error);
8
+ return new Response(message, {
9
+ status,
10
+ headers: { "content-type": "text/plain; charset=utf-8" }
11
+ });
12
+ };
13
+ function serve(app, options = {}) {
14
+ return serve$1({
15
+ error: errorToResponse,
16
+ ...options,
17
+ fetch: toFetchHandler(app)
18
+ });
19
+ }
20
+ //#endregion
21
+ export { errorToResponse, serve };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Radix-tree HTTP router for the Rhythm middleware kernel, with Bun, Node, and Deno adapters.",
5
5
  "homepage": "https://rhythm.js.org/router",
6
6
  "license": "ISC",
@@ -23,9 +23,21 @@
23
23
  "types": "./dist/radix-tree.d.ts",
24
24
  "default": "./dist/radix-tree.js"
25
25
  },
26
- "./adapters/web-std": {
27
- "types": "./dist/adapters/web-std.d.ts",
28
- "default": "./dist/adapters/web-std.js"
26
+ "./context": {
27
+ "types": "./dist/context.d.ts",
28
+ "default": "./dist/context.js"
29
+ },
30
+ "./fetch": {
31
+ "types": "./dist/fetch.d.ts",
32
+ "default": "./dist/fetch.js"
33
+ },
34
+ "./serve": {
35
+ "types": "./dist/serve.d.ts",
36
+ "default": "./dist/serve.js"
37
+ },
38
+ "./adapters/node": {
39
+ "types": "./dist/adapters/node.d.ts",
40
+ "default": "./dist/adapters/node.js"
29
41
  },
30
42
  "./adapters/bun": {
31
43
  "types": "./dist/adapters/bun.d.ts",
@@ -35,13 +47,29 @@
35
47
  "types": "./dist/adapters/deno.d.ts",
36
48
  "default": "./dist/adapters/deno.js"
37
49
  },
38
- "./adapters/node": {
39
- "types": "./dist/adapters/node.d.ts",
40
- "default": "./dist/adapters/node.js"
50
+ "./adapters/vercel": {
51
+ "types": "./dist/adapters/vercel.d.ts",
52
+ "default": "./dist/adapters/vercel.js"
53
+ },
54
+ "./adapters/netlify": {
55
+ "types": "./dist/adapters/netlify.d.ts",
56
+ "default": "./dist/adapters/netlify.js"
57
+ },
58
+ "./adapters/cloudflare": {
59
+ "types": "./dist/adapters/cloudflare.d.ts",
60
+ "default": "./dist/adapters/cloudflare.js"
61
+ },
62
+ "./adapters/aws-lambda": {
63
+ "types": "./dist/adapters/aws-lambda.d.ts",
64
+ "default": "./dist/adapters/aws-lambda.js"
65
+ },
66
+ "./adapters/service-worker": {
67
+ "types": "./dist/adapters/service-worker.d.ts",
68
+ "default": "./dist/adapters/service-worker.js"
41
69
  },
42
70
  "./adapters/context": {
43
- "types": "./dist/adapters/context.d.ts",
44
- "default": "./dist/adapters/context.js"
71
+ "types": "./dist/context.d.ts",
72
+ "default": "./dist/context.js"
45
73
  },
46
74
  "./package.json": "./package.json"
47
75
  },
@@ -49,7 +77,8 @@
49
77
  "access": "public"
50
78
  },
51
79
  "dependencies": {
52
- "@rhythmjs/rhythm": "0.0.7"
80
+ "srvx": "^1.0.5",
81
+ "@rhythmjs/rhythm": "0.0.9"
53
82
  },
54
83
  "devDependencies": {
55
84
  "@types/node": "^26.6.3",
@@ -1,2 +0,0 @@
1
- import { a as toResponse, i as createHttpContext, n as RhythmResponse, r as RhythmResponseBody, t as RhythmHttpContext } from "../context-B0711qPV.js";
2
- export { RhythmHttpContext, RhythmResponse, RhythmResponseBody, createHttpContext, toResponse };
@@ -1,11 +0,0 @@
1
- import { createHttpContext, toResponse } from "./context.js";
2
- //#region src/adapters/web-std.ts
3
- function toFetchHandler(app) {
4
- const run = app.callback();
5
- return async (request) => {
6
- const ctx = await run(createHttpContext(request));
7
- return toResponse(ctx.response);
8
- };
9
- }
10
- //#endregion
11
- export { toFetchHandler };