@absolutejs/router 0.1.0 → 0.2.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 CHANGED
@@ -94,6 +94,32 @@ export type RouterOptions = {
94
94
  clock?: () => number;
95
95
  };
96
96
  export type RouteDecision = 'allow' | 'rate-limited' | 'capped' | 'no-shards' | 'denied';
97
+ /**
98
+ * Returned by {@link Router.metrics}. Combines point-in-time state with
99
+ * cumulative counters since `createRouter()`. Survives `dispose()` so
100
+ * post-shutdown introspection still reads totals. Added in 0.2.0.
101
+ *
102
+ * - `routes` — total `route()` calls (any decision).
103
+ * - `acquires` — total `acquire()` calls.
104
+ * - `rejectsByDecision` — counts per non-allow `RouteDecision`. The
105
+ * operator's "where am I shedding load?" answer.
106
+ * - `shardLoadDistribution` — cumulative routes assigned per shard
107
+ * since `createRouter()`. With `markHealthy`/`drainShard` this is
108
+ * the "is rebalancing actually rebalancing?" signal. (Per-shard
109
+ * *active* connection count would require route-to-acquire
110
+ * correlation that the current `acquire(tenantId)` API doesn't
111
+ * capture; cumulative routes are the directly-observable proxy.)
112
+ * - `lastRouteMs` — wall-clock of the most recent `route()` call (a
113
+ * climb means the hot path is getting slower — usually a sign that
114
+ * `load:` is doing too much work, or the shard count exploded).
115
+ */
116
+ export type RouterMetrics = {
117
+ routes: number;
118
+ acquires: number;
119
+ rejectsByDecision: Record<Exclude<RouteDecision, 'allow'>, number>;
120
+ shardLoadDistribution: Record<string, number>;
121
+ lastRouteMs: number;
122
+ };
97
123
  export type RouteRequest = {
98
124
  tenantId: string;
99
125
  /**
@@ -169,6 +195,14 @@ export type Router = {
169
195
  shards: () => Shard[];
170
196
  snapshot: () => RouterSnapshot;
171
197
  restore: (snapshot: RouterSnapshot) => void;
198
+ /**
199
+ * Operator-shaped point-in-time + cumulative metrics since
200
+ * `createRouter()`. Use for tier dashboards and "where am I
201
+ * shedding load" alerts. Distinct from `snapshot()`, which is the
202
+ * persistence shape (preserves tenant + bucket state across a
203
+ * shard reboot). Added in 0.2.0.
204
+ */
205
+ metrics: () => RouterMetrics;
172
206
  dispose: () => void;
173
207
  };
174
208
  export declare const createRouter: (options: RouterOptions) => Router;
