@rhythmjs/router 0.0.1 → 0.0.2

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
 
@@ -24,16 +26,17 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
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.
26
28
  - **`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()`.
29
+ - **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
+ - **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()`.
31
+ - **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
32
 
30
33
  ## API
31
34
 
32
- - `new RhythmRouter(options?)` — `options.name`, `options.prefix`.
35
+ - `new RhythmRouter(options?)` — `options.prefix`.
33
36
  - `.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.
37
+ - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmRouter` (prefixes compose).
38
+ - `.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.
39
+ - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
37
40
 
38
41
  ## Runtime adapters
39
42
 
@@ -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,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
1
  import { t as RhythmHttpContext } from "./context-CjxK7bH9.js";
2
- import { DeepReadonly, Middleware, OmitHashKeys, Rhythm } from "@rhythmjs/rhythm";
2
+ import { Middleware } from "@rhythmjs/rhythm";
3
3
  //#region src/rhythm-router.d.ts
4
4
  export interface RhythmRouterContext {
5
5
  params: 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";
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.2",
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",
@@ -44,7 +45,7 @@
44
45
  "access": "public"
45
46
  },
46
47
  "dependencies": {
47
- "@rhythmjs/rhythm": "0.0.1"
48
+ "@rhythmjs/rhythm": "0.0.2"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/node": "^26.6.3",
@@ -53,10 +54,5 @@
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=20.19.0"
56
- },
57
- "scripts": {
58
- "build": "vp pack",
59
- "typecheck": "tsc --noEmit",
60
- "test": "vp test"
61
57
  }
62
58
  }