@rhythmjs/router 0.0.2 → 0.0.3

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
@@ -13,7 +13,7 @@ import { toFetchHandler } from "@rhythmjs/router/adapters/bun";
13
13
  import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
14
14
 
15
15
  const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
16
- ctx.response.body = JSON.stringify({ id: ctx.params.id });
16
+ ctx.json({ id: ctx.params.id });
17
17
  });
18
18
 
19
19
  const app = new Rhythm<RhythmHttpContext>().use(usersRouter.routes());
@@ -25,6 +25,7 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
25
25
  ## Concepts
26
26
 
27
27
  - **`ctx.response`** is a plain mutable object (`status`, `statusText`, `headers`, `body`) — set it directly rather than constructing a `Response` yourself. The adapter converts it to a real `Response` at the end.
28
+ - **Response helpers** — `ctx.json(data, status?)`, `ctx.text(body, status?)`, `ctx.html(body, status?)`, `ctx.error(status, message?)`, and `ctx.redirect(url, status = 302)` set the content type, body, and status on `ctx.response` in one call. `error()` defaults the message from the status code (`ctx.error(404)` → `"Not Found"`). They're sugar over `ctx.response`, so mixing both styles is fine, and later writes win.
28
29
  - **`ctx.params`** — captured `:name` path segments, added once a route matches.
29
30
  - **Prefixes compose across nesting** — a child router mounted into a prefixed parent via `.use(child)` gets the parent's prefix joined onto every one of its routes, at any nesting depth. Mounting copies the child's routes and middleware at that moment; routes added to the child afterwards don't appear in the parent, and the child keeps working standalone.
30
31
  - **Registration order is execution order** — a `.use()` middleware wraps only the routes registered after it; routes registered before it are untouched, and a matched route that doesn't call `next()` returns without reaching anything registered later. Consecutive routes share one radix tree lookup; an unmatched request falls through, entry by entry, to the outer `next()`.
@@ -36,14 +37,16 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
36
37
  - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
37
38
  - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmRouter` (prefixes compose).
38
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`).
39
41
  - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
40
42
 
41
43
  ## Runtime adapters
42
44
 
43
45
  Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic; only turning it into an actual server touches a specific runtime.
44
46
 
45
- - **`@rhythmjs/router/adapters/bun`** — `toFetchHandler(app)`, for `Bun.serve({ fetch: toFetchHandler(app) })`.
46
- - **`@rhythmjs/router/adapters/deno`** — re-exports the same `toFetchHandler`, for `Deno.serve(toFetchHandler(app))`. `Deno.serve()` accepts the identical `(Request) => Promise<Response>` shape `Bun.serve()` does, so no conversion is needed.
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.
47
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:
48
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.
49
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.
@@ -1,5 +1,2 @@
1
- import { t as RhythmHttpContext } from "../context-CjxK7bH9.js";
2
- import { Rhythm } from "@rhythmjs/rhythm";
3
- //#region src/adapters/bun.d.ts
4
- export declare function toFetchHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
- //#endregion
1
+ import { toFetchHandler } from "./web-std.js";
2
+ export { toFetchHandler };
@@ -1,14 +1,2 @@
1
- import { RhythmResponse, toResponse } from "./context.js";
2
- //#region src/adapters/bun.ts
3
- function toFetchHandler(app) {
4
- const run = app.callback();
5
- return async (request) => {
6
- const ctx = await run({
7
- request,
8
- response: new RhythmResponse()
9
- });
10
- return toResponse(ctx.response);
11
- };
12
- }
13
- //#endregion
1
+ import { toFetchHandler } from "./web-std.js";
14
2
  export { toFetchHandler };
@@ -1,2 +1,2 @@
1
- import { i as toResponse, n as RhythmResponse, r as RhythmResponseBody, t as RhythmHttpContext } from "../context-CjxK7bH9.js";
2
- export { RhythmHttpContext, RhythmResponse, RhythmResponseBody, toResponse };
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,12 +1,60 @@
1
- import { RhythmMutable } from "@rhythmjs/rhythm";
2
1
  //#region src/adapters/context.ts
