@rindle/router 0.2.0 → 0.4.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/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +21 -0
- package/dist/bin.js.map +1 -0
- package/dist/bootstrap.d.ts +34 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +45 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/bin.ts +21 -0
- package/src/bootstrap.ts +79 -0
- package/src/index.ts +3 -0
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable entry for `@rindle/router`: start a read router from a rendered `.rindle/router.json`
|
|
3
|
+
// (the shape `rindle render` emits). `rindle up` supervises a routed topology's router through
|
|
4
|
+
// this; you can also run it directly:
|
|
5
|
+
//
|
|
6
|
+
// node --conditions=@rindle/source packages/router/src/bin.ts .rindle/router.json # from source
|
|
7
|
+
// rindle-router .rindle/router.json # installed
|
|
8
|
+
//
|
|
9
|
+
// Tokens/port can be overridden out-of-band via ROUTER_TOKEN / DAEMON_TOKEN / ROUTER_PORT so the
|
|
10
|
+
// rendered config stays secret-free (see `startRouterFromFile`).
|
|
11
|
+
import { startRouterFromFile } from "./bootstrap.js";
|
|
12
|
+
const configPath = process.argv[2] ?? ".rindle/router.json";
|
|
13
|
+
startRouterFromFile(configPath)
|
|
14
|
+
.then((server) => {
|
|
15
|
+
console.log(`[rindle-router] listening on :${server.port} (config ${configPath})`);
|
|
16
|
+
})
|
|
17
|
+
.catch((err) => {
|
|
18
|
+
console.error(`[rindle-router] failed to start from ${configPath}:`, err);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,iGAAiG;AACjG,+FAA+F;AAC/F,sCAAsC;AACtC,EAAE;AACF,oGAAoG;AACpG,kGAAkG;AAClG,EAAE;AACF,iGAAiG;AACjG,iEAAiE;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,qBAAqB,CAAC;AAC5D,mBAAmB,CAAC,UAAU,CAAC;KAC5B,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;IACf,OAAO,CAAC,GAAG,CAAC,iCAAiC,MAAM,CAAC,IAAI,YAAY,UAAU,GAAG,CAAC,CAAC;AACrF,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACtB,OAAO,CAAC,KAAK,CAAC,wCAAwC,UAAU,GAAG,EAAE,GAAG,CAAC,CAAC;IAC1E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type RouterServer } from "./http.ts";
|
|
2
|
+
import { type Follower } from "./registry.ts";
|
|
3
|
+
/** The on-disk router config — `@rindle/router`'s slice of a rendered Rindle topology. Matches the
|
|
4
|
+
* `RouterConfig` Nickel contract field-for-field (`rindle-cli/nickel/rindle.ncl`). */
|
|
5
|
+
export interface RouterConfig {
|
|
6
|
+
/** the router's own HTTP port (default 7650, matching the topology + README). */
|
|
7
|
+
port?: number;
|
|
8
|
+
/** inbound bearer the api-server must present (ROUTER_TOKEN); omit on a loopback dev bind. */
|
|
9
|
+
token?: string;
|
|
10
|
+
/** outbound bearer the router presents to each follower's control plane (DAEMON_TOKEN). */
|
|
11
|
+
daemonToken?: string;
|
|
12
|
+
/** the routable fleet — one entry per follower (`{ id, controlPlaneUrl, wsEndpoint, weight? }`). */
|
|
13
|
+
followers: Follower[];
|
|
14
|
+
}
|
|
15
|
+
/** Out-of-band overrides, so SECRETS need not live in the rendered config file: pass tokens/port
|
|
16
|
+
* from the environment and they win over the file's values. */
|
|
17
|
+
export interface RouterBootstrapOverrides {
|
|
18
|
+
/** inbound token (e.g. `process.env.ROUTER_TOKEN`); wins over `config.token`. */
|
|
19
|
+
token?: string;
|
|
20
|
+
/** outbound daemon token (e.g. `process.env.DAEMON_TOKEN`); wins over `config.daemonToken`. */
|
|
21
|
+
daemonToken?: string;
|
|
22
|
+
/** listen port (e.g. `process.env.ROUTER_PORT`); wins over `config.port`. */
|
|
23
|
+
port?: number;
|
|
24
|
+
}
|
|
25
|
+
/** Build and start a router from a parsed {@link RouterConfig}. Resolves once listening. */
|
|
26
|
+
export declare function createRouterServerFromConfig(config: RouterConfig, overrides?: RouterBootstrapOverrides): Promise<RouterServer>;
|
|
27
|
+
/** Read + validate a `.rindle/router.json` (or any `RouterConfig` JSON) from disk. */
|
|
28
|
+
export declare function loadRouterConfig(path: string): RouterConfig;
|
|
29
|
+
/** Load a rendered router config from disk and start the server, taking tokens + port from the
|
|
30
|
+
* environment (`ROUTER_TOKEN`, `DAEMON_TOKEN` / `RINDLE_DAEMON_TOKEN`, `ROUTER_PORT`) so the
|
|
31
|
+
* rendered file can stay secret-free. The one call a routed deploy's entrypoint needs:
|
|
32
|
+
* `startRouterFromFile(".rindle/router.json")`. */
|
|
33
|
+
export declare function startRouterFromFile(path: string, env?: Record<string, string | undefined>): Promise<RouterServer>;
|
|
34
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAQA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGtE;uFACuF;AACvF,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oGAAoG;IACpG,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED;gEACgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4FAA4F;AAC5F,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,YAAY,EACpB,SAAS,GAAE,wBAA6B,GACvC,OAAO,CAAC,YAAY,CAAC,CAcvB;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAM3D;AAED;;;oDAGoD;AACpD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,OAAO,CAAC,YAAY,CAAC,CAMvB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// bootstrap.ts — start a router from a plain config object (the `.rindle/router.json` shape that
|
|
2
|
+
// `rindle render` emits), so a routed topology can launch the router without hand-wiring
|
|
3
|
+
// `StaticFollowerRegistry` + `RoutingReadClient` + `createRouterServer` in app code. The registry's
|
|
4
|
+
// follower list is the only state; it is DERIVED from the topology, so it can't drift from the
|
|
5
|
+
// followers that actually exist (RINDLE-CLI Nickel topology, `rindle-cli/nickel/rindle.ncl` `RouterConfig`).
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { createRouterServer } from "./http.js";
|
|
8
|
+
import { StaticFollowerRegistry } from "./registry.js";
|
|
9
|
+
import { RoutingReadClient } from "./router.js";
|
|
10
|
+
/** Build and start a router from a parsed {@link RouterConfig}. Resolves once listening. */
|
|
11
|
+
export function createRouterServerFromConfig(config, overrides = {}) {
|
|
12
|
+
if (!Array.isArray(config.followers) || config.followers.length === 0) {
|
|
13
|
+
throw new Error("router config: `followers` must list at least one follower");
|
|
14
|
+
}
|
|
15
|
+
const registry = new StaticFollowerRegistry(config.followers);
|
|
16
|
+
const router = new RoutingReadClient({
|
|
17
|
+
registry,
|
|
18
|
+
daemonToken: overrides.daemonToken ?? config.daemonToken,
|
|
19
|
+
});
|
|
20
|
+
return createRouterServer({
|
|
21
|
+
router,
|
|
22
|
+
token: overrides.token ?? config.token,
|
|
23
|
+
port: overrides.port ?? config.port ?? 7650,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Read + validate a `.rindle/router.json` (or any `RouterConfig` JSON) from disk. */
|
|
27
|
+
export function loadRouterConfig(path) {
|
|
28
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
+
if (config === null || typeof config !== "object" || !Array.isArray(config.followers)) {
|
|
30
|
+
throw new Error(`router config ${path}: expected an object with a "followers" array`);
|
|
31
|
+
}
|
|
32
|
+
return config;
|
|
33
|
+
}
|
|
34
|
+
/** Load a rendered router config from disk and start the server, taking tokens + port from the
|
|
35
|
+
* environment (`ROUTER_TOKEN`, `DAEMON_TOKEN` / `RINDLE_DAEMON_TOKEN`, `ROUTER_PORT`) so the
|
|
36
|
+
* rendered file can stay secret-free. The one call a routed deploy's entrypoint needs:
|
|
37
|
+
* `startRouterFromFile(".rindle/router.json")`. */
|
|
38
|
+
export function startRouterFromFile(path, env = process.env) {
|
|
39
|
+
return createRouterServerFromConfig(loadRouterConfig(path), {
|
|
40
|
+
token: env.ROUTER_TOKEN,
|
|
41
|
+
daemonToken: env.DAEMON_TOKEN ?? env.RINDLE_DAEMON_TOKEN,
|
|
42
|
+
port: env.ROUTER_PORT ? Number(env.ROUTER_PORT) : undefined,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,yFAAyF;AACzF,oGAAoG;AACpG,+FAA+F;AAC/F,6GAA6G;AAE7G,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAqB,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAiB,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA0BhD,4FAA4F;AAC5F,MAAM,UAAU,4BAA4B,CAC1C,MAAoB,EACpB,YAAsC,EAAE;IAExC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,sBAAsB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC;QACnC,QAAQ;QACR,WAAW,EAAE,SAAS,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW;KACzD,CAAC,CAAC;IACH,OAAO,kBAAkB,CAAC;QACxB,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK;QACtC,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI;KAC5C,CAAC,CAAC;AACL,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;IACtE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACtF,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,+CAA+C,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;oDAGoD;AACpD,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,MAA0C,OAAO,CAAC,GAAG;IAErD,OAAO,4BAA4B,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE;QAC1D,KAAK,EAAE,GAAG,CAAC,YAAY;QACvB,WAAW,EAAE,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,mBAAmB;QACxD,IAAI,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS;KAC5D,CAAC,CAAC;AACL,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ export { NoLiveFollowersError, RoutingReadClient } from "./router.ts";
|
|
|
6
6
|
export type { FollowerClientFactory, RoutingReadClientOptions } from "./router.ts";
|
|
7
7
|
export { ROUTER_PATHS, createRouterPinClient, createRouterServer, dispatchRouter } from "./http.ts";
|
|
8
8
|
export type { RouterPinClient, RouterPinClientOptions, RouterServer, RouterServerOptions } from "./http.ts";
|
|
9
|
+
export { createRouterServerFromConfig, loadRouterConfig, startRouterFromFile } from "./bootstrap.ts";
|
|
10
|
+
export type { RouterConfig, RouterBootstrapOverrides } from "./bootstrap.ts";
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,YAAY,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAEnF,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,YAAY,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAEnF,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAE5G,OAAO,EAAE,4BAA4B,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrG,YAAY,EAAE,YAAY,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,4 +6,5 @@ export { hashUnit, route } from "./hrw.js";
|
|
|
6
6
|
export { StaticFollowerRegistry } from "./registry.js";
|
|
7
7
|
export { NoLiveFollowersError, RoutingReadClient } from "./router.js";
|
|
8
8
|
export { ROUTER_PATHS, createRouterPinClient, createRouterServer, dispatchRouter } from "./http.js";
|
|
9
|
+
export { createRouterServerFromConfig, loadRouterConfig, startRouterFromFile } from "./bootstrap.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,6FAA6F;AAC7F,iGAAiG;AACjG,gGAAgG;AAEhG,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAG3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGvD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,6FAA6F;AAC7F,iGAAiG;AACjG,gGAAgG;AAEhG,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAG3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGvD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGpG,OAAO,EAAE,4BAA4B,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rindle/router",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"description": "Stateless read-routing control-plane proxy: HRW follower placement + explicit fleet pin fan-out (READ-ROUTER-DESIGN.md).",
|
|
12
12
|
"main": "./dist/index.js",
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"rindle-router": "./dist/bin.js"
|
|
16
|
+
},
|
|
14
17
|
"sideEffects": false,
|
|
15
18
|
"exports": {
|
|
16
19
|
".": {
|
|
@@ -25,7 +28,7 @@
|
|
|
25
28
|
"README.md"
|
|
26
29
|
],
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"@rindle/daemon-client": "0.2
|
|
31
|
+
"@rindle/daemon-client": "0.4.2"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@types/node": "^22.10.0",
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable entry for `@rindle/router`: start a read router from a rendered `.rindle/router.json`
|
|
3
|
+
// (the shape `rindle render` emits). `rindle up` supervises a routed topology's router through
|
|
4
|
+
// this; you can also run it directly:
|
|
5
|
+
//
|
|
6
|
+
// node --conditions=@rindle/source packages/router/src/bin.ts .rindle/router.json # from source
|
|
7
|
+
// rindle-router .rindle/router.json # installed
|
|
8
|
+
//
|
|
9
|
+
// Tokens/port can be overridden out-of-band via ROUTER_TOKEN / DAEMON_TOKEN / ROUTER_PORT so the
|
|
10
|
+
// rendered config stays secret-free (see `startRouterFromFile`).
|
|
11
|
+
import { startRouterFromFile } from "./bootstrap.ts";
|
|
12
|
+
|
|
13
|
+
const configPath = process.argv[2] ?? ".rindle/router.json";
|
|
14
|
+
startRouterFromFile(configPath)
|
|
15
|
+
.then((server) => {
|
|
16
|
+
console.log(`[rindle-router] listening on :${server.port} (config ${configPath})`);
|
|
17
|
+
})
|
|
18
|
+
.catch((err: unknown) => {
|
|
19
|
+
console.error(`[rindle-router] failed to start from ${configPath}:`, err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// bootstrap.ts — start a router from a plain config object (the `.rindle/router.json` shape that
|
|
2
|
+
// `rindle render` emits), so a routed topology can launch the router without hand-wiring
|
|
3
|
+
// `StaticFollowerRegistry` + `RoutingReadClient` + `createRouterServer` in app code. The registry's
|
|
4
|
+
// follower list is the only state; it is DERIVED from the topology, so it can't drift from the
|
|
5
|
+
// followers that actually exist (RINDLE-CLI Nickel topology, `rindle-cli/nickel/rindle.ncl` `RouterConfig`).
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
import { createRouterServer, type RouterServer } from "./http.ts";
|
|
10
|
+
import { StaticFollowerRegistry, type Follower } from "./registry.ts";
|
|
11
|
+
import { RoutingReadClient } from "./router.ts";
|
|
12
|
+
|
|
13
|
+
/** The on-disk router config — `@rindle/router`'s slice of a rendered Rindle topology. Matches the
|
|
14
|
+
* `RouterConfig` Nickel contract field-for-field (`rindle-cli/nickel/rindle.ncl`). */
|
|
15
|
+
export interface RouterConfig {
|
|
16
|
+
/** the router's own HTTP port (default 7650, matching the topology + README). */
|
|
17
|
+
port?: number;
|
|
18
|
+
/** inbound bearer the api-server must present (ROUTER_TOKEN); omit on a loopback dev bind. */
|
|
19
|
+
token?: string;
|
|
20
|
+
/** outbound bearer the router presents to each follower's control plane (DAEMON_TOKEN). */
|
|
21
|
+
daemonToken?: string;
|
|
22
|
+
/** the routable fleet — one entry per follower (`{ id, controlPlaneUrl, wsEndpoint, weight? }`). */
|
|
23
|
+
followers: Follower[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Out-of-band overrides, so SECRETS need not live in the rendered config file: pass tokens/port
|
|
27
|
+
* from the environment and they win over the file's values. */
|
|
28
|
+
export interface RouterBootstrapOverrides {
|
|
29
|
+
/** inbound token (e.g. `process.env.ROUTER_TOKEN`); wins over `config.token`. */
|
|
30
|
+
token?: string;
|
|
31
|
+
/** outbound daemon token (e.g. `process.env.DAEMON_TOKEN`); wins over `config.daemonToken`. */
|
|
32
|
+
daemonToken?: string;
|
|
33
|
+
/** listen port (e.g. `process.env.ROUTER_PORT`); wins over `config.port`. */
|
|
34
|
+
port?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build and start a router from a parsed {@link RouterConfig}. Resolves once listening. */
|
|
38
|
+
export function createRouterServerFromConfig(
|
|
39
|
+
config: RouterConfig,
|
|
40
|
+
overrides: RouterBootstrapOverrides = {},
|
|
41
|
+
): Promise<RouterServer> {
|
|
42
|
+
if (!Array.isArray(config.followers) || config.followers.length === 0) {
|
|
43
|
+
throw new Error("router config: `followers` must list at least one follower");
|
|
44
|
+
}
|
|
45
|
+
const registry = new StaticFollowerRegistry(config.followers);
|
|
46
|
+
const router = new RoutingReadClient({
|
|
47
|
+
registry,
|
|
48
|
+
daemonToken: overrides.daemonToken ?? config.daemonToken,
|
|
49
|
+
});
|
|
50
|
+
return createRouterServer({
|
|
51
|
+
router,
|
|
52
|
+
token: overrides.token ?? config.token,
|
|
53
|
+
port: overrides.port ?? config.port ?? 7650,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read + validate a `.rindle/router.json` (or any `RouterConfig` JSON) from disk. */
|
|
58
|
+
export function loadRouterConfig(path: string): RouterConfig {
|
|
59
|
+
const config = JSON.parse(readFileSync(path, "utf8")) as RouterConfig;
|
|
60
|
+
if (config === null || typeof config !== "object" || !Array.isArray(config.followers)) {
|
|
61
|
+
throw new Error(`router config ${path}: expected an object with a "followers" array`);
|
|
62
|
+
}
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Load a rendered router config from disk and start the server, taking tokens + port from the
|
|
67
|
+
* environment (`ROUTER_TOKEN`, `DAEMON_TOKEN` / `RINDLE_DAEMON_TOKEN`, `ROUTER_PORT`) so the
|
|
68
|
+
* rendered file can stay secret-free. The one call a routed deploy's entrypoint needs:
|
|
69
|
+
* `startRouterFromFile(".rindle/router.json")`. */
|
|
70
|
+
export function startRouterFromFile(
|
|
71
|
+
path: string,
|
|
72
|
+
env: Record<string, string | undefined> = process.env,
|
|
73
|
+
): Promise<RouterServer> {
|
|
74
|
+
return createRouterServerFromConfig(loadRouterConfig(path), {
|
|
75
|
+
token: env.ROUTER_TOKEN,
|
|
76
|
+
daemonToken: env.DAEMON_TOKEN ?? env.RINDLE_DAEMON_TOKEN,
|
|
77
|
+
port: env.ROUTER_PORT ? Number(env.ROUTER_PORT) : undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,3 +14,6 @@ export type { FollowerClientFactory, RoutingReadClientOptions } from "./router.t
|
|
|
14
14
|
|
|
15
15
|
export { ROUTER_PATHS, createRouterPinClient, createRouterServer, dispatchRouter } from "./http.ts";
|
|
16
16
|
export type { RouterPinClient, RouterPinClientOptions, RouterServer, RouterServerOptions } from "./http.ts";
|
|
17
|
+
|
|
18
|
+
export { createRouterServerFromConfig, loadRouterConfig, startRouterFromFile } from "./bootstrap.ts";
|
|
19
|
+
export type { RouterConfig, RouterBootstrapOverrides } from "./bootstrap.ts";
|