@rhythmjs/router 0.0.1 → 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
@@ -1,6 +1,8 @@
1
1
  # @rhythmjs/router
2
2
 
3
- Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches routes with a compressed radix tree (a static segment always wins over a `:param` segment, regardless of registration order), supports prefixes and nested routers, and mounts flat into a parent via `.use(router.routes())`, so an unmatched request correctly falls through to whatever's registered after it.
3
+ Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches routes with a compressed radix tree (a static segment always wins over a `:param` segment, regardless of registration order), supports prefixes and nested routers, and mounts flat into a parent `Rhythm` app via `.use(router.routes())`, so an unmatched request correctly falls through to whatever's registered after it.
4
+
5
+ `RhythmRouter` is not an app and does not extend `Rhythm` — it is a controller that compiles routes and middleware down to a single middleware (`.routes()`). It shares the core middleware contract (`compose`, `Middleware`, `next(extra)`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle and the adapters.
4
6
 
5
7
  ## Example
6
8
 
@@ -11,7 +13,7 @@ import { toFetchHandler } from "@rhythmjs/router/adapters/bun";
11
13
  import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
12
14
 
13
15
  const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
14
- ctx.response.body = JSON.stringify({ id: ctx.params.id });
16
+ ctx.json({ id: ctx.params.id });
15
17
  });
16
18
 
17
19
  const app = new Rhythm<RhythmHttpContext>().use(usersRouter.routes());
@@ -23,24 +25,28 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
23
25
  ## Concepts
24
26
 
25
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.
26
29
  - **`ctx.params`** — captured `:name` path segments, added once a route matches.
27
- - **Prefixes compose across nesting** — a router mounted into a prefixed parent via `.use(child.routes())` gets the parent's prefix joined onto every one of its routes, at any nesting depth.
28
- - **`register()` is disabled** on `RhythmRouter` — a router is a "controller"; it composes routes and middleware, not other modules. Mount it into a module with `.use(router.routes())`, not `.register()`.
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.
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()`.
32
+ - **A router is a controller, not a module** — it has no `provide()` or `register()`, and it cannot be `register()`ed into a `Rhythm` app either; `register()` composes `Rhythm` modules only. A router mounts into an app exactly one way: koa-style, via `.use(router.routes())`.
29
33
 
30
34
  ## API
31
35
 
32
- - `new RhythmRouter(options?)` — `options.name`, `options.prefix`.
36
+ - `new RhythmRouter(options?)` — `options.prefix`.
33
37
  - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
34
- - `.use(fn)` — plain middleware, or mount a nested `RhythmRouter` via `.use(child.routes())`.
35
- - `.routes()` — returns this router as a plain middleware, for mounting into a parent via `.use()`.
36
- - `toFetchHandler(app)` — bridges a `Rhythm`/`RhythmRouter` app to a Web-standard `(Request) => Promise<Response>` handler.
38
+ - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmRouter` (prefixes compose).
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`).
41
+ - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
37
42
 
38
43
  ## Runtime adapters
39
44
 
40
45
  Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic; only turning it into an actual server touches a specific runtime.
41
46
 
42
- - **`@rhythmjs/router/adapters/bun`** — `toFetchHandler(app)`, for `Bun.serve({ fetch: toFetchHandler(app) })`.
43
- - **`@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.
44
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:
45
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.
46
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
@@ -6,16 +6,6 @@ export declare class RhythmBodyTooLargeError extends Error {
6
6
  constructor(limit: number);
7
7
  }
8
8
  export interface RhythmNodeHandlerOptions {
9
- /**
10
- * Maximum request body size in bytes, enforced by eagerly buffering the body
11
- * before the handler runs. Defaults to 1mb, same as raw-body/koa-bodyparser.
12
- *
13
- * Pass `false` to opt out of buffering entirely: ctx.request's body becomes a
14
- * live stream over the raw connection, with no size limit and no automatic
15
- * drain if the handler doesn't read it (see the adapter's README for the
16
- * keep-alive caveat that reintroduces). Use this for uploads or proxying,
17
- * where materializing the whole body in memory isn't acceptable.
18
- */
19
9
  bodyLimit?: number | false;
20
10
  }