3
2
  var RhythmResponse = class {
4
- [RhythmMutable] = true;
5
3
  status = 200;
6
4
  statusText = void 0;
7
5
  headers = new Headers();
8
6
  body = null;
9
7
  };
8
+ const STATUS_TEXT = {
9
+ 400: "Bad Request",
10
+ 401: "Unauthorized",
11
+ 403: "Forbidden",
12
+ 404: "Not Found",
13
+ 405: "Method Not Allowed",
14
+ 409: "Conflict",
15
+ 410: "Gone",
16
+ 413: "Payload Too Large",
17
+ 415: "Unsupported Media Type",
18
+ 422: "Unprocessable Entity",
19
+ 429: "Too Many Requests",
20
+ 500: "Internal Server Error",
21
+ 501: "Not Implemented",
22
+ 502: "Bad Gateway",
23
+ 503: "Service Unavailable",
24
+ 504: "Gateway Timeout"
25
+ };
26
+ function createHttpContext(request) {
27
+ const response = new RhythmResponse();
28
+ return {
29
+ request,
30
+ response,
31
+ json(data, status) {
32
+ if (status !== void 0) response.status = status;
33
+ response.headers.set("content-type", "application/json; charset=utf-8");
34
+ response.body = JSON.stringify(data);
35
+ },
36
+ text(body, status) {
37
+ if (status !== void 0) response.status = status;
38
+ response.headers.set("content-type", "text/plain; charset=utf-8");
39
+ response.body = body;
40
+ },
41
+ html(body, status) {
42
+ if (status !== void 0) response.status = status;
43
+ response.headers.set("content-type", "text/html; charset=utf-8");
44
+ response.body = body;
45
+ },
46
+ error(status, message) {
47
+ response.status = status;
48
+ response.headers.set("content-type", "text/plain; charset=utf-8");
49
+ response.body = message ?? STATUS_TEXT[status] ?? `Error ${status}`;
50
+ },
51
+ redirect(url, status = 302) {
52
+ response.status = status;
53
+ response.headers.set("location", url);
54
+ response.body = null;
55
+ }
56
+ };
57
+ }
10
58
  function toResponse(response) {
11
59
  return new Response(response.body, {
12
60
  status: response.status,
@@ -15,4 +63,4 @@ function toResponse(response) {
15
63
  });
16
64
  }
17
65
  //#endregion
18
- export { RhythmResponse, toResponse };
66
+ export { RhythmResponse, createHttpContext, toResponse };
@@ -1,2 +1,2 @@
1
- import { toFetchHandler } from "./bun.js";
1
+ import { toFetchHandler } from "./web-std.js";
2
2
  export { toFetchHandler };
@@ -1,2 +1,2 @@
1
- import { toFetchHandler } from "./bun.js";
1
+ import { toFetchHandler } from "./web-std.js";
2
2
  export { toFetchHandler };
@@ -1,4 +1,4 @@
1
- import { t as RhythmHttpContext } from "../context-CjxK7bH9.js";
1
+ import { t as RhythmHttpContext } from "../context-B0711qPV.js";
2
2
  import { Rhythm } from "@rhythmjs/rhythm";
3
3
  import { IncomingMessage, ServerResponse } from "node:http";
4
4
  //#region src/adapters/node.d.ts
@@ -1,4 +1,4 @@
1
- import { RhythmResponse, toResponse } from "./context.js";
1
+ import { createHttpContext, toResponse } from "./context.js";
2
2
  import { Readable } from "node:stream";
3
3
  //#region src/adapters/node.ts
4
4
  const DEFAULT_BODY_LIMIT = 1048576;
@@ -98,10 +98,7 @@ function toNodeHandler(app, options = {}) {
98
98
  return async (req, res) => {
99
99
  try {
100
100
  const request = await toWebRequest(req, bodyLimit);
101
- const ctx = await run({
102
- request,
103
- response: new RhythmResponse()
104
- });
101
+ const ctx = await run(createHttpContext(request));
105
102
  await writeWebResponse(toResponse(ctx.response), res);
106
103
  } catch (err) {
107
104
  if (!(err instanceof RhythmBodyTooLargeError)) console.error(err);
@@ -0,0 +1,5 @@
1
+ import { t as RhythmHttpContext } from "../context-B0711qPV.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/web-std.d.ts
4
+ export declare function toFetchHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
+ //#endregion
@@ -0,0 +1,11 @@
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 };
@@ -0,0 +1,21 @@
1
+ //#region src/adapters/context.d.ts
2
+ type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
3
+ declare class RhythmResponse {
4
+ status: number;
5
+ statusText: string | undefined;
6
+ headers: Headers;
7
+ body: RhythmResponseBody;
8
+ }
9
+ interface RhythmHttpContext {
10
+ readonly request: Request;
11
+ readonly response: RhythmResponse;
12
+ json(data: unknown, status?: number): void;
13
+ text(body: string, status?: number): void;
14
+ html(body: string, status?: number): void;
15
+ error(status: number, message?: string): void;
16
+ redirect(url: string, status?: number): void;
17
+ }
18
+ declare function createHttpContext(request: Request): RhythmHttpContext;
19
+ declare function toResponse(response: RhythmResponse): Response;
20
+ //#endregion
21
+ export { toResponse as a, createHttpContext as i, RhythmResponse as n, RhythmResponseBody as r, RhythmHttpContext as t };
@@ -1,8 +1,8 @@
1
- import { t as RhythmHttpContext } from "./context-CjxK7bH9.js";
2
- import { Middleware } from "@rhythmjs/rhythm";
1
+ import { t as RhythmHttpContext } from "./context-B0711qPV.js";
2
+ import { Middleware } from "@rhythmjs/rhythm/types";
3
3
  //#region src/rhythm-router.d.ts
4
4
  export interface RhythmRouterContext {
5
- params: Record<string, string>;
5
+ readonly params: Readonly<Record<string, string>>;
6
6
  }
7
7
  export interface RhythmRouterOptions {
8
8
  prefix?: string;
@@ -1,5 +1,5 @@
1
1
  import { createNode, insertRoute, joinPath, lookupRoute } from "./radix-tree.js";
2
- import { compose } from "@rhythmjs/rhythm";
2
+ import { compose } from "@rhythmjs/rhythm/compose";
3
3
  //#region src/rhythm-router.ts
4
4
  var RhythmRouter = class RhythmRouter {
5
5
  #options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
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,6 +23,10 @@
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"
29
+ },
26
30
  "./adapters/bun": {
27
31
  "types": "./dist/adapters/bun.d.ts",
28
32
  "default": "./dist/adapters/bun.js"
@@ -45,7 +49,7 @@
45
49
  "access": "public"
46
50
  },
47
51
  "dependencies": {
48
- "@rhythmjs/rhythm": "0.0.2"
52
+ "@rhythmjs/rhythm": "0.0.3"
49
53
  },
50
54
  "devDependencies": {
51
55
  "@types/node": "^26.6.3",
@@ -1,17 +0,0 @@
1
- import { RhythmMutable } from "@rhythmjs/rhythm";
2
- //#region src/adapters/context.d.ts
3
- type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
4
- declare class RhythmResponse {
5
- readonly [RhythmMutable] = true;
6
- status: number;
7
- statusText: string | undefined;
8
- headers: Headers;
9
- body: RhythmResponseBody;
10
- }
11
- interface RhythmHttpContext {
12
- request: Request;
13
- response: RhythmResponse;
14
- }
15
- declare function toResponse(response: RhythmResponse): Response;
16
- //#endregion
17
- export { toResponse as i, RhythmResponse as n, RhythmResponseBody as r, RhythmHttpContext as t };