@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 +16 -10
- package/dist/adapters/bun.d.ts +2 -5
- package/dist/adapters/bun.js +1 -13
- package/dist/adapters/context.d.ts +2 -2
- package/dist/adapters/context.js +51 -3
- package/dist/adapters/deno.d.ts +1 -1
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/node.d.ts +1 -11
- package/dist/adapters/node.js +2 -5
- package/dist/adapters/web-std.d.ts +5 -0
- package/dist/adapters/web-std.js +11 -0
- package/dist/context-B0711qPV.d.ts +21 -0
- package/dist/radix-tree.d.ts +8 -14
- package/dist/radix-tree.js +10 -14
- package/dist/rhythm-router.d.ts +6 -8
- package/dist/rhythm-router.js +58 -54
- package/package.json +7 -7
- package/dist/context-CjxK7bH9.d.ts +0 -17
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.
|
|
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
|
|
28
|
-
-
|
|
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.
|
|
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
|
|
35
|
-
- `.routes()` —
|
|
36
|
-
- `
|
|
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/
|
|
43
|
-
- **`@rhythmjs/router/adapters/
|
|
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.
|
package/dist/adapters/bun.d.ts
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 };
|
package/dist/adapters/bun.js
CHANGED
|
@@ -1,14 +1,2 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 };
|
package/dist/adapters/context.js
CHANGED
|
@@ -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 };
|
package/dist/adapters/deno.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { toFetchHandler } from "./
|
|
1
|
+
import { toFetchHandler } from "./web-std.js";
|
|
2
2
|
export { toFetchHandler };
|
package/dist/adapters/deno.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { toFetchHandler } from "./
|
|
1
|
+
import { toFetchHandler } from "./web-std.js";
|
|
2
2
|
export { toFetchHandler };
|
package/dist/adapters/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as RhythmHttpContext } from "../context-
|
|
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>;
|
package/dist/adapters/node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 };
|
package/dist/radix-tree.d.ts
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import { Middleware, NextFn } from "@rhythmjs/rhythm";
|
|
2
1
|
//#region src/radix-tree.d.ts
|
|
3
|
-
|
|
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,
|
|
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,
|
|
18
|
-
export declare function lookupRoute<TMethod extends string>(root: TreeNode<TMethod>, method: string, pathname: string): {
|
|
19
|
-
|
|
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;
|
package/dist/radix-tree.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
67
|
+
insertAt(child, path.slice(staticPart.length), method, payload);
|
|
72
68
|
}
|
|
73
|
-
function insertRoute(root, method, path,
|
|
74
|
-
insertAt(root, path, method,
|
|
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
|
|
109
|
-
if (
|
|
104
|
+
const payload = node.methods.get(method);
|
|
105
|
+
if (payload === void 0) return null;
|
|
110
106
|
return {
|
|
111
|
-
|
|
107
|
+
payload,
|
|
112
108
|
params
|
|
113
109
|
};
|
|
114
110
|
}
|
package/dist/rhythm-router.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { t as RhythmHttpContext } from "./context-
|
|
2
|
-
import {
|
|
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
|
|
10
|
+
export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext> {
|
|
12
11
|
#private;
|
|
13
12
|
constructor(options?: RhythmRouterOptions);
|
|
14
|
-
use
|
|
15
|
-
|
|
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;
|
package/dist/rhythm-router.js
CHANGED
|
@@ -1,71 +1,39 @@
|
|
|
1
1
|
import { createNode, insertRoute, joinPath, lookupRoute } from "./radix-tree.js";
|
|
2
|
-
import {
|
|
2
|
+
import { compose } from "@rhythmjs/rhythm/compose";
|
|
3
3
|
//#region src/rhythm-router.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
#prefix;
|
|
4
|
+
var RhythmRouter = class RhythmRouter {
|
|
5
|
+
#options;
|
|
7
6
|
#entries = [];
|
|
8
|
-
#
|
|
9
|
-
#dispatchInstalled = false;
|
|
7
|
+
#composed = null;
|
|
10
8
|
constructor(options = {}) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.#prefix = options.prefix ?? "";
|
|
9
|
+
this.#options = options;
|
|
10
|
+
}
|
|
11
|
+
get #prefix() {
|
|
12
|
+
return this.#options.prefix ?? "";
|
|
16
13
|
}
|
|
17
|
-
use(
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
-
|
|
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:
|
|
33
|
+
path: joinPath(this.#prefix, path),
|
|
49
34
|
handlers
|
|
50
35
|
});
|
|
51
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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.
|
|
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 };
|