package/dist/index.js CHANGED
@@ -65,6 +65,16 @@ var createRouter = (options) => {
65
65
  const tenants = new Map;
66
66
  const routeBuckets = new Map;
67
67
  let disposed = false;
68
+ let totalRoutes = 0;
69
+ let totalAcquires = 0;
70
+ let lastRouteMs = 0;
71
+ const rejectsByDecision = {
72
+ "rate-limited": 0,
73
+ capped: 0,
74
+ "no-shards": 0,
75
+ denied: 0
76
+ };
77
+ const shardLoadByShard = new Map;
68
78
  const freshTenant = (now) => ({
69
79
  active: 0,
70
80
  bucket: { lastRefillAt: now, tokens: rate.tokens }
@@ -98,22 +108,35 @@ var createRouter = (options) => {
98
108
  };
99
109
  const eligibleShards = () => shardList.filter((shard) => healthy.get(shard.id) === true && !draining.has(shard.id));
100
110
  const route = (request) => {
111
+ const routeStart = clock();
112
+ totalRoutes += 1;
113
+ const recordRejection = (decision) => {
114
+ rejectsByDecision[decision] += 1;
115
+ lastRouteMs = clock() - routeStart;
116
+ return { decision, shard: null };
117
+ };
101
118
  if (disposed)
102
- return { decision: "no-shards", shard: null };
119
+ return recordRejection("no-shards");
103
120
  const live = eligibleShards();
104
121
  if (live.length === 0)
105
- return { decision: "no-shards", shard: null };
122
+ return recordRejection("no-shards");
106
123
  if (allowHook && !allowHook(request.tenantId)) {
107
- return { decision: "denied", shard: null };
124
+ return recordRejection("denied");
108
125
  }
109
- const now = clock();
126
+ const now = routeStart;
110
127
  const state = ensureTenant(request.tenantId, now);
111
128
  if (state.active >= cap) {
112
- return { decision: "capped", shard: null };
129
+ return recordRejection("capped");
113
130
  }
114
131
  refillBucket(state.bucket, rate, now);
115
132
  if (state.bucket.tokens < 1) {
116
- return { decision: "rate-limited", emptiedBucket: "tenant", shard: null };
133
+ rejectsByDecision["rate-limited"] += 1;
134
+ lastRouteMs = clock() - routeStart;
135
+ return {
136
+ decision: "rate-limited",
137
+ emptiedBucket: "tenant",
138
+ shard: null
139
+ };
117
140
  }
118
141
  let routeBucket = null;
119
142
  let routeRule = null;
@@ -122,7 +145,13 @@ var createRouter = (options) => {
122
145
  routeBucket = ensureRouteBucket(request.tenantId, request.route, routeRule, now);
123
146
  refillBucket(routeBucket, routeRule, now);
124
147
  if (routeBucket.tokens < 1) {
125
- return { decision: "rate-limited", emptiedBucket: request.route, shard: null };
148
+ rejectsByDecision["rate-limited"] += 1;
149
+ lastRouteMs = clock() - routeStart;
150
+ return {
151
+ decision: "rate-limited",
152
+ emptiedBucket: request.route,
153
+ shard: null
154
+ };
126
155
  }
127
156
  }
128
157
  state.bucket.tokens -= 1;
@@ -131,12 +160,15 @@ var createRouter = (options) => {
131
160
  const key = request.channelId === undefined ? request.tenantId : `${request.tenantId}:${request.channelId}`;
132
161
  const index = strategy(key, live);
133
162
  const chosen = live[Math.max(0, Math.min(index, live.length - 1))];
163
+ shardLoadByShard.set(chosen.id, (shardLoadByShard.get(chosen.id) ?? 0) + 1);
164
+ lastRouteMs = clock() - routeStart;
134
165
  return { decision: "allow", shard: chosen };
135
166
  };
136
167
  const acquire = (tenantId) => {
137
168
  const now = clock();
138
169
  const state = ensureTenant(tenantId, now);
139
170
  state.active += 1;
171
+ totalAcquires += 1;
140
172
  let released = false;
141
173
  return {
142
174
  active: state.active,
@@ -172,6 +204,13 @@ var createRouter = (options) => {
172
204
  },
173
205
  isDraining: (id) => draining.has(id),
174
206
  isHealthy: (id) => healthy.get(id) === true,
207
+ metrics: () => ({
208
+ acquires: totalAcquires,
209
+ lastRouteMs,
210
+ rejectsByDecision: { ...rejectsByDecision },
211
+ routes: totalRoutes,
212
+ shardLoadDistribution: Object.fromEntries(shardLoadByShard)
213
+ }),
175
214
  markHealthy: (id) => {
176
215
  if (healthy.has(id)) {
177
216
  healthy.set(id, true);
@@ -258,5 +297,5 @@ export {
258
297
  createRouter
259
298
  };
260
299
 
261
- //# debugId=C4E01145DD33C32964756E2164756E21
300
+ //# debugId=E49309CC1211AA2864756E2164756E21
262
301
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../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\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\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\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\tif (disposed) return { decision: 'no-shards', shard: null };\n\n\t\tconst live = eligibleShards();\n\t\tif (live.length === 0) return { decision: 'no-shards', shard: null };\n\n\t\tif (allowHook && !allowHook(request.tenantId)) {\n\t\t\treturn { decision: 'denied', shard: null };\n\t\t}\n\n\t\tconst now = clock();\n\t\tconst state = ensureTenant(request.tenantId, now);\n\n\t\tif (state.active >= cap) {\n\t\t\treturn { decision: 'capped', shard: null };\n\t\t}\n\n\t\trefillBucket(state.bucket, rate, now);\n\t\tif (state.bucket.tokens < 1) {\n\t\t\treturn { decision: 'rate-limited', emptiedBucket: 'tenant', shard: null };\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\treturn { decision: 'rate-limited', emptiedBucket: request.route, shard: null };\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\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\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\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
+ "/**\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"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "C4E01145DD33C32964756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "E49309CC1211AA2864756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/router",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",