21
11
  export declare function toNodeHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmNodeHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -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,22 +1,16 @@
1
- import { Middleware, NextFn } from "@rhythmjs/rhythm";
2
1
  //#region src/radix-tree.d.ts
3
- type RouteDispatch = (context: any, next?: NextFn<any>) => Promise<any>;
4
- export type MethodEntry = {
5
- handlers: Middleware<any>[];
6
- dispatch: RouteDispatch;
7
- };
8
- export declare class TreeNode<TMethod extends string = string> {
2
+ export declare class TreeNode<TMethod extends string = string, TPayload = unknown> {
9
3
  path: string;
10
4
  indices: string;
11
- children: TreeNode<TMethod>[];
12
- paramChild: TreeNode<TMethod> | null;
5
+ children: TreeNode<TMethod, TPayload>[];
6
+ paramChild: TreeNode<TMethod, TPayload> | null;
13
7
  paramName: string;
14
- methods: Map<TMethod, MethodEntry> | null;
8
+ methods: Map<TMethod, TPayload> | null;
15
9
  }
16
- export declare function createNode<TMethod extends string = string>(): TreeNode<TMethod>;
17
- export declare function insertRoute<TMethod extends string>(root: TreeNode<TMethod>, method: TMethod, path: string, handlers: Middleware<any>[]): void;
18
- export declare function lookupRoute<TMethod extends string>(root: TreeNode<TMethod>, method: string, pathname: string): {
19
- entry: MethodEntry;
10
+ export declare function createNode<TMethod extends string = string, TPayload = unknown>(): TreeNode<TMethod, TPayload>;
11
+ export declare function insertRoute<TMethod extends string, TPayload>(root: TreeNode<TMethod, TPayload>, method: TMethod, path: string, payload: TPayload): void;
12
+ export declare function lookupRoute<TMethod extends string, TPayload>(root: TreeNode<TMethod, TPayload>, method: string, pathname: string): {
13
+ payload: TPayload;
20
14
  params: Record<string, string>;
21
15
  } | null;
22
16
  export declare function joinPath(prefix: string, path: string): string;
@@ -1,4 +1,3 @@
1
- import { compose } from "@rhythmjs/rhythm";
2
1
  //#region src/radix-tree.ts
3
2
  var TreeNode = class {
4
3
  path = "";
@@ -32,13 +31,10 @@ function splitChild(node, at) {
32
31
  node.paramName = "";
33
32
  node.methods = null;
34
33
  }
35
- function insertAt(node, path, method, handlers) {
34
+ function insertAt(node, path, method, payload) {
36
35
  if (path.length === 0) {
37
36
  if (!node.methods) node.methods = /* @__PURE__ */ new Map();
38
- node.methods.set(method, {
39
- handlers,
40
- dispatch: compose(handlers)
41
- });
37
+ node.methods.set(method, payload);
42
38
  return;
43
39
  }
44
40
  if (path.charCodeAt(0) === 58) {
@@ -49,7 +45,7 @@ function insertAt(node, path, method, handlers) {
49
45
  node.paramChild = new TreeNode();
50
46
  node.paramChild.paramName = name;
51
47
  }
52
- insertAt(node.paramChild, rest, method, handlers);
48
+ insertAt(node.paramChild, rest, method, payload);
53
49
  return;
54
50
  }
55
51
  const colonIndex = path.indexOf(":");
@@ -61,17 +57,17 @@ function insertAt(node, path, method, handlers) {
61
57
  const cpl = commonPrefixLength(staticPart, child.path);
62
58
  if (cpl === 0) continue;
63
59
  if (cpl < child.path.length) splitChild(child, cpl);
64
- insertAt(child, path.slice(cpl), method, handlers);
60
+ insertAt(child, path.slice(cpl), method, payload);
65
61
  return;
66
62
  }
67
63
  const child = new TreeNode();
68
64
  child.path = staticPart;
69
65
  node.children.push(child);
70
66
  node.indices += firstChar;
71
- insertAt(child, path.slice(staticPart.length), method, handlers);
67
+ insertAt(child, path.slice(staticPart.length), method, payload);
72
68
  }
73
- function insertRoute(root, method, path, handlers) {
74
- insertAt(root, path, method, handlers);
69
+ function insertRoute(root, method, path, payload) {
70
+ insertAt(root, path, method, payload);
75
71
  }
76
72
  function lookupRoute(root, method, pathname) {
77
73
  const params = {};
@@ -105,10 +101,10 @@ function lookupRoute(root, method, pathname) {
105
101
  return null;
106
102
  }
107
103
  if (!node.methods) return null;
108
- const entry = node.methods.get(method);
109
- if (!entry) return null;
104
+ const payload = node.methods.get(method);
105
+ if (payload === void 0) return null;
110
106
  return {
111
- entry,
107
+ payload,
112
108
  params
113
109
  };
114
110
  }
@@ -1,19 +1,17 @@
1
- import { t as RhythmHttpContext } from "./context-CjxK7bH9.js";
2
- import { DeepReadonly, Middleware, OmitHashKeys, Rhythm } 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
- name?: string;
9
8
  prefix?: string;
10
9
  }
11
- export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext, TProviders extends object = {}> extends Rhythm<RhythmHttpContext, TContext, TProviders> {
10
+ export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext> {
12
11
  #private;
13
12
  constructor(options?: RhythmRouterOptions);
14
- use<TExtra extends object = {}>(fn: Middleware<TContext>): RhythmRouter<TContext & TExtra, TProviders>;
15
- provide<TValue extends object>(factory: (deps: DeepReadonly<TProviders>) => TValue | Promise<TValue>, dispose?: (value: TValue) => void | Promise<void>): RhythmRouter<TContext & OmitHashKeys<TValue>, TProviders & OmitHashKeys<TValue>>;
16
- register(): never;
13
+ use(child: RhythmRouter<any>): this;
14
+ use<TExtra extends object = {}>(fn: Middleware<TContext>): RhythmRouter<TContext & TExtra>;
17
15
  get<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
18
16
  post<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
19
17
  put<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
@@ -1,71 +1,39 @@
1
1
  import { createNode, insertRoute, joinPath, lookupRoute } from "./radix-tree.js";
2
- import { Rhythm } from "@rhythmjs/rhythm";
2
+ import { compose } from "@rhythmjs/rhythm/compose";
3
3
  //#region src/rhythm-router.ts
4
- const RhythmRouterTag = Symbol("RhythmRouterTag");
5
- var RhythmRouter = class extends Rhythm {
6
- #prefix;
4
+ var RhythmRouter = class RhythmRouter {
5
+ #options;
7
6
  #entries = [];
8
- #tree = createNode();
9
- #dispatchInstalled = false;
7
+ #composed = null;
10
8
  constructor(options = {}) {
11
- super({
12
- name: options.name ?? "router",
13
- type: "controller"
14
- });
15
- this.#prefix = options.prefix ?? "";
9
+ this.#options = options;
10
+ }
11
+ get #prefix() {
12
+ return this.#options.prefix ?? "";
16
13
  }
17
- use(fn) {
18
- const nested = fn[RhythmRouterTag];
19
- if (nested) for (const entry of nested.#entries) this.#mount(entry);
14
+ use(arg) {
15
+ if (arg instanceof RhythmRouter) for (const entry of arg.#entries) this.#entries.push(entry.kind === "route" ? {
16
+ ...entry,
17
+ path: joinPath(this.#prefix, entry.path)
18
+ } : entry);
20
19
  else {
20
+ if (typeof arg !== "function") throw new TypeError("middleware must be a function!");
21
21
  this.#entries.push({
22
22
  kind: "middleware",
23
- fn
23
+ fn: arg
24
24
  });
25
- super.use(fn);
26
25
  }
26
+ this.#composed = null;
27
27
  return this;
28
28
  }
29
- provide(factory, dispose) {
30
- super.provide(factory, dispose);
31
- return this;
32
- }
33
- register() {
34
- throw new Error("RhythmRouter is a controller and cannot register() other modules or controllers");
35
- }
36
- #mount(entry) {
37
- if (entry.kind === "middleware") {
38
- this.#entries.push(entry);
39
- super.use(entry.fn);
40
- return;
41
- }
42
- this.#registerRoute(entry.method, joinPath(this.#prefix, entry.path), entry.handlers);
43
- }
44
- #registerRoute(method, fullPath, handlers) {
29
+ #route(method, path, handlers) {
45
30
  this.#entries.push({
46
31
  kind: "route",
47
32
  method,
48
- path: fullPath,
33
+ path: joinPath(this.#prefix, path),
49
34
  handlers
50
35
  });
51
- insertRoute(this.#tree, method, fullPath, handlers);
52
- if (this.#dispatchInstalled) return;
53
- this.#dispatchInstalled = true;
54
- const tree = this.#tree;
55
- super.use(async (ctx, next) => {
56
- const match = lookupRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
57
- if (!match) {
58
- await next();
59
- return;
60
- }
61
- await match.entry.dispatch({
62
- ...ctx,
63
- params: match.params
64
- }, next);
65
- });
66
- }
67
- #route(method, path, handlers) {
68
- this.#registerRoute(method, joinPath(this.#prefix, path), handlers);
36
+ this.#composed = null;
69
37
  return this;
70
38
  }
71
39
  get(path, ...handlers) {
@@ -83,10 +51,46 @@ var RhythmRouter = class extends Rhythm {
83
51
  delete(path, ...handlers) {
84
52
  return this.#route("DELETE", path, handlers);
85
53
  }
54
+ #compile() {
55
+ if (this.#composed) return this.#composed;
56
+ const dispatchFor = (tree) => {
57
+ return async (ctx, next) => {
58
+ const match = lookupRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
59
+ if (!match) {
60
+ await next();
61
+ return;
62
+ }
63
+ await match.payload({
64
+ ...ctx,
65
+ params: match.params
66
+ }, next);
67
+ };
68
+ };
69
+ const stack = [];
70
+ let i = 0;
71
+ while (i < this.#entries.length) {
72
+ const entry = this.#entries[i];
73
+ if (entry.kind === "middleware") {
74
+ stack.push(entry.fn);
75
+ i++;
76
+ continue;
77
+ }
78
+ const tree = createNode();
79
+ while (i < this.#entries.length) {
80
+ const route = this.#entries[i];
81
+ if (route.kind !== "route") break;
82
+ insertRoute(tree, route.method, route.path, compose(route.handlers));
83
+ i++;
84
+ }
85
+ stack.push(dispatchFor(tree));
86
+ }
87
+ this.#composed = compose(stack);
88
+ return this.#composed;
89
+ }
86
90
  routes() {
87
- const mw = this.middleware();
88
- mw[RhythmRouterTag] = this;
89
- return mw;
91
+ return async (ctx, next) => {
92
+ await this.#compile()(ctx, next);
93
+ };
90
94
  }
91
95
  };
92
96
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Radix-tree HTTP router for the Rhythm middleware kernel, with Bun, Node, and Deno adapters.",
5
+ "homepage": "https://rhythm.js.org/router",
5
6
  "license": "ISC",
6
7
  "repository": {
7
8
  "type": "git",
@@ -22,6 +23,10 @@
22
23
  "types": "./dist/radix-tree.d.ts",
23
24
  "default": "./dist/radix-tree.js"
24
25
  },
26
+ "./adapters/web-std": {
27
+ "types": "./dist/adapters/web-std.d.ts",
28
+ "default": "./dist/adapters/web-std.js"
29
+ },
25
30
  "./adapters/bun": {
26
31
  "types": "./dist/adapters/bun.d.ts",
27
32
  "default": "./dist/adapters/bun.js"
@@ -44,7 +49,7 @@
44
49
  "access": "public"
45
50
  },
46
51
  "dependencies": {
47
- "@rhythmjs/rhythm": "0.0.1"
52
+ "@rhythmjs/rhythm": "0.0.3"
48
53
  },
49
54
  "devDependencies": {
50
55
  "@types/node": "^26.6.3",
@@ -53,10 +58,5 @@
53
58
  },
54
59
  "engines": {
55
60
  "node": ">=20.19.0"
56
- },
57
- "scripts": {
58
- "build": "vp pack",
59
- "typecheck": "tsc --noEmit",
60
- "test": "vp test"
61
61
  }
62
62
  }
@@ -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 };