@absolutejs/router 0.2.0 → 0.3.0
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/index.d.ts +12 -0
- package/dist/index.js +84 -7
- package/dist/index.js.map +5 -4
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* The caller wires the routing decision into whichever HTTP/WS layer they
|
|
14
14
|
* have. An Elysia adapter ships in a later 0.0.x as a subpath.
|
|
15
15
|
*/
|
|
16
|
+
import { type TracerProvider } from '@absolutejs/telemetry';
|
|
16
17
|
export type Shard = {
|
|
17
18
|
id: string;
|
|
18
19
|
/**
|
|
@@ -92,6 +93,17 @@ export type RouterOptions = {
|
|
|
92
93
|
allow?: AllowHook;
|
|
93
94
|
/** Override `Date.now` for tests. */
|
|
94
95
|
clock?: () => number;
|
|
96
|
+
/**
|
|
97
|
+
* Optional OpenTelemetry tracer provider. When set, `route()` and
|
|
98
|
+
* `acquire()` are wrapped in `router.route` / `router.acquire`
|
|
99
|
+
* spans with `abs.tenant`, `abs.route.decision`, and (on allow)
|
|
100
|
+
* `abs.route.shard` attributes. When omitted, all tracing is a
|
|
101
|
+
* zero-allocation noop. Added in 0.3.0.
|
|
102
|
+
*
|
|
103
|
+
* Structural type via `@absolutejs/telemetry`; no peer-dep on
|
|
104
|
+
* `@opentelemetry/api`.
|
|
105
|
+
*/
|
|
106
|
+
tracerProvider?: TracerProvider;
|
|
95
107
|
};
|
|
96
108
|
export type RouteDecision = 'allow' | 'rate-limited' | 'capped' | 'no-shards' | 'denied';
|
|
97
109
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,57 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// node_modules/@absolutejs/telemetry/dist/index.js
|
|
3
|
+
var NOOP_SPAN_CONTEXT = {
|
|
4
|
+
spanId: "0000000000000000",
|
|
5
|
+
traceFlags: 0,
|
|
6
|
+
traceId: "00000000000000000000000000000000"
|
|
7
|
+
};
|
|
8
|
+
var noopSpan = {
|
|
9
|
+
addEvent: () => noopSpan,
|
|
10
|
+
end: () => {},
|
|
11
|
+
isRecording: () => false,
|
|
12
|
+
recordException: () => {},
|
|
13
|
+
setAttribute: () => noopSpan,
|
|
14
|
+
setAttributes: () => noopSpan,
|
|
15
|
+
setStatus: () => noopSpan,
|
|
16
|
+
spanContext: () => NOOP_SPAN_CONTEXT,
|
|
17
|
+
updateName: () => noopSpan
|
|
18
|
+
};
|
|
19
|
+
var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
|
|
20
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
21
|
+
return fn(noopSpan);
|
|
22
|
+
};
|
|
23
|
+
var noopTracer = {
|
|
24
|
+
startActiveSpan: startActiveSpanNoop,
|
|
25
|
+
startSpan: () => noopSpan
|
|
26
|
+
};
|
|
27
|
+
var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
|
|
28
|
+
var ABS_ATTRS = {
|
|
29
|
+
tenant: "abs.tenant",
|
|
30
|
+
shardId: "abs.shard.id",
|
|
31
|
+
engineId: "abs.engine.id",
|
|
32
|
+
collection: "abs.collection",
|
|
33
|
+
mutation: "abs.mutation",
|
|
34
|
+
mutationAttempt: "abs.mutation.attempt",
|
|
35
|
+
subscriptionId: "abs.subscription.id",
|
|
36
|
+
batchSize: "abs.batch.size",
|
|
37
|
+
clusterMessageOrigin: "abs.cluster.origin",
|
|
38
|
+
jobId: "abs.job.id",
|
|
39
|
+
jobKind: "abs.job.kind",
|
|
40
|
+
jobAttempt: "abs.job.attempt",
|
|
41
|
+
jobMaxAttempts: "abs.job.max_attempts",
|
|
42
|
+
workerId: "abs.worker.id",
|
|
43
|
+
runtimeKey: "abs.runtime.key",
|
|
44
|
+
runtimePid: "abs.runtime.pid",
|
|
45
|
+
runtimePort: "abs.runtime.port",
|
|
46
|
+
runtimeExitReason: "abs.runtime.exit_reason",
|
|
47
|
+
runtimeReadinessMs: "abs.runtime.readiness_ms",
|
|
48
|
+
routeShard: "abs.route.shard",
|
|
49
|
+
routeDecision: "abs.route.decision",
|
|
50
|
+
secretName: "abs.secret.name",
|
|
51
|
+
secretFingerprint: "abs.secret.fingerprint",
|
|
52
|
+
auditKind: "abs.audit.kind"
|
|
53
|
+
};
|
|
54
|
+
|
|
2
55
|
// src/index.ts
|
|
3
56
|
var fnv1a32 = (input) => {
|
|
4
57
|
let hash = 2166136261;
|
|
@@ -65,6 +118,7 @@ var createRouter = (options) => {
|
|
|
65
118
|
const tenants = new Map;
|
|
66
119
|
const routeBuckets = new Map;
|
|
67
120
|
let disposed = false;
|
|
121
|
+
const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/router");
|
|
68
122
|
let totalRoutes = 0;
|
|
69
123
|
let totalAcquires = 0;
|
|
70
124
|
let lastRouteMs = 0;
|
|
@@ -108,12 +162,29 @@ var createRouter = (options) => {
|
|
|
108
162
|
};
|
|
109
163
|
const eligibleShards = () => shardList.filter((shard) => healthy.get(shard.id) === true && !draining.has(shard.id));
|
|
110
164
|
const route = (request) => {
|
|
165
|
+
const span = tracer.startSpan("router.route", {
|
|
166
|
+
attributes: {
|
|
167
|
+
[ABS_ATTRS.tenant]: request.tenantId,
|
|
168
|
+
...request.route !== undefined ? { "abs.route.name": request.route } : {}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
111
171
|
const routeStart = clock();
|
|
112
172
|
totalRoutes += 1;
|
|
173
|
+
const finishSpan = (result) => {
|
|
174
|
+
span.setAttribute(ABS_ATTRS.routeDecision, result.decision);
|
|
175
|
+
if (result.shard !== null) {
|
|
176
|
+
span.setAttribute(ABS_ATTRS.routeShard, result.shard.id);
|
|
177
|
+
}
|
|
178
|
+
span.setStatus({
|
|
179
|
+
code: result.decision === "allow" ? 1 : 2
|
|
180
|
+
});
|
|
181
|
+
span.end();
|
|
182
|
+
return result;
|
|
183
|
+
};
|
|
113
184
|
const recordRejection = (decision) => {
|
|
114
185
|
rejectsByDecision[decision] += 1;
|
|
115
186
|
lastRouteMs = clock() - routeStart;
|
|
116
|
-
return { decision, shard: null };
|
|
187
|
+
return finishSpan({ decision, shard: null });
|
|
117
188
|
};
|
|
118
189
|
if (disposed)
|
|
119
190
|
return recordRejection("no-shards");
|
|
@@ -132,11 +203,11 @@ var createRouter = (options) => {
|
|
|
132
203
|
if (state.bucket.tokens < 1) {
|
|
133
204
|
rejectsByDecision["rate-limited"] += 1;
|
|
134
205
|
lastRouteMs = clock() - routeStart;
|
|
135
|
-
return {
|
|
206
|
+
return finishSpan({
|
|
136
207
|
decision: "rate-limited",
|
|
137
208
|
emptiedBucket: "tenant",
|
|
138
209
|
shard: null
|
|
139
|
-
};
|
|
210
|
+
});
|
|
140
211
|
}
|
|
141
212
|
let routeBucket = null;
|
|
142
213
|
let routeRule = null;
|
|
@@ -147,11 +218,11 @@ var createRouter = (options) => {
|
|
|
147
218
|
if (routeBucket.tokens < 1) {
|
|
148
219
|
rejectsByDecision["rate-limited"] += 1;
|
|
149
220
|
lastRouteMs = clock() - routeStart;
|
|
150
|
-
return {
|
|
221
|
+
return finishSpan({
|
|
151
222
|
decision: "rate-limited",
|
|
152
223
|
emptiedBucket: request.route,
|
|
153
224
|
shard: null
|
|
154
|
-
};
|
|
225
|
+
});
|
|
155
226
|
}
|
|
156
227
|
}
|
|
157
228
|
state.bucket.tokens -= 1;
|
|
@@ -162,13 +233,19 @@ var createRouter = (options) => {
|
|
|
162
233
|
const chosen = live[Math.max(0, Math.min(index, live.length - 1))];
|
|
163
234
|
shardLoadByShard.set(chosen.id, (shardLoadByShard.get(chosen.id) ?? 0) + 1);
|
|
164
235
|
lastRouteMs = clock() - routeStart;
|
|
165
|
-
return { decision: "allow", shard: chosen };
|
|
236
|
+
return finishSpan({ decision: "allow", shard: chosen });
|
|
166
237
|
};
|
|
167
238
|
const acquire = (tenantId) => {
|
|
239
|
+
const span = tracer.startSpan("router.acquire", {
|
|
240
|
+
attributes: { [ABS_ATTRS.tenant]: tenantId }
|
|
241
|
+
});
|
|
168
242
|
const now = clock();
|
|
169
243
|
const state = ensureTenant(tenantId, now);
|
|
170
244
|
state.active += 1;
|
|
171
245
|
totalAcquires += 1;
|
|
246
|
+
span.setAttribute("abs.tenant.active", state.active);
|
|
247
|
+
span.setStatus({ code: 1 });
|
|
248
|
+
span.end();
|
|
172
249
|
let released = false;
|
|
173
250
|
return {
|
|
174
251
|
active: state.active,
|
|
@@ -297,5 +374,5 @@ export {
|
|
|
297
374
|
createRouter
|
|
298
375
|
};
|
|
299
376
|
|
|
300
|
-
//# debugId=
|
|
377
|
+
//# debugId=E0A5EDB21819D2C764756E2164756E21
|
|
301
378
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts"],
|
|
3
|
+
"sources": ["../node_modules/@absolutejs/telemetry/dist/index.js", "../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @absolutejs/router — multi-tenant connection routing primitive for Bun PaaS\n * gateways.\n *\n * Sits between an incoming request / WS upgrade and N backend processes (each\n * one a `@absolutejs/runtime` instance hosting a `@absolutejs/sync` engine\n * for a subset of tenants). Decides which shard owns the tenant, whether the\n * tenant is over its rate limit (tenant-wide + per-route), whether the tenant\n * is over its connection cap, whether the caller-supplied allow hook approves,\n * and whether the chosen shard is healthy and not draining.\n *\n * v0.1.0 is bun/elysia-agnostic — pure logic, no `Bun.serve` or proxy code.\n * The caller wires the routing decision into whichever HTTP/WS layer they\n * have. An Elysia adapter ships in a later 0.0.x as a subpath.\n */\n\nexport type Shard = {\n\tid: string;\n\t/**\n\t * Connection target for the chosen backend. Format is opaque to the router\n\t * — `ws://host:port`, `https://host`, a unix socket path, whatever the\n\t * caller's proxy layer understands.\n\t */\n\turl: string;\n\t/**\n\t * Relative weight for hash strategies that support it (rendezvous). Jump\n\t * hash ignores weights — every shard is treated as weight 1. Default 1.\n\t */\n\tweight?: number;\n};\n\n/**\n * Pure hash-strategy function. Given the routing key + the list of currently\n * eligible shards (healthy AND not draining), return the index in `shards` of\n * the chosen shard. The router never calls a strategy with an empty `shards`\n * array — that case is handled upstream as `no-shards`.\n */\nexport type HashStrategyFn = (key: string, shards: ReadonlyArray<Shard>) => number;\n\nexport type HashStrategy = 'jump' | 'rendezvous' | HashStrategyFn;\n\n/**\n * Per-tenant token-bucket rate limit. `tokens` is the bucket capacity AND the\n * starting balance; `refillPerSecond` is added continuously up to capacity.\n * Defaults: `Infinity` tokens / `0` refill = no limit.\n */\nexport type RateLimit = {\n\ttokens: number;\n\trefillPerSecond: number;\n};\n\n/**\n * Optional caller-supplied gate. Called per-route with the tenant id; returning\n * `false` causes the route to return `decision: 'denied'`. The intended\n * use is `meter.allow` from `@absolutejs/metering` — pass it directly to refuse\n * routes for over-quota tenants without wiring the integration manually.\n */\nexport type AllowHook = (tenantId: string) => boolean;\n\n/**\n * Optional caller-supplied load metric. Called per `route()` with a shard id;\n * returning a value > 1 makes the rendezvous strategy bias AWAY from this shard\n * (effective weight = `shard.weight / load`). Used to avoid hot-spotting when\n * a stickiness-locked shard is overloaded — the router can't move existing\n * tenants, but it can avoid sending NEW tenants there.\n *\n * Jump-hash ignores this — it has no per-call weight bias by design.\n */\nexport type ShardLoadFn = (shardId: string) => number;\n\nexport type RouterOptions = {\n\t/** Initial shard set. Can be empty (every `route()` returns `no-shards`). */\n\tshards: Shard[];\n\t/** Hash strategy. Default `'jump'`. */\n\thashStrategy?: HashStrategy;\n\t/**\n\t * Max concurrent connections per tenant (counted via `acquire()` /\n\t * `release()`). Default `Infinity` (no cap). When reached, `route()`\n\t * returns `'capped'`.\n\t */\n\tperTenantConnectionCap?: number;\n\t/**\n\t * Per-tenant token bucket. Default `{ tokens: Infinity, refillPerSecond: 0 }`\n\t * (no limit). When the tenant's bucket is empty, `route()` returns\n\t * `'rate-limited'`. One `route()` call costs one token.\n\t */\n\tperTenantRateLimit?: RateLimit;\n\t/**\n\t * Per-route token buckets layered on top of the tenant-wide bucket.\n\t * Keyed by route name; supplied by the caller via\n\t * `route({ route: 'someRoute' })`. The tenant bucket AND the route bucket\n\t * must both have a token available. When the route bucket is empty,\n\t * `route()` returns `'rate-limited'` with `routeId` set.\n\t */\n\tperRouteRateLimits?: Record<string, RateLimit>;\n\t/** Optional load hook biasing the rendezvous strategy. */\n\tload?: ShardLoadFn;\n\t/** Optional caller-supplied allow gate. */\n\tallow?: AllowHook;\n\t/** Override `Date.now` for tests. */\n\tclock?: () => number;\n};\n\nexport type RouteDecision =\n\t| 'allow'\n\t| 'rate-limited'\n\t| 'capped'\n\t| 'no-shards'\n\t| 'denied';\n\n/**\n * Returned by {@link Router.metrics}. Combines point-in-time state with\n * cumulative counters since `createRouter()`. Survives `dispose()` so\n * post-shutdown introspection still reads totals. Added in 0.2.0.\n *\n * - `routes` — total `route()` calls (any decision).\n * - `acquires` — total `acquire()` calls.\n * - `rejectsByDecision` — counts per non-allow `RouteDecision`. The\n * operator's \"where am I shedding load?\" answer.\n * - `shardLoadDistribution` — cumulative routes assigned per shard\n * since `createRouter()`. With `markHealthy`/`drainShard` this is\n * the \"is rebalancing actually rebalancing?\" signal. (Per-shard\n * *active* connection count would require route-to-acquire\n * correlation that the current `acquire(tenantId)` API doesn't\n * capture; cumulative routes are the directly-observable proxy.)\n * - `lastRouteMs` — wall-clock of the most recent `route()` call (a\n * climb means the hot path is getting slower — usually a sign that\n * `load:` is doing too much work, or the shard count exploded).\n */\nexport type RouterMetrics = {\n\troutes: number;\n\tacquires: number;\n\trejectsByDecision: Record<Exclude<RouteDecision, 'allow'>, number>;\n\tshardLoadDistribution: Record<string, number>;\n\tlastRouteMs: number;\n};\n\nexport type RouteRequest = {\n\ttenantId: string;\n\t/**\n\t * Optional sub-key within a tenant for shardable channels. When set, the\n\t * hash key is `${tenantId}:${channelId}`. Use this when a single tenant is\n\t * too hot for one engine and its work can be partitioned (e.g. per-doc,\n\t * per-room) across multiple engines. Different channels of the same\n\t * tenant may land on different shards.\n\t */\n\tchannelId?: string;\n\t/**\n\t * Optional per-route rate-limit key. Looked up in `perRouteRateLimits`.\n\t * Unknown routes pass without per-route gating (only the tenant-wide\n\t * bucket applies).\n\t */\n\troute?: string;\n};\n\nexport type RouteResult = {\n\tdecision: RouteDecision;\n\t/** The chosen shard. `null` when `decision !== 'allow'`. */\n\tshard: Shard | null;\n\t/**\n\t * Set on a `'rate-limited'` result to tell the caller which bucket emptied:\n\t * `'tenant'` for the tenant-wide bucket, the route id for a per-route bucket.\n\t */\n\temptiedBucket?: 'tenant' | string;\n};\n\nexport type AcquireHandle = {\n\t/** Number of active connections for this tenant after acquire (>=1). */\n\tactive: number;\n\t/** Releases one connection for this tenant. Idempotent — safe to call once. */\n\trelease: () => void;\n};\n\nexport type RouterSnapshot = {\n\tversion: 1;\n\tat: number;\n\tshards: Array<Shard & { healthy: boolean; draining: boolean; active: number }>;\n\ttenants: Array<{\n\t\ttenant: string;\n\t\tactive: number;\n\t\ttokens: number;\n\t\tlastRefillAt: number;\n\t}>;\n\trouteBuckets: Array<{\n\t\ttenant: string;\n\t\troute: string;\n\t\ttokens: number;\n\t\tlastRefillAt: number;\n\t}>;\n};\n\nexport type Router = {\n\troute: (request: RouteRequest) => RouteResult;\n\tacquire: (tenantId: string) => AcquireHandle;\n\tmarkHealthy: (shardId: string) => void;\n\tmarkUnhealthy: (shardId: string) => void;\n\t/**\n\t * Begin draining a shard — exclude it from new routing decisions while\n\t * leaving existing acquires alone. Semantically distinct from\n\t * `markUnhealthy` (an operator-intentional state, not a failure).\n\t * `markHealthy` cancels both states. Use this before a planned shard\n\t * shutdown — tenants on the shard rehash to healthy non-draining shards\n\t * on their next route, but in-flight requests are NOT torn down.\n\t */\n\tdrainShard: (shardId: string) => void;\n\tisHealthy: (shardId: string) => boolean;\n\tisDraining: (shardId: string) => boolean;\n\taddShard: (shard: Shard) => void;\n\tremoveShard: (shardId: string) => void;\n\tshards: () => Shard[];\n\tsnapshot: () => RouterSnapshot;\n\trestore: (snapshot: RouterSnapshot) => void;\n\t/**\n\t * Operator-shaped point-in-time + cumulative metrics since\n\t * `createRouter()`. Use for tier dashboards and \"where am I\n\t * shedding load\" alerts. Distinct from `snapshot()`, which is the\n\t * persistence shape (preserves tenant + bucket state across a\n\t * shard reboot). Added in 0.2.0.\n\t */\n\tmetrics: () => RouterMetrics;\n\tdispose: () => void;\n};\n\n// -----------------------------------------------------------------------------\n// Hash strategies\n// -----------------------------------------------------------------------------\n\n/**\n * FNV-1a 32-bit. Cheap, no deps, good enough as a seed for the consistent\n * hash strategies below. Not cryptographic — do NOT use as a security hash.\n */\nconst fnv1a32 = (input: string): number => {\n\tlet hash = 0x811c9dc5;\n\tfor (let i = 0; i < input.length; i++) {\n\t\thash ^= input.charCodeAt(i);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn hash >>> 0;\n};\n\n/**\n * Jump-consistent-hash, Lamping & Veach 2014. Maps a 64-bit key to a bucket\n * in `[0, numBuckets)`. Properties: O(log n), no memory, exactly 1/N keys\n * move when buckets are added at the tail.\n */\nconst jumpHash = (key: string, numBuckets: number): number => {\n\tif (numBuckets <= 0) return -1;\n\tlet k = BigInt(fnv1a32(key)) | (BigInt(fnv1a32(key + '#hi')) << 32n);\n\tlet b = -1n;\n\tlet j = 0n;\n\twhile (j < BigInt(numBuckets)) {\n\t\tb = j;\n\t\tk = (k * 2862933555777941757n + 1n) & 0xffffffffffffffffn;\n\t\tconst shifted = (k >> 33n) + 1n;\n\t\tj = ((b + 1n) * (1n << 31n)) / shifted;\n\t}\n\treturn Number(b);\n};\n\nconst jumpStrategy: HashStrategyFn = (key, shards) => jumpHash(key, shards.length);\n\nconst makeRendezvousStrategy = (load?: ShardLoadFn): HashStrategyFn => (key, shards) => {\n\tlet bestIndex = 0;\n\tlet bestScore = -Infinity;\n\tfor (let i = 0; i < shards.length; i++) {\n\t\tconst shard = shards[i]!;\n\t\tconst baseWeight = shard.weight ?? 1;\n\t\tif (baseWeight <= 0) continue;\n\t\tconst loadValue = load ? Math.max(load(shard.id), 0.0001) : 1;\n\t\tconst effectiveWeight = baseWeight / loadValue;\n\t\tconst seed = fnv1a32(`${key}|${shard.id}`);\n\t\tconst u = (seed + 1) / 0x1_0000_0000;\n\t\tconst score = effectiveWeight * -Math.log(u);\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbestIndex = i;\n\t\t}\n\t}\n\treturn bestIndex;\n};\n\nconst resolveStrategy = (strategy: HashStrategy, load?: ShardLoadFn): HashStrategyFn => {\n\tif (strategy === 'jump') return jumpStrategy;\n\tif (strategy === 'rendezvous') return makeRendezvousStrategy(load);\n\treturn strategy;\n};\n\n// -----------------------------------------------------------------------------\n// Router\n// -----------------------------------------------------------------------------\n\ntype Bucket = {\n\ttokens: number;\n\tlastRefillAt: number;\n};\n\ntype TenantState = {\n\tactive: number;\n\tbucket: Bucket;\n};\n\nexport const createRouter = (options: RouterOptions): Router => {\n\tconst clock = options.clock ?? Date.now;\n\tconst strategy = resolveStrategy(options.hashStrategy ?? 'jump', options.load);\n\tconst cap = options.perTenantConnectionCap ?? Infinity;\n\tconst rate = options.perTenantRateLimit ?? { refillPerSecond: 0, tokens: Infinity };\n\tconst perRouteRateLimits = options.perRouteRateLimits ?? {};\n\tconst allowHook = options.allow;\n\n\tconst shardList: Shard[] = [...options.shards];\n\tconst healthy = new Map<string, boolean>();\n\tconst draining = new Set<string>();\n\tfor (const shard of shardList) healthy.set(shard.id, true);\n\n\tconst tenants = new Map<string, TenantState>();\n\t/** Per-tenant per-route bucket. Key = `${tenant}|${route}`. */\n\tconst routeBuckets = new Map<string, Bucket>();\n\tlet disposed = false;\n\t// 0.2.0: cumulative operator counters. Per-shard active count is\n\t// derived from `acquires` minus per-shard releases — but tracking\n\t// per-shard requires routing → acquire correlation that the\n\t// existing API doesn't enforce. Use point-in-time aggregation from\n\t// `tenants` Map: each tenant's active count contributes to its\n\t// most-recently-routed shard. (Acquire isn't shard-tagged today;\n\t// adding a `shardId` to the AcquireHandle is a separate change.)\n\tlet totalRoutes = 0;\n\tlet totalAcquires = 0;\n\tlet lastRouteMs = 0;\n\tconst rejectsByDecision: Record<\n\t\tExclude<RouteDecision, 'allow'>,\n\t\tnumber\n\t> = {\n\t\t'rate-limited': 0,\n\t\t'capped': 0,\n\t\t'no-shards': 0,\n\t\t'denied': 0\n\t};\n\tconst shardLoadByShard = new Map<string, number>();\n\n\tconst freshTenant = (now: number): TenantState => ({\n\t\tactive: 0,\n\t\tbucket: { lastRefillAt: now, tokens: rate.tokens },\n\t});\n\n\tconst ensureTenant = (id: string, now: number): TenantState => {\n\t\tconst found = tenants.get(id);\n\t\tif (found) return found;\n\t\tconst fresh = freshTenant(now);\n\t\ttenants.set(id, fresh);\n\t\treturn fresh;\n\t};\n\n\tconst refillBucket = (bucket: Bucket, rule: RateLimit, now: number) => {\n\t\tif (rule.refillPerSecond <= 0) return;\n\t\tconst elapsedMs = now - bucket.lastRefillAt;\n\t\tif (elapsedMs <= 0) return;\n\t\tconst added = (elapsedMs / 1000) * rule.refillPerSecond;\n\t\tbucket.tokens = Math.min(rule.tokens, bucket.tokens + added);\n\t\tbucket.lastRefillAt = now;\n\t};\n\n\tconst ensureRouteBucket = (tenant: string, route: string, rule: RateLimit, now: number): Bucket => {\n\t\tconst key = `${tenant}|${route}`;\n\t\tconst found = routeBuckets.get(key);\n\t\tif (found) return found;\n\t\tconst fresh: Bucket = { lastRefillAt: now, tokens: rule.tokens };\n\t\trouteBuckets.set(key, fresh);\n\t\treturn fresh;\n\t};\n\n\tconst eligibleShards = (): Shard[] =>\n\t\tshardList.filter((shard) => healthy.get(shard.id) === true && !draining.has(shard.id));\n\n\tconst route: Router['route'] = (request) => {\n\t\tconst routeStart = clock();\n\t\ttotalRoutes += 1;\n\t\tconst recordRejection = (\n\t\t\tdecision: Exclude<RouteDecision, 'allow'>\n\t\t): RouteResult => {\n\t\t\trejectsByDecision[decision] += 1;\n\t\t\tlastRouteMs = clock() - routeStart;\n\t\t\treturn { decision, shard: null };\n\t\t};\n\t\tif (disposed) return recordRejection('no-shards');\n\n\t\tconst live = eligibleShards();\n\t\tif (live.length === 0) return recordRejection('no-shards');\n\n\t\tif (allowHook && !allowHook(request.tenantId)) {\n\t\t\treturn recordRejection('denied');\n\t\t}\n\n\t\tconst now = routeStart;\n\t\tconst state = ensureTenant(request.tenantId, now);\n\n\t\tif (state.active >= cap) {\n\t\t\treturn recordRejection('capped');\n\t\t}\n\n\t\trefillBucket(state.bucket, rate, now);\n\t\tif (state.bucket.tokens < 1) {\n\t\t\trejectsByDecision['rate-limited'] += 1;\n\t\t\tlastRouteMs = clock() - routeStart;\n\t\t\treturn {\n\t\t\t\tdecision: 'rate-limited',\n\t\t\t\temptiedBucket: 'tenant',\n\t\t\t\tshard: null\n\t\t\t};\n\t\t}\n\n\t\tlet routeBucket: Bucket | null = null;\n\t\tlet routeRule: RateLimit | null = null;\n\t\tif (request.route !== undefined && perRouteRateLimits[request.route] !== undefined) {\n\t\t\trouteRule = perRouteRateLimits[request.route]!;\n\t\t\trouteBucket = ensureRouteBucket(request.tenantId, request.route, routeRule, now);\n\t\t\trefillBucket(routeBucket, routeRule, now);\n\t\t\tif (routeBucket.tokens < 1) {\n\t\t\t\trejectsByDecision['rate-limited'] += 1;\n\t\t\t\tlastRouteMs = clock() - routeStart;\n\t\t\t\treturn {\n\t\t\t\t\tdecision: 'rate-limited',\n\t\t\t\t\temptiedBucket: request.route,\n\t\t\t\t\tshard: null\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Commit both buckets only after both passed.\n\t\tstate.bucket.tokens -= 1;\n\t\tif (routeBucket) routeBucket.tokens -= 1;\n\n\t\tconst key = request.channelId === undefined\n\t\t\t? request.tenantId\n\t\t\t: `${request.tenantId}:${request.channelId}`;\n\t\tconst index = strategy(key, live);\n\t\tconst chosen = live[Math.max(0, Math.min(index, live.length - 1))]!;\n\t\tshardLoadByShard.set(\n\t\t\tchosen.id,\n\t\t\t(shardLoadByShard.get(chosen.id) ?? 0) + 1\n\t\t);\n\t\tlastRouteMs = clock() - routeStart;\n\t\treturn { decision: 'allow', shard: chosen };\n\t};\n\n\tconst acquire: Router['acquire'] = (tenantId) => {\n\t\tconst now = clock();\n\t\tconst state = ensureTenant(tenantId, now);\n\t\tstate.active += 1;\n\t\ttotalAcquires += 1;\n\t\tlet released = false;\n\t\treturn {\n\t\t\tactive: state.active,\n\t\t\trelease: () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tconst current = tenants.get(tenantId);\n\t\t\t\tif (current && current.active > 0) current.active -= 1;\n\t\t\t},\n\t\t};\n\t};\n\n\treturn {\n\t\tacquire,\n\t\taddShard: (shard) => {\n\t\t\tif (shardList.some((existing) => existing.id === shard.id)) return;\n\t\t\tshardList.push(shard);\n\t\t\thealthy.set(shard.id, true);\n\t\t},\n\t\tdispose: () => {\n\t\t\tdisposed = true;\n\t\t\tshardList.length = 0;\n\t\t\thealthy.clear();\n\t\t\tdraining.clear();\n\t\t\ttenants.clear();\n\t\t\trouteBuckets.clear();\n\t\t},\n\t\tdrainShard: (id) => {\n\t\t\tif (healthy.has(id)) draining.add(id);\n\t\t},\n\t\tisDraining: (id) => draining.has(id),\n\t\tisHealthy: (id) => healthy.get(id) === true,\n\t\tmetrics: () => ({\n\t\t\tacquires: totalAcquires,\n\t\t\tlastRouteMs,\n\t\t\trejectsByDecision: { ...rejectsByDecision },\n\t\t\troutes: totalRoutes,\n\t\t\tshardLoadDistribution: Object.fromEntries(shardLoadByShard)\n\t\t}),\n\t\tmarkHealthy: (id) => {\n\t\t\tif (healthy.has(id)) {\n\t\t\t\thealthy.set(id, true);\n\t\t\t\tdraining.delete(id);\n\t\t\t}\n\t\t},\n\t\tmarkUnhealthy: (id) => {\n\t\t\tif (healthy.has(id)) healthy.set(id, false);\n\t\t},\n\t\tremoveShard: (id) => {\n\t\t\tconst at = shardList.findIndex((shard) => shard.id === id);\n\t\t\tif (at >= 0) shardList.splice(at, 1);\n\t\t\thealthy.delete(id);\n\t\t\tdraining.delete(id);\n\t\t},\n\t\troute,\n\t\tshards: () => shardList.map((shard) => ({ ...shard })),\n\t\tsnapshot: () => {\n\t\t\tconst now = clock();\n\t\t\tconst tenantsOut: RouterSnapshot['tenants'] = [];\n\t\t\tfor (const [tenant, state] of tenants) {\n\t\t\t\ttenantsOut.push({\n\t\t\t\t\tactive: state.active,\n\t\t\t\t\tlastRefillAt: state.bucket.lastRefillAt,\n\t\t\t\t\ttenant,\n\t\t\t\t\ttokens: state.bucket.tokens,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst routesOut: RouterSnapshot['routeBuckets'] = [];\n\t\t\tfor (const [key, bucket] of routeBuckets) {\n\t\t\t\tconst pipe = key.indexOf('|');\n\t\t\t\tif (pipe < 0) continue;\n\t\t\t\troutesOut.push({\n\t\t\t\t\tlastRefillAt: bucket.lastRefillAt,\n\t\t\t\t\troute: key.slice(pipe + 1),\n\t\t\t\t\ttenant: key.slice(0, pipe),\n\t\t\t\t\ttokens: bucket.tokens,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tat: now,\n\t\t\t\trouteBuckets: routesOut,\n\t\t\t\tshards: shardList.map((shard) => {\n\t\t\t\t\tconst active = Array.from(tenants.values()).reduce((acc, state) => acc + state.active, 0);\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...shard,\n\t\t\t\t\t\tactive,\n\t\t\t\t\t\tdraining: draining.has(shard.id),\n\t\t\t\t\t\thealthy: healthy.get(shard.id) === true,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t\ttenants: tenantsOut,\n\t\t\t\tversion: 1,\n\t\t\t};\n\t\t},\n\t\trestore: (snap) => {\n\t\t\ttenants.clear();\n\t\t\trouteBuckets.clear();\n\t\t\tdraining.clear();\n\t\t\tfor (const t of snap.tenants) {\n\t\t\t\ttenants.set(t.tenant, {\n\t\t\t\t\tactive: t.active,\n\t\t\t\t\tbucket: { lastRefillAt: t.lastRefillAt, tokens: t.tokens },\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const r of snap.routeBuckets) {\n\t\t\t\trouteBuckets.set(`${r.tenant}|${r.route}`, {\n\t\t\t\t\tlastRefillAt: r.lastRefillAt,\n\t\t\t\t\ttokens: r.tokens,\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const shard of snap.shards) {\n\t\t\t\thealthy.set(shard.id, shard.healthy);\n\t\t\t\tif (shard.draining) draining.add(shard.id);\n\t\t\t}\n\t\t},\n\t};\n};\n"
|
|
5
|
+
"// @bun\n// src/index.ts\nvar SpanKind = {\n INTERNAL: 0,\n SERVER: 1,\n CLIENT: 2,\n PRODUCER: 3,\n CONSUMER: 4\n};\nvar SpanStatusCode = {\n UNSET: 0,\n OK: 1,\n ERROR: 2\n};\nvar NOOP_SPAN_CONTEXT = {\n spanId: \"0000000000000000\",\n traceFlags: 0,\n traceId: \"00000000000000000000000000000000\"\n};\nvar noopSpan = {\n addEvent: () => noopSpan,\n end: () => {},\n isRecording: () => false,\n recordException: () => {},\n setAttribute: () => noopSpan,\n setAttributes: () => noopSpan,\n setStatus: () => noopSpan,\n spanContext: () => NOOP_SPAN_CONTEXT,\n updateName: () => noopSpan\n};\nvar createNoopSpan = () => noopSpan;\nvar startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {\n const fn = typeof optionsOrFn === \"function\" ? optionsOrFn : maybeFn;\n return fn(noopSpan);\n};\nvar noopTracer = {\n startActiveSpan: startActiveSpanNoop,\n startSpan: () => noopSpan\n};\nvar createNoopTracer = () => noopTracer;\nvar createNoopTracerProvider = () => ({\n getTracer: () => noopTracer\n});\nvar tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;\nvar ABS_ATTRS = {\n tenant: \"abs.tenant\",\n shardId: \"abs.shard.id\",\n engineId: \"abs.engine.id\",\n collection: \"abs.collection\",\n mutation: \"abs.mutation\",\n mutationAttempt: \"abs.mutation.attempt\",\n subscriptionId: \"abs.subscription.id\",\n batchSize: \"abs.batch.size\",\n clusterMessageOrigin: \"abs.cluster.origin\",\n jobId: \"abs.job.id\",\n jobKind: \"abs.job.kind\",\n jobAttempt: \"abs.job.attempt\",\n jobMaxAttempts: \"abs.job.max_attempts\",\n workerId: \"abs.worker.id\",\n runtimeKey: \"abs.runtime.key\",\n runtimePid: \"abs.runtime.pid\",\n runtimePort: \"abs.runtime.port\",\n runtimeExitReason: \"abs.runtime.exit_reason\",\n runtimeReadinessMs: \"abs.runtime.readiness_ms\",\n routeShard: \"abs.route.shard\",\n routeDecision: \"abs.route.decision\",\n secretName: \"abs.secret.name\",\n secretFingerprint: \"abs.secret.fingerprint\",\n auditKind: \"abs.audit.kind\"\n};\nvar withSpan = async (tracer, name, options, fn) => tracer.startActiveSpan(name, options, async (span) => {\n try {\n const result = await fn(span);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error)\n });\n span.recordException(error);\n throw error;\n } finally {\n span.end();\n }\n});\nvar withSpanSync = (tracer, name, options, fn) => tracer.startActiveSpan(name, options, (span) => {\n try {\n const result = fn(span);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error)\n });\n span.recordException(error);\n throw error;\n } finally {\n span.end();\n }\n});\nexport {\n withSpanSync,\n withSpan,\n tracerOrNoop,\n createNoopTracerProvider,\n createNoopTracer,\n createNoopSpan,\n SpanStatusCode,\n SpanKind,\n ABS_ATTRS\n};\n\n//# debugId=3038092490E875A764756E2164756E21\n//# sourceMappingURL=index.js.map\n",
|
|
6
|
+
"/**\n * @absolutejs/router — multi-tenant connection routing primitive for Bun PaaS\n * gateways.\n *\n * Sits between an incoming request / WS upgrade and N backend processes (each\n * one a `@absolutejs/runtime` instance hosting a `@absolutejs/sync` engine\n * for a subset of tenants). Decides which shard owns the tenant, whether the\n * tenant is over its rate limit (tenant-wide + per-route), whether the tenant\n * is over its connection cap, whether the caller-supplied allow hook approves,\n * and whether the chosen shard is healthy and not draining.\n *\n * v0.1.0 is bun/elysia-agnostic — pure logic, no `Bun.serve` or proxy code.\n * The caller wires the routing decision into whichever HTTP/WS layer they\n * have. An Elysia adapter ships in a later 0.0.x as a subpath.\n */\n\nimport {\n\tABS_ATTRS,\n\ttracerOrNoop,\n\ttype TracerProvider\n} from '@absolutejs/telemetry';\n\nexport type Shard = {\n\tid: string;\n\t/**\n\t * Connection target for the chosen backend. Format is opaque to the router\n\t * — `ws://host:port`, `https://host`, a unix socket path, whatever the\n\t * caller's proxy layer understands.\n\t */\n\turl: string;\n\t/**\n\t * Relative weight for hash strategies that support it (rendezvous). Jump\n\t * hash ignores weights — every shard is treated as weight 1. Default 1.\n\t */\n\tweight?: number;\n};\n\n/**\n * Pure hash-strategy function. Given the routing key + the list of currently\n * eligible shards (healthy AND not draining), return the index in `shards` of\n * the chosen shard. The router never calls a strategy with an empty `shards`\n * array — that case is handled upstream as `no-shards`.\n */\nexport type HashStrategyFn = (key: string, shards: ReadonlyArray<Shard>) => number;\n\nexport type HashStrategy = 'jump' | 'rendezvous' | HashStrategyFn;\n\n/**\n * Per-tenant token-bucket rate limit. `tokens` is the bucket capacity AND the\n * starting balance; `refillPerSecond` is added continuously up to capacity.\n * Defaults: `Infinity` tokens / `0` refill = no limit.\n */\nexport type RateLimit = {\n\ttokens: number;\n\trefillPerSecond: number;\n};\n\n/**\n * Optional caller-supplied gate. Called per-route with the tenant id; returning\n * `false` causes the route to return `decision: 'denied'`. The intended\n * use is `meter.allow` from `@absolutejs/metering` — pass it directly to refuse\n * routes for over-quota tenants without wiring the integration manually.\n */\nexport type AllowHook = (tenantId: string) => boolean;\n\n/**\n * Optional caller-supplied load metric. Called per `route()` with a shard id;\n * returning a value > 1 makes the rendezvous strategy bias AWAY from this shard\n * (effective weight = `shard.weight / load`). Used to avoid hot-spotting when\n * a stickiness-locked shard is overloaded — the router can't move existing\n * tenants, but it can avoid sending NEW tenants there.\n *\n * Jump-hash ignores this — it has no per-call weight bias by design.\n */\nexport type ShardLoadFn = (shardId: string) => number;\n\nexport type RouterOptions = {\n\t/** Initial shard set. Can be empty (every `route()` returns `no-shards`). */\n\tshards: Shard[];\n\t/** Hash strategy. Default `'jump'`. */\n\thashStrategy?: HashStrategy;\n\t/**\n\t * Max concurrent connections per tenant (counted via `acquire()` /\n\t * `release()`). Default `Infinity` (no cap). When reached, `route()`\n\t * returns `'capped'`.\n\t */\n\tperTenantConnectionCap?: number;\n\t/**\n\t * Per-tenant token bucket. Default `{ tokens: Infinity, refillPerSecond: 0 }`\n\t * (no limit). When the tenant's bucket is empty, `route()` returns\n\t * `'rate-limited'`. One `route()` call costs one token.\n\t */\n\tperTenantRateLimit?: RateLimit;\n\t/**\n\t * Per-route token buckets layered on top of the tenant-wide bucket.\n\t * Keyed by route name; supplied by the caller via\n\t * `route({ route: 'someRoute' })`. The tenant bucket AND the route bucket\n\t * must both have a token available. When the route bucket is empty,\n\t * `route()` returns `'rate-limited'` with `routeId` set.\n\t */\n\tperRouteRateLimits?: Record<string, RateLimit>;\n\t/** Optional load hook biasing the rendezvous strategy. */\n\tload?: ShardLoadFn;\n\t/** Optional caller-supplied allow gate. */\n\tallow?: AllowHook;\n\t/** Override `Date.now` for tests. */\n\tclock?: () => number;\n\t/**\n\t * Optional OpenTelemetry tracer provider. When set, `route()` and\n\t * `acquire()` are wrapped in `router.route` / `router.acquire`\n\t * spans with `abs.tenant`, `abs.route.decision`, and (on allow)\n\t * `abs.route.shard` attributes. When omitted, all tracing is a\n\t * zero-allocation noop. Added in 0.3.0.\n\t *\n\t * Structural type via `@absolutejs/telemetry`; no peer-dep on\n\t * `@opentelemetry/api`.\n\t */\n\ttracerProvider?: TracerProvider;\n};\n\nexport type RouteDecision =\n\t| 'allow'\n\t| 'rate-limited'\n\t| 'capped'\n\t| 'no-shards'\n\t| 'denied';\n\n/**\n * Returned by {@link Router.metrics}. Combines point-in-time state with\n * cumulative counters since `createRouter()`. Survives `dispose()` so\n * post-shutdown introspection still reads totals. Added in 0.2.0.\n *\n * - `routes` — total `route()` calls (any decision).\n * - `acquires` — total `acquire()` calls.\n * - `rejectsByDecision` — counts per non-allow `RouteDecision`. The\n * operator's \"where am I shedding load?\" answer.\n * - `shardLoadDistribution` — cumulative routes assigned per shard\n * since `createRouter()`. With `markHealthy`/`drainShard` this is\n * the \"is rebalancing actually rebalancing?\" signal. (Per-shard\n * *active* connection count would require route-to-acquire\n * correlation that the current `acquire(tenantId)` API doesn't\n * capture; cumulative routes are the directly-observable proxy.)\n * - `lastRouteMs` — wall-clock of the most recent `route()` call (a\n * climb means the hot path is getting slower — usually a sign that\n * `load:` is doing too much work, or the shard count exploded).\n */\nexport type RouterMetrics = {\n\troutes: number;\n\tacquires: number;\n\trejectsByDecision: Record<Exclude<RouteDecision, 'allow'>, number>;\n\tshardLoadDistribution: Record<string, number>;\n\tlastRouteMs: number;\n};\n\nexport type RouteRequest = {\n\ttenantId: string;\n\t/**\n\t * Optional sub-key within a tenant for shardable channels. When set, the\n\t * hash key is `${tenantId}:${channelId}`. Use this when a single tenant is\n\t * too hot for one engine and its work can be partitioned (e.g. per-doc,\n\t * per-room) across multiple engines. Different channels of the same\n\t * tenant may land on different shards.\n\t */\n\tchannelId?: string;\n\t/**\n\t * Optional per-route rate-limit key. Looked up in `perRouteRateLimits`.\n\t * Unknown routes pass without per-route gating (only the tenant-wide\n\t * bucket applies).\n\t */\n\troute?: string;\n};\n\nexport type RouteResult = {\n\tdecision: RouteDecision;\n\t/** The chosen shard. `null` when `decision !== 'allow'`. */\n\tshard: Shard | null;\n\t/**\n\t * Set on a `'rate-limited'` result to tell the caller which bucket emptied:\n\t * `'tenant'` for the tenant-wide bucket, the route id for a per-route bucket.\n\t */\n\temptiedBucket?: 'tenant' | string;\n};\n\nexport type AcquireHandle = {\n\t/** Number of active connections for this tenant after acquire (>=1). */\n\tactive: number;\n\t/** Releases one connection for this tenant. Idempotent — safe to call once. */\n\trelease: () => void;\n};\n\nexport type RouterSnapshot = {\n\tversion: 1;\n\tat: number;\n\tshards: Array<Shard & { healthy: boolean; draining: boolean; active: number }>;\n\ttenants: Array<{\n\t\ttenant: string;\n\t\tactive: number;\n\t\ttokens: number;\n\t\tlastRefillAt: number;\n\t}>;\n\trouteBuckets: Array<{\n\t\ttenant: string;\n\t\troute: string;\n\t\ttokens: number;\n\t\tlastRefillAt: number;\n\t}>;\n};\n\nexport type Router = {\n\troute: (request: RouteRequest) => RouteResult;\n\tacquire: (tenantId: string) => AcquireHandle;\n\tmarkHealthy: (shardId: string) => void;\n\tmarkUnhealthy: (shardId: string) => void;\n\t/**\n\t * Begin draining a shard — exclude it from new routing decisions while\n\t * leaving existing acquires alone. Semantically distinct from\n\t * `markUnhealthy` (an operator-intentional state, not a failure).\n\t * `markHealthy` cancels both states. Use this before a planned shard\n\t * shutdown — tenants on the shard rehash to healthy non-draining shards\n\t * on their next route, but in-flight requests are NOT torn down.\n\t */\n\tdrainShard: (shardId: string) => void;\n\tisHealthy: (shardId: string) => boolean;\n\tisDraining: (shardId: string) => boolean;\n\taddShard: (shard: Shard) => void;\n\tremoveShard: (shardId: string) => void;\n\tshards: () => Shard[];\n\tsnapshot: () => RouterSnapshot;\n\trestore: (snapshot: RouterSnapshot) => void;\n\t/**\n\t * Operator-shaped point-in-time + cumulative metrics since\n\t * `createRouter()`. Use for tier dashboards and \"where am I\n\t * shedding load\" alerts. Distinct from `snapshot()`, which is the\n\t * persistence shape (preserves tenant + bucket state across a\n\t * shard reboot). Added in 0.2.0.\n\t */\n\tmetrics: () => RouterMetrics;\n\tdispose: () => void;\n};\n\n// -----------------------------------------------------------------------------\n// Hash strategies\n// -----------------------------------------------------------------------------\n\n/**\n * FNV-1a 32-bit. Cheap, no deps, good enough as a seed for the consistent\n * hash strategies below. Not cryptographic — do NOT use as a security hash.\n */\nconst fnv1a32 = (input: string): number => {\n\tlet hash = 0x811c9dc5;\n\tfor (let i = 0; i < input.length; i++) {\n\t\thash ^= input.charCodeAt(i);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn hash >>> 0;\n};\n\n/**\n * Jump-consistent-hash, Lamping & Veach 2014. Maps a 64-bit key to a bucket\n * in `[0, numBuckets)`. Properties: O(log n), no memory, exactly 1/N keys\n * move when buckets are added at the tail.\n */\nconst jumpHash = (key: string, numBuckets: number): number => {\n\tif (numBuckets <= 0) return -1;\n\tlet k = BigInt(fnv1a32(key)) | (BigInt(fnv1a32(key + '#hi')) << 32n);\n\tlet b = -1n;\n\tlet j = 0n;\n\twhile (j < BigInt(numBuckets)) {\n\t\tb = j;\n\t\tk = (k * 2862933555777941757n + 1n) & 0xffffffffffffffffn;\n\t\tconst shifted = (k >> 33n) + 1n;\n\t\tj = ((b + 1n) * (1n << 31n)) / shifted;\n\t}\n\treturn Number(b);\n};\n\nconst jumpStrategy: HashStrategyFn = (key, shards) => jumpHash(key, shards.length);\n\nconst makeRendezvousStrategy = (load?: ShardLoadFn): HashStrategyFn => (key, shards) => {\n\tlet bestIndex = 0;\n\tlet bestScore = -Infinity;\n\tfor (let i = 0; i < shards.length; i++) {\n\t\tconst shard = shards[i]!;\n\t\tconst baseWeight = shard.weight ?? 1;\n\t\tif (baseWeight <= 0) continue;\n\t\tconst loadValue = load ? Math.max(load(shard.id), 0.0001) : 1;\n\t\tconst effectiveWeight = baseWeight / loadValue;\n\t\tconst seed = fnv1a32(`${key}|${shard.id}`);\n\t\tconst u = (seed + 1) / 0x1_0000_0000;\n\t\tconst score = effectiveWeight * -Math.log(u);\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbestIndex = i;\n\t\t}\n\t}\n\treturn bestIndex;\n};\n\nconst resolveStrategy = (strategy: HashStrategy, load?: ShardLoadFn): HashStrategyFn => {\n\tif (strategy === 'jump') return jumpStrategy;\n\tif (strategy === 'rendezvous') return makeRendezvousStrategy(load);\n\treturn strategy;\n};\n\n// -----------------------------------------------------------------------------\n// Router\n// -----------------------------------------------------------------------------\n\ntype Bucket = {\n\ttokens: number;\n\tlastRefillAt: number;\n};\n\ntype TenantState = {\n\tactive: number;\n\tbucket: Bucket;\n};\n\nexport const createRouter = (options: RouterOptions): Router => {\n\tconst clock = options.clock ?? Date.now;\n\tconst strategy = resolveStrategy(options.hashStrategy ?? 'jump', options.load);\n\tconst cap = options.perTenantConnectionCap ?? Infinity;\n\tconst rate = options.perTenantRateLimit ?? { refillPerSecond: 0, tokens: Infinity };\n\tconst perRouteRateLimits = options.perRouteRateLimits ?? {};\n\tconst allowHook = options.allow;\n\n\tconst shardList: Shard[] = [...options.shards];\n\tconst healthy = new Map<string, boolean>();\n\tconst draining = new Set<string>();\n\tfor (const shard of shardList) healthy.set(shard.id, true);\n\n\tconst tenants = new Map<string, TenantState>();\n\t/** Per-tenant per-route bucket. Key = `${tenant}|${route}`. */\n\tconst routeBuckets = new Map<string, Bucket>();\n\tlet disposed = false;\n\t// 0.3.0: OTel tracer (noop when options.tracerProvider unset).\n\tconst tracer = tracerOrNoop(options.tracerProvider, '@absolutejs/router');\n\t// 0.2.0: cumulative operator counters. Per-shard active count is\n\t// derived from `acquires` minus per-shard releases — but tracking\n\t// per-shard requires routing → acquire correlation that the\n\t// existing API doesn't enforce. Use point-in-time aggregation from\n\t// `tenants` Map: each tenant's active count contributes to its\n\t// most-recently-routed shard. (Acquire isn't shard-tagged today;\n\t// adding a `shardId` to the AcquireHandle is a separate change.)\n\tlet totalRoutes = 0;\n\tlet totalAcquires = 0;\n\tlet lastRouteMs = 0;\n\tconst rejectsByDecision: Record<\n\t\tExclude<RouteDecision, 'allow'>,\n\t\tnumber\n\t> = {\n\t\t'rate-limited': 0,\n\t\t'capped': 0,\n\t\t'no-shards': 0,\n\t\t'denied': 0\n\t};\n\tconst shardLoadByShard = new Map<string, number>();\n\n\tconst freshTenant = (now: number): TenantState => ({\n\t\tactive: 0,\n\t\tbucket: { lastRefillAt: now, tokens: rate.tokens },\n\t});\n\n\tconst ensureTenant = (id: string, now: number): TenantState => {\n\t\tconst found = tenants.get(id);\n\t\tif (found) return found;\n\t\tconst fresh = freshTenant(now);\n\t\ttenants.set(id, fresh);\n\t\treturn fresh;\n\t};\n\n\tconst refillBucket = (bucket: Bucket, rule: RateLimit, now: number) => {\n\t\tif (rule.refillPerSecond <= 0) return;\n\t\tconst elapsedMs = now - bucket.lastRefillAt;\n\t\tif (elapsedMs <= 0) return;\n\t\tconst added = (elapsedMs / 1000) * rule.refillPerSecond;\n\t\tbucket.tokens = Math.min(rule.tokens, bucket.tokens + added);\n\t\tbucket.lastRefillAt = now;\n\t};\n\n\tconst ensureRouteBucket = (tenant: string, route: string, rule: RateLimit, now: number): Bucket => {\n\t\tconst key = `${tenant}|${route}`;\n\t\tconst found = routeBuckets.get(key);\n\t\tif (found) return found;\n\t\tconst fresh: Bucket = { lastRefillAt: now, tokens: rule.tokens };\n\t\trouteBuckets.set(key, fresh);\n\t\treturn fresh;\n\t};\n\n\tconst eligibleShards = (): Shard[] =>\n\t\tshardList.filter((shard) => healthy.get(shard.id) === true && !draining.has(shard.id));\n\n\tconst route: Router['route'] = (request) => {\n\t\t// 0.3.0: span the routing decision. Hot-path safe — when no\n\t\t// tracerProvider is set, the noop tracer's startSpan returns\n\t\t// the singleton noop span (zero allocation).\n\t\tconst span = tracer.startSpan('router.route', {\n\t\t\tattributes: {\n\t\t\t\t[ABS_ATTRS.tenant]: request.tenantId,\n\t\t\t\t...(request.route !== undefined\n\t\t\t\t\t? { 'abs.route.name': request.route }\n\t\t\t\t\t: {})\n\t\t\t}\n\t\t});\n\t\tconst routeStart = clock();\n\t\ttotalRoutes += 1;\n\t\tconst finishSpan = (result: RouteResult): RouteResult => {\n\t\t\tspan.setAttribute(ABS_ATTRS.routeDecision, result.decision);\n\t\t\tif (result.shard !== null) {\n\t\t\t\tspan.setAttribute(ABS_ATTRS.routeShard, result.shard.id);\n\t\t\t}\n\t\t\tspan.setStatus({\n\t\t\t\tcode: result.decision === 'allow' ? 1 /* OK */ : 2 /* ERROR */\n\t\t\t});\n\t\t\tspan.end();\n\t\t\treturn result;\n\t\t};\n\t\tconst recordRejection = (\n\t\t\tdecision: Exclude<RouteDecision, 'allow'>\n\t\t): RouteResult => {\n\t\t\trejectsByDecision[decision] += 1;\n\t\t\tlastRouteMs = clock() - routeStart;\n\t\t\treturn finishSpan({ decision, shard: null });\n\t\t};\n\t\tif (disposed) return recordRejection('no-shards');\n\n\t\tconst live = eligibleShards();\n\t\tif (live.length === 0) return recordRejection('no-shards');\n\n\t\tif (allowHook && !allowHook(request.tenantId)) {\n\t\t\treturn recordRejection('denied');\n\t\t}\n\n\t\tconst now = routeStart;\n\t\tconst state = ensureTenant(request.tenantId, now);\n\n\t\tif (state.active >= cap) {\n\t\t\treturn recordRejection('capped');\n\t\t}\n\n\t\trefillBucket(state.bucket, rate, now);\n\t\tif (state.bucket.tokens < 1) {\n\t\t\trejectsByDecision['rate-limited'] += 1;\n\t\t\tlastRouteMs = clock() - routeStart;\n\t\t\treturn finishSpan({\n\t\t\t\tdecision: 'rate-limited',\n\t\t\t\temptiedBucket: 'tenant',\n\t\t\t\tshard: null\n\t\t\t});\n\t\t}\n\n\t\tlet routeBucket: Bucket | null = null;\n\t\tlet routeRule: RateLimit | null = null;\n\t\tif (request.route !== undefined && perRouteRateLimits[request.route] !== undefined) {\n\t\t\trouteRule = perRouteRateLimits[request.route]!;\n\t\t\trouteBucket = ensureRouteBucket(request.tenantId, request.route, routeRule, now);\n\t\t\trefillBucket(routeBucket, routeRule, now);\n\t\t\tif (routeBucket.tokens < 1) {\n\t\t\t\trejectsByDecision['rate-limited'] += 1;\n\t\t\t\tlastRouteMs = clock() - routeStart;\n\t\t\t\treturn finishSpan({\n\t\t\t\t\tdecision: 'rate-limited',\n\t\t\t\t\temptiedBucket: request.route,\n\t\t\t\t\tshard: null\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Commit both buckets only after both passed.\n\t\tstate.bucket.tokens -= 1;\n\t\tif (routeBucket) routeBucket.tokens -= 1;\n\n\t\tconst key = request.channelId === undefined\n\t\t\t? request.tenantId\n\t\t\t: `${request.tenantId}:${request.channelId}`;\n\t\tconst index = strategy(key, live);\n\t\tconst chosen = live[Math.max(0, Math.min(index, live.length - 1))]!;\n\t\tshardLoadByShard.set(\n\t\t\tchosen.id,\n\t\t\t(shardLoadByShard.get(chosen.id) ?? 0) + 1\n\t\t);\n\t\tlastRouteMs = clock() - routeStart;\n\t\treturn finishSpan({ decision: 'allow', shard: chosen });\n\t};\n\n\tconst acquire: Router['acquire'] = (tenantId) => {\n\t\t// 0.3.0: span the acquire. It's instantaneous bookkeeping, but\n\t\t// the per-tenant active count is one of the most operationally\n\t\t// interesting metrics — a sustained climb signals tenants\n\t\t// holding more connections than expected.\n\t\tconst span = tracer.startSpan('router.acquire', {\n\t\t\tattributes: { [ABS_ATTRS.tenant]: tenantId }\n\t\t});\n\t\tconst now = clock();\n\t\tconst state = ensureTenant(tenantId, now);\n\t\tstate.active += 1;\n\t\ttotalAcquires += 1;\n\t\tspan.setAttribute('abs.tenant.active', state.active);\n\t\tspan.setStatus({ code: 1 /* OK */ });\n\t\tspan.end();\n\t\tlet released = false;\n\t\treturn {\n\t\t\tactive: state.active,\n\t\t\trelease: () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tconst current = tenants.get(tenantId);\n\t\t\t\tif (current && current.active > 0) current.active -= 1;\n\t\t\t},\n\t\t};\n\t};\n\n\treturn {\n\t\tacquire,\n\t\taddShard: (shard) => {\n\t\t\tif (shardList.some((existing) => existing.id === shard.id)) return;\n\t\t\tshardList.push(shard);\n\t\t\thealthy.set(shard.id, true);\n\t\t},\n\t\tdispose: () => {\n\t\t\tdisposed = true;\n\t\t\tshardList.length = 0;\n\t\t\thealthy.clear();\n\t\t\tdraining.clear();\n\t\t\ttenants.clear();\n\t\t\trouteBuckets.clear();\n\t\t},\n\t\tdrainShard: (id) => {\n\t\t\tif (healthy.has(id)) draining.add(id);\n\t\t},\n\t\tisDraining: (id) => draining.has(id),\n\t\tisHealthy: (id) => healthy.get(id) === true,\n\t\tmetrics: () => ({\n\t\t\tacquires: totalAcquires,\n\t\t\tlastRouteMs,\n\t\t\trejectsByDecision: { ...rejectsByDecision },\n\t\t\troutes: totalRoutes,\n\t\t\tshardLoadDistribution: Object.fromEntries(shardLoadByShard)\n\t\t}),\n\t\tmarkHealthy: (id) => {\n\t\t\tif (healthy.has(id)) {\n\t\t\t\thealthy.set(id, true);\n\t\t\t\tdraining.delete(id);\n\t\t\t}\n\t\t},\n\t\tmarkUnhealthy: (id) => {\n\t\t\tif (healthy.has(id)) healthy.set(id, false);\n\t\t},\n\t\tremoveShard: (id) => {\n\t\t\tconst at = shardList.findIndex((shard) => shard.id === id);\n\t\t\tif (at >= 0) shardList.splice(at, 1);\n\t\t\thealthy.delete(id);\n\t\t\tdraining.delete(id);\n\t\t},\n\t\troute,\n\t\tshards: () => shardList.map((shard) => ({ ...shard })),\n\t\tsnapshot: () => {\n\t\t\tconst now = clock();\n\t\t\tconst tenantsOut: RouterSnapshot['tenants'] = [];\n\t\t\tfor (const [tenant, state] of tenants) {\n\t\t\t\ttenantsOut.push({\n\t\t\t\t\tactive: state.active,\n\t\t\t\t\tlastRefillAt: state.bucket.lastRefillAt,\n\t\t\t\t\ttenant,\n\t\t\t\t\ttokens: state.bucket.tokens,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst routesOut: RouterSnapshot['routeBuckets'] = [];\n\t\t\tfor (const [key, bucket] of routeBuckets) {\n\t\t\t\tconst pipe = key.indexOf('|');\n\t\t\t\tif (pipe < 0) continue;\n\t\t\t\troutesOut.push({\n\t\t\t\t\tlastRefillAt: bucket.lastRefillAt,\n\t\t\t\t\troute: key.slice(pipe + 1),\n\t\t\t\t\ttenant: key.slice(0, pipe),\n\t\t\t\t\ttokens: bucket.tokens,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tat: now,\n\t\t\t\trouteBuckets: routesOut,\n\t\t\t\tshards: shardList.map((shard) => {\n\t\t\t\t\tconst active = Array.from(tenants.values()).reduce((acc, state) => acc + state.active, 0);\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...shard,\n\t\t\t\t\t\tactive,\n\t\t\t\t\t\tdraining: draining.has(shard.id),\n\t\t\t\t\t\thealthy: healthy.get(shard.id) === true,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t\ttenants: tenantsOut,\n\t\t\t\tversion: 1,\n\t\t\t};\n\t\t},\n\t\trestore: (snap) => {\n\t\t\ttenants.clear();\n\t\t\trouteBuckets.clear();\n\t\t\tdraining.clear();\n\t\t\tfor (const t of snap.tenants) {\n\t\t\t\ttenants.set(t.tenant, {\n\t\t\t\t\tactive: t.active,\n\t\t\t\t\tbucket: { lastRefillAt: t.lastRefillAt, tokens: t.tokens },\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const r of snap.routeBuckets) {\n\t\t\t\trouteBuckets.set(`${r.tenant}|${r.route}`, {\n\t\t\t\t\tlastRefillAt: r.lastRefillAt,\n\t\t\t\t\ttokens: r.tokens,\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const shard of snap.shards) {\n\t\t\t\thealthy.set(shard.id, shard.healthy);\n\t\t\t\tif (shard.draining) draining.add(shard.id);\n\t\t\t}\n\t\t},\n\t};\n};\n"
|
|
6
7
|
],
|
|
7
|
-
"mappings": ";;
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "E0A5EDB21819D2C764756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Multi-tenant connection routing primitive for Bun PaaS gateways. Consistent-hash tenant→shard, per-tenant connection cap, per-tenant rate limit, healthy-shard skip. The library that goes in front of N @absolutejs/runtime instances.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,10 +33,14 @@
|
|
|
33
33
|
"check:package": "bun run typecheck && bun run build && bun run test",
|
|
34
34
|
"release": "bun run format && bun run check:package && bun publish"
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@absolutejs/telemetry": "^0.0.2"
|
|
38
|
+
},
|
|
36
39
|
"peerDependencies": {
|
|
37
40
|
"bun-types": "^1.3.14"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
43
|
+
"@absolutejs/telemetry": "^0.0.2",
|
|
40
44
|
"@types/bun": "^1.3.14",
|
|
41
45
|
"prettier": "^3.8.3",
|
|
42
46
|
"typescript": "^6.0.3"
|