Router
route and the Router/Membership/hashing internals behind cache-affinity routing, hedging, and failover.
import { route } from "lakefront/router";
The cluster front door: deterministic cache-affinity routing, hedging, and failover across a fixed set of server nodes.
Functions
route
function route(options: RouteOptions): Promise<RouterServer>;
| Parameter | Type | Default | Description |
|---|---|---|---|
options |
RouteOptions |
(required) | See RouteOptions. |
Returns the running RouterServer. Probes membership once, starts the health-poll loop, and opens the HTTP front door.
const router = await route({
nodes: [
{ id: "node-a", url: "http://127.0.0.1:58001" },
{ id: "node-b", url: "http://127.0.0.1:58002" },
{ id: "node-c", url: "http://127.0.0.1:58003" },
],
port: 8080,
});
quantiseWeight
function quantiseWeight(raw: number): number;
Snaps a raw capacity number to the nearest WEIGHT_LEVELS band (<= 0 maps to 0). Keeping weight banded stops routing from oscillating on every small capacity change.
rank
function rank(nodes: readonly NodeInfo[], boardId: string): NodeInfo[];
All eligible (weight > 0) nodes for boardId, best first; ties broken by id lexical order. The head is the board’s native node, the rest are failover order.
score
function score(node: NodeInfo, boardId: string): number;
-weight / ln(u), where u = hash(node, board) is normalised to (0,1) via 64-bit wyhash: gives each node a selection probability exactly proportional to its weight. weight <= 0 scores -Infinity.
isIdempotentPath
function isIdempotentPath(path: string): boolean;
Whether path names a procedure in IDEMPOTENT_PROCEDURES. Re-exported from lakefront/contract, where it is defined, for one import path.
isRetryable
function isRetryable(path: string, headers?: Headers): boolean;
isIdempotentPath(path), or headers carrying IDEMPOTENT_HEADER set to "1". Re-exported from lakefront/contract.
Classes
Router
Router is the class route wires up; construct it directly to embed routing in your own HTTP layer.
const router = new Router(options: RouterOptions);
See RouterOptions.
route
route(boardId: string): NodeInfo[]
Deterministic node preference for a board, best first: every router instance computes the same ranking from the same membership, with no coordination.
forward
forward(
boardId: string,
path: string,
body: string,
retryable?: boolean,
authorization?: string,
): Promise<ForwardResult>
| Parameter | Type | Default | Description |
|---|---|---|---|
boardId |
string |
(required) | Board to route. |
path |
string |
(required) | Request path, e.g. /rpc/query.execute. |
body |
string |
(required) | Raw request body to forward. |
retryable |
boolean |
isIdempotentPath(path) |
Whether a failure may hedge and fail over to other nodes. |
authorization |
string |
undefined |
Forwarded verbatim, never parsed. |
Sends to the board’s native node, hedging to the next-ranked node if the first has not answered by its own p95 (or hedgeDelayMs, before it has 8 samples). A non-retryable call (a bare mutation, no idempotencyKey) gets exactly one attempt on exactly one node: no hedge, no failover. Hedging or retrying a write that may have already landed risks a second copy that nothing downstream could undo, since the lake is append-only; a keyed mutation deduplicates at the WAL and can be hedged and retried on another node. Design and rationale are covered under routing.
counters
readonly counters: { requests: number; hedges: number; hedgeWins: number; failovers: number };
Running totals, mutated as forward runs.
Membership
const membership = new Membership(options: MembershipOptions);
See MembershipOptions.
nodes
nodes(): NodeInfo[]
Every configured member with its current weight: unhealthy members carry weight 0, which removes them from ranking without renumbering anything else.
all
all(): readonly MemberConfig[]
Every configured member, healthy or not.
healthyIds
healthyIds(): string[]
Ids currently marked healthy.
probeOnce
probeOnce(): Promise<void>
Hits GET /health on every member once, in parallel, with timeoutMs.
start
start(): void
Starts the intervalMs poll loop.
stop
stop(): void
Stops the poll loop.
setCapacity
setCapacity(id: string, capacity: number): void
Feeds a capacity signal in, quantised at nodes() time. Kept separate from health so a slow node is de-preferred, not removed.
markUnhealthy
markUnhealthy(id: string): void
Marks a member unhealthy immediately, outside the poll cadence.
LatencyWindow
Per-node latency tracking used to set the hedge delay: hedging at a node’s own recent p95 keeps roughly 5% of requests hedged in steady state rather than over- or under-hedging on a fixed timeout.
const window = new LatencyWindow();
record
record(nodeId: string, ms: number): void
Appends a sample to a 128-entry sliding window per node.
percentile
percentile(nodeId: string, p: number): number | undefined
undefined until the node has 8 or more samples: callers fall back to a configured default rather than hedging on noise.
count
count(nodeId: string): number
Samples currently held for a node.
Types
NodeInfo
interface NodeInfo {
readonly id: string;
readonly url: string;
readonly weight: number; // 0 excludes the node entirely
}
RouteOptions
| Field | Type | Default | Notes |
|---|---|---|---|
nodes |
readonly MemberConfig[] |
(required) | { id, url } per server node. |
port |
number |
0 |
0 asks the OS to pick a free port. |
hedgeDelayMs |
number |
250 |
Fallback hedge delay before a node has 8+ latency samples of its own. |
healthIntervalMs |
number |
1000 |
Health-poll interval: forwarded to Membership. |
RouterServer
| Field | Type |
|---|---|
port |
number |
url |
string |
router |
Router |
membership |
Membership |
stop |
(): Promise<void>: stops the health-poll loop, then the HTTP server. |
RouterOptions
interface RouterOptions {
readonly membership: Membership;
readonly hedgeDelayMs?: number; // default 250
readonly hedgeAfterPercentile?: number; // default 95
readonly requestTimeoutMs?: number; // default 10_000
readonly fetch?: typeof globalThis.fetch;
}
ForwardResult
interface ForwardResult {
readonly status: number;
readonly body: string;
readonly servedBy: string;
readonly hedged: boolean;
readonly attempted: readonly string[];
readonly latencyMs: number;
}
MemberConfig
interface MemberConfig {
readonly id: string;
readonly url: string;
}
MembershipOptions
interface MembershipOptions {
readonly members: readonly MemberConfig[];
readonly intervalMs?: number; // default 1000
readonly timeoutMs?: number; // default 500
readonly fetch?: typeof globalThis.fetch;
}
Constants
WEIGHT_LEVELS
[0.25, 0.5, 1, 1.5, 2]. Quantisation bands quantiseWeight snaps to.
IDEMPOTENT_HEADER
"x-lakefront-idempotent". Re-exported from lakefront/contract, where it is defined.
IDEMPOTENT_PROCEDURES
ReadonlySet<string> of {"query.execute", "node.stats", "changes.read"}. Re-exported from lakefront/contract, where it is defined.
HTTP surface
| Route | Method | Response |
|---|---|---|
/health |
GET |
{ ok: true, healthy: string[] }: currently healthy member ids. |
/route?board=<id> |
GET |
{ board, ranking: { id, weight }[], healthy: string[] }: introspection of the rendezvous ranking for a board, without forwarding anything. |
/stats |
GET |
{ requests, hedges, hedgeWins, failovers, healthy }: the router’s counters plus currently healthy member ids. |
/rpc/* |
POST |
Proxied to the board’s ranked node(s). Requires header x-lakefront-board, 401 without it. Responds with x-lakefront-served-by and x-lakefront-hedged; see the wire protocol reference. |
| anything else | any | 404, { "error": "not found" }. |
Standalone entrypoint
The container entrypoint (router/main.ts) reads its configuration from the environment: LAKEFRONT_NODES, LAKEFRONT_HEDGE_MS, PORT. See the environment variable reference. route itself takes options directly and reads no env.