@absolutejs/router 0.0.1
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/LICENSE +94 -0
- package/README.md +98 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +10 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
**Licensor:** Alex Kahn
|
|
4
|
+
|
|
5
|
+
**Licensed Work:** @absolutejs/router (https://github.com/absolutejs/router)
|
|
6
|
+
|
|
7
|
+
**Change Date:** May 29, 2030
|
|
8
|
+
|
|
9
|
+
**Change License:** Apache License, Version 2.0
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Terms
|
|
14
|
+
|
|
15
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
16
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
17
|
+
Licensor may make an Additional Use Grant, permitting limited production use.
|
|
18
|
+
|
|
19
|
+
### Additional Use Grant
|
|
20
|
+
|
|
21
|
+
You may use the Licensed Work in production, provided your use does not include
|
|
22
|
+
any of the following:
|
|
23
|
+
|
|
24
|
+
1. **Offering a Competing Service.** You may not offer the Licensed Work, or
|
|
25
|
+
any derivative or substantial portion of it, to third parties as a hosted or
|
|
26
|
+
managed service that competes with a hosted multi-tenant connection router
|
|
27
|
+
or WebSocket edge gateway for application runtimes (including, but not
|
|
28
|
+
limited to, services like Cloudflare Workers WebSockets / Smart Placement,
|
|
29
|
+
Vercel's edge router, Liveblocks' WebSocket fan-out, PartyKit, Ably,
|
|
30
|
+
Pusher, Soketi, or any similar hosted offering whose primary value to its
|
|
31
|
+
users is multi-tenant connection routing, tenant-to-shard stickiness,
|
|
32
|
+
per-tenant connection capping, or per-tenant rate limiting at the network
|
|
33
|
+
edge). This includes any product whose primary value to its users is the
|
|
34
|
+
functionality the Licensed Work provides.
|
|
35
|
+
|
|
36
|
+
2. **Resale or Redistribution as a Standalone Product.** You may not sell,
|
|
37
|
+
license, or distribute the Licensed Work, or any derivative or fork of it,
|
|
38
|
+
as a standalone commercial product.
|
|
39
|
+
|
|
40
|
+
3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
|
|
41
|
+
the Licensed Work must prominently credit AbsoluteJS and include a link to
|
|
42
|
+
the original project repository (https://github.com/absolutejs/router).
|
|
43
|
+
|
|
44
|
+
For clarity, the following uses are expressly permitted:
|
|
45
|
+
|
|
46
|
+
- Using the Licensed Work to build and operate your own applications, websites,
|
|
47
|
+
internal tools, or SaaS products (whether commercial or non-commercial), so
|
|
48
|
+
long as the Licensed Work itself is not the primary product you are selling.
|
|
49
|
+
- Using the Licensed Work as a dependency in commercial software you build and
|
|
50
|
+
sell, as long as the software is not itself a competing managed service of
|
|
51
|
+
the kind described in clause 1.
|
|
52
|
+
- Providing consulting, development, or professional services to clients using
|
|
53
|
+
the Licensed Work.
|
|
54
|
+
- Forking and modifying the Licensed Work for your own internal use, provided
|
|
55
|
+
attribution is maintained.
|
|
56
|
+
|
|
57
|
+
### Change Date and Change License
|
|
58
|
+
|
|
59
|
+
On the Change Date specified above, or on such other date as the Licensor may
|
|
60
|
+
specify by written notice, the Licensed Work will be made available under the
|
|
61
|
+
Change License (Apache License, Version 2.0). Until the Change Date, the terms
|
|
62
|
+
of this Business Source License 1.1 apply.
|
|
63
|
+
|
|
64
|
+
### Trademark
|
|
65
|
+
|
|
66
|
+
This license does not grant you any rights to use the "AbsoluteJS" or
|
|
67
|
+
"@absolutejs" name, logo, or any related trademarks. Forks and derivative works
|
|
68
|
+
must not be named or branded in a manner that suggests endorsement by or
|
|
69
|
+
affiliation with AbsoluteJS or the Licensor.
|
|
70
|
+
|
|
71
|
+
### Notices
|
|
72
|
+
|
|
73
|
+
You must not remove or obscure any licensing, copyright, or other notices
|
|
74
|
+
included in the Licensed Work.
|
|
75
|
+
|
|
76
|
+
### No Warranty
|
|
77
|
+
|
|
78
|
+
THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
|
|
79
|
+
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
|
80
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
|
|
81
|
+
EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
|
|
82
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
83
|
+
IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
|
|
84
|
+
LICENSED WORK.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Contact
|
|
89
|
+
|
|
90
|
+
For commercial licensing inquiries or additional permissions, contact:
|
|
91
|
+
|
|
92
|
+
- **Alex Kahn**
|
|
93
|
+
- alexkahndev@gmail.com
|
|
94
|
+
- alexkahndev.github.io
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @absolutejs/router
|
|
2
|
+
|
|
3
|
+
Multi-tenant connection routing primitive for Bun PaaS gateways. Sits in front
|
|
4
|
+
of N backend processes (each a [`@absolutejs/runtime`](https://github.com/absolutejs/runtime)
|
|
5
|
+
instance hosting a [`@absolutejs/sync`](https://github.com/absolutejs/sync) engine
|
|
6
|
+
for a subset of tenants) and decides — per request:
|
|
7
|
+
|
|
8
|
+
1. Which shard owns this tenant (consistent hash, sticky)
|
|
9
|
+
2. Is the tenant over its connection cap?
|
|
10
|
+
3. Is the tenant over its rate limit?
|
|
11
|
+
4. Is the chosen shard healthy?
|
|
12
|
+
|
|
13
|
+
Pure logic, zero Bun / Elysia surface. Wire `router.route(...)` into whichever
|
|
14
|
+
HTTP/WS layer you have (Bun.serve, Elysia, native node:http, anything that can
|
|
15
|
+
return a 503). An Elysia adapter ships in a later 0.0.x as a subpath.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createRouter } from '@absolutejs/router';
|
|
19
|
+
|
|
20
|
+
const router = createRouter({
|
|
21
|
+
shards: [
|
|
22
|
+
{ id: 'engine-1', url: 'ws://10.0.0.11:3000' },
|
|
23
|
+
{ id: 'engine-2', url: 'ws://10.0.0.12:3000' },
|
|
24
|
+
],
|
|
25
|
+
hashStrategy: 'jump',
|
|
26
|
+
perTenantConnectionCap: 100,
|
|
27
|
+
perTenantRateLimit: { tokens: 100, refillPerSecond: 10 },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// In your WS upgrade handler:
|
|
31
|
+
const decision = router.route({ tenantId, channelId });
|
|
32
|
+
if (decision.decision !== 'allow') {
|
|
33
|
+
return new Response(decision.decision, { status: 429 });
|
|
34
|
+
}
|
|
35
|
+
const handle = router.acquire(tenantId);
|
|
36
|
+
ws.data = { ...ws.data, release: handle.release, upstream: decision.shard!.url };
|
|
37
|
+
// ...proxy WS frames to decision.shard.url; call handle.release() on close.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## v0.0.1 surface
|
|
41
|
+
|
|
42
|
+
| API | Purpose |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `createRouter(options)` | Factory. Returns a `Router`. |
|
|
45
|
+
| `router.route({ tenantId, channelId? })` | Returns `{ shard, decision }`. Decision is `allow` / `rate-limited` / `capped` / `no-shards`. |
|
|
46
|
+
| `router.acquire(tenantId)` | Increment a tenant's active-connection counter; returns `{ active, release }`. `release` is idempotent. |
|
|
47
|
+
| `router.markHealthy(id)` / `router.markUnhealthy(id)` | Caller-driven health state. Unhealthy shards are skipped; tenants on them rehash to healthy ones. |
|
|
48
|
+
| `router.addShard(shard)` / `router.removeShard(id)` | Runtime shard membership changes. |
|
|
49
|
+
| `router.shards()` / `router.snapshot()` | Inspection. |
|
|
50
|
+
| `router.dispose()` | Stop accepting routes; all subsequent `route()` returns `no-shards`. |
|
|
51
|
+
|
|
52
|
+
### Hash strategies
|
|
53
|
+
|
|
54
|
+
- **`jump`** (default) — Lamping & Veach 2014. O(log n) with no memory, exactly
|
|
55
|
+
1/N keys move when shards are added at the tail. Ignores `weight`.
|
|
56
|
+
- **`rendezvous`** — HRW hash. Supports per-shard `weight` for heterogeneous
|
|
57
|
+
engine sizes. O(N) per lookup.
|
|
58
|
+
- **Custom**: pass `(key, shards) => index`.
|
|
59
|
+
|
|
60
|
+
### Connection cap
|
|
61
|
+
|
|
62
|
+
`perTenantConnectionCap` is the max concurrent connections one tenant can hold,
|
|
63
|
+
counted via `acquire()` / the returned `release()`. When reached, `route()`
|
|
64
|
+
returns `capped` — your gateway should refuse the upgrade with `429` /
|
|
65
|
+
`503`. Default `Infinity` (no cap).
|
|
66
|
+
|
|
67
|
+
### Rate limit
|
|
68
|
+
|
|
69
|
+
`perTenantRateLimit` is a token bucket per tenant: `tokens` is bucket capacity
|
|
70
|
+
AND starting balance; `refillPerSecond` continuously refills up to capacity.
|
|
71
|
+
Each successful `route()` costs one token. Bucket is computed lazily at lookup
|
|
72
|
+
time — no timer churn for idle tenants. Default `{ tokens: Infinity,
|
|
73
|
+
refillPerSecond: 0 }` (no limit).
|
|
74
|
+
|
|
75
|
+
### Health
|
|
76
|
+
|
|
77
|
+
The router does not probe backends itself — keeping it bun/elysia-free means
|
|
78
|
+
no I/O. Wire your own health-check loop and call `markHealthy` / `markUnhealthy`.
|
|
79
|
+
A live health-checking adapter is a candidate for a later 0.0.x subpath.
|
|
80
|
+
|
|
81
|
+
## Architectural role
|
|
82
|
+
|
|
83
|
+
- **`@absolutejs/sync`** — the engine each backend shard runs.
|
|
84
|
+
- **`@absolutejs/runtime`** — the process pool each backend shard spawns from.
|
|
85
|
+
- **`@absolutejs/metering`** — counts the bill; `meter.allow(tenant)` reads.
|
|
86
|
+
- **`@absolutejs/router`** — *this library*. The edge decision before traffic reaches a shard. `meter.allow()` can be wired into the gateway alongside `router.route()` to refuse over-quota tenants without paying for the upstream hop.
|
|
87
|
+
|
|
88
|
+
## What v0.0.1 does NOT include
|
|
89
|
+
|
|
90
|
+
- The actual WS proxy implementation. Caller wires `Bun.serve` (or any HTTP/WS layer) to `router.route()` and forwards bytes themselves.
|
|
91
|
+
- The Elysia adapter (subpath in a later 0.0.x).
|
|
92
|
+
- Distributed router state across multiple edge replicas (v0.2+).
|
|
93
|
+
- Backend health-checking probe loop.
|
|
94
|
+
- TLS / HTTP3 termination.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
BSL 1.1 with a named carveout for the hosted multi-tenant connection routing / WebSocket edge gateway category (Cloudflare Workers WebSockets, Cloudflare Smart Placement, Vercel edge router, Liveblocks' WebSocket fan-out, PartyKit, Ably, Pusher, Soketi). See [LICENSE](./LICENSE). Change Date: 4 years from first release; Change License: Apache 2.0.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/router — multi-tenant connection routing primitive for Bun PaaS
|
|
3
|
+
* gateways.
|
|
4
|
+
*
|
|
5
|
+
* Sits between an incoming request / WS upgrade and N backend processes (each
|
|
6
|
+
* one a `@absolutejs/runtime` instance hosting a `@absolutejs/sync` engine
|
|
7
|
+
* for a subset of tenants). Decides which shard owns the tenant, whether the
|
|
8
|
+
* tenant is over its rate limit, whether the tenant is over its connection
|
|
9
|
+
* cap, and whether the chosen shard is healthy.
|
|
10
|
+
*
|
|
11
|
+
* v0.0.1 is intentionally bun/elysia-agnostic — pure logic, no `Bun.serve`
|
|
12
|
+
* or proxy code. The caller wires the routing decision into whichever HTTP/WS
|
|
13
|
+
* layer they have. An Elysia adapter ships in a later 0.0.x as a subpath.
|
|
14
|
+
*/
|
|
15
|
+
export type Shard = {
|
|
16
|
+
id: string;
|
|
17
|
+
/**
|
|
18
|
+
* Connection target for the chosen backend. Format is opaque to the router
|
|
19
|
+
* — `ws://host:port`, `https://host`, a unix socket path, whatever the
|
|
20
|
+
* caller's proxy layer understands.
|
|
21
|
+
*/
|
|
22
|
+
url: string;
|
|
23
|
+
/**
|
|
24
|
+
* Relative weight for hash strategies that support it (rendezvous). Jump
|
|
25
|
+
* hash ignores weights — every shard is treated as weight 1. Default 1.
|
|
26
|
+
*/
|
|
27
|
+
weight?: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Pure hash-strategy function. Given the routing key + the list of currently
|
|
31
|
+
* healthy shards (with their weights), return the index in `shards` of the
|
|
32
|
+
* chosen shard. The router never calls a strategy with an empty `shards`
|
|
33
|
+
* array — that case is handled upstream as `no-shards`.
|
|
34
|
+
*/
|
|
35
|
+
export type HashStrategyFn = (key: string, shards: ReadonlyArray<Shard>) => number;
|
|
36
|
+
export type HashStrategy = 'jump' | 'rendezvous' | HashStrategyFn;
|
|
37
|
+
/**
|
|
38
|
+
* Per-tenant token-bucket rate limit. `tokens` is the bucket capacity AND the
|
|
39
|
+
* starting balance; `refillPerSecond` is added continuously up to capacity.
|
|
40
|
+
* Defaults: `Infinity` tokens / `0` refill = no limit.
|
|
41
|
+
*/
|
|
42
|
+
export type RateLimit = {
|
|
43
|
+
tokens: number;
|
|
44
|
+
refillPerSecond: number;
|
|
45
|
+
};
|
|
46
|
+
export type RouterOptions = {
|
|
47
|
+
/** Initial shard set. Can be empty (every `route()` returns `no-shards`). */
|
|
48
|
+
shards: Shard[];
|
|
49
|
+
/** Hash strategy. Default `'jump'`. */
|
|
50
|
+
hashStrategy?: HashStrategy;
|
|
51
|
+
/**
|
|
52
|
+
* Max concurrent connections per tenant (counted via `acquire()` /
|
|
53
|
+
* `release()`). Default `Infinity` (no cap). When reached, `route()`
|
|
54
|
+
* returns `'capped'`.
|
|
55
|
+
*/
|
|
56
|
+
perTenantConnectionCap?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Per-tenant token bucket. Default `{ tokens: Infinity, refillPerSecond: 0 }`
|
|
59
|
+
* (no limit). When the tenant's bucket is empty, `route()` returns
|
|
60
|
+
* `'rate-limited'`. One `route()` call costs one token.
|
|
61
|
+
*/
|
|
62
|
+
perTenantRateLimit?: RateLimit;
|
|
63
|
+
/** Override `Date.now` for tests. */
|
|
64
|
+
clock?: () => number;
|
|
65
|
+
};
|
|
66
|
+
export type RouteDecision = 'allow' | 'rate-limited' | 'capped' | 'no-shards';
|
|
67
|
+
export type RouteRequest = {
|
|
68
|
+
tenantId: string;
|
|
69
|
+
/**
|
|
70
|
+
* Optional sub-key within a tenant for shardable channels. When set, the
|
|
71
|
+
* hash key is `${tenantId}:${channelId}`. Use this when a single tenant is
|
|
72
|
+
* too hot for one engine and its work can be partitioned (e.g. per-doc,
|
|
73
|
+
* per-room) across multiple engines. Different channels of the same
|
|
74
|
+
* tenant may land on different shards.
|
|
75
|
+
*/
|
|
76
|
+
channelId?: string;
|
|
77
|
+
};
|
|
78
|
+
export type RouteResult = {
|
|
79
|
+
decision: RouteDecision;
|
|
80
|
+
/** The chosen shard. `null` only when `decision === 'no-shards'`. */
|
|
81
|
+
shard: Shard | null;
|
|
82
|
+
};
|
|
83
|
+
export type AcquireHandle = {
|
|
84
|
+
/** Number of active connections for this tenant after acquire (>=1). */
|
|
85
|
+
active: number;
|
|
86
|
+
/** Releases one connection for this tenant. Idempotent — safe to call once. */
|
|
87
|
+
release: () => void;
|
|
88
|
+
};
|
|
89
|
+
export type RouterSnapshot = {
|
|
90
|
+
shards: Array<Shard & {
|
|
91
|
+
healthy: boolean;
|
|
92
|
+
active: number;
|
|
93
|
+
}>;
|
|
94
|
+
tenants: number;
|
|
95
|
+
};
|
|
96
|
+
export type Router = {
|
|
97
|
+
route: (request: RouteRequest) => RouteResult;
|
|
98
|
+
acquire: (tenantId: string) => AcquireHandle;
|
|
99
|
+
markHealthy: (shardId: string) => void;
|
|
100
|
+
markUnhealthy: (shardId: string) => void;
|
|
101
|
+
addShard: (shard: Shard) => void;
|
|
102
|
+
removeShard: (shardId: string) => void;
|
|
103
|
+
shards: () => Shard[];
|
|
104
|
+
snapshot: () => RouterSnapshot;
|
|
105
|
+
dispose: () => void;
|
|
106
|
+
};
|
|
107
|
+
export declare const createRouter: (options: RouterOptions) => Router;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
var fnv1a32 = (input) => {
|
|
4
|
+
let hash = 2166136261;
|
|
5
|
+
for (let i = 0;i < input.length; i++) {
|
|
6
|
+
hash ^= input.charCodeAt(i);
|
|
7
|
+
hash = Math.imul(hash, 16777619);
|
|
8
|
+
}
|
|
9
|
+
return hash >>> 0;
|
|
10
|
+
};
|
|
11
|
+
var jumpHash = (key, numBuckets) => {
|
|
12
|
+
if (numBuckets <= 0)
|
|
13
|
+
return -1;
|
|
14
|
+
let k = BigInt(fnv1a32(key)) | BigInt(fnv1a32(key + "#hi")) << 32n;
|
|
15
|
+
let b = -1n;
|
|
16
|
+
let j = 0n;
|
|
17
|
+
while (j < BigInt(numBuckets)) {
|
|
18
|
+
b = j;
|
|
19
|
+
k = k * 2862933555777941757n + 1n & 0xffffffffffffffffn;
|
|
20
|
+
const shifted = (k >> 33n) + 1n;
|
|
21
|
+
j = (b + 1n) * (1n << 31n) / shifted;
|
|
22
|
+
}
|
|
23
|
+
return Number(b);
|
|
24
|
+
};
|
|
25
|
+
var jumpStrategy = (key, shards) => jumpHash(key, shards.length);
|
|
26
|
+
var rendezvousStrategy = (key, shards) => {
|
|
27
|
+
let bestIndex = 0;
|
|
28
|
+
let bestScore = -Infinity;
|
|
29
|
+
for (let i = 0;i < shards.length; i++) {
|
|
30
|
+
const shard = shards[i];
|
|
31
|
+
const weight = shard.weight ?? 1;
|
|
32
|
+
if (weight <= 0)
|
|
33
|
+
continue;
|
|
34
|
+
const seed = fnv1a32(`${key}|${shard.id}`);
|
|
35
|
+
const u = (seed + 1) / 4294967296;
|
|
36
|
+
const score = weight * -Math.log(u);
|
|
37
|
+
if (score > bestScore) {
|
|
38
|
+
bestScore = score;
|
|
39
|
+
bestIndex = i;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return bestIndex;
|
|
43
|
+
};
|
|
44
|
+
var resolveStrategy = (strategy) => {
|
|
45
|
+
if (strategy === "jump")
|
|
46
|
+
return jumpStrategy;
|
|
47
|
+
if (strategy === "rendezvous")
|
|
48
|
+
return rendezvousStrategy;
|
|
49
|
+
return strategy;
|
|
50
|
+
};
|
|
51
|
+
var createRouter = (options) => {
|
|
52
|
+
const clock = options.clock ?? Date.now;
|
|
53
|
+
const strategy = resolveStrategy(options.hashStrategy ?? "jump");
|
|
54
|
+
const cap = options.perTenantConnectionCap ?? Infinity;
|
|
55
|
+
const rate = options.perTenantRateLimit ?? { refillPerSecond: 0, tokens: Infinity };
|
|
56
|
+
const shardList = [...options.shards];
|
|
57
|
+
const healthy = new Map;
|
|
58
|
+
for (const shard of shardList)
|
|
59
|
+
healthy.set(shard.id, true);
|
|
60
|
+
const tenants = new Map;
|
|
61
|
+
let disposed = false;
|
|
62
|
+
const freshTenant = (now) => ({
|
|
63
|
+
active: 0,
|
|
64
|
+
lastRefillAt: now,
|
|
65
|
+
tokens: rate.tokens
|
|
66
|
+
});
|
|
67
|
+
const ensureTenant = (id, now) => {
|
|
68
|
+
const found = tenants.get(id);
|
|
69
|
+
if (found)
|
|
70
|
+
return found;
|
|
71
|
+
const fresh = freshTenant(now);
|
|
72
|
+
tenants.set(id, fresh);
|
|
73
|
+
return fresh;
|
|
74
|
+
};
|
|
75
|
+
const refill = (state, now) => {
|
|
76
|
+
if (rate.refillPerSecond <= 0)
|
|
77
|
+
return;
|
|
78
|
+
const elapsedMs = now - state.lastRefillAt;
|
|
79
|
+
if (elapsedMs <= 0)
|
|
80
|
+
return;
|
|
81
|
+
const added = elapsedMs / 1000 * rate.refillPerSecond;
|
|
82
|
+
state.tokens = Math.min(rate.tokens, state.tokens + added);
|
|
83
|
+
state.lastRefillAt = now;
|
|
84
|
+
};
|
|
85
|
+
const healthyShards = () => shardList.filter((shard) => healthy.get(shard.id) === true);
|
|
86
|
+
const route = (request) => {
|
|
87
|
+
if (disposed)
|
|
88
|
+
return { decision: "no-shards", shard: null };
|
|
89
|
+
const live = healthyShards();
|
|
90
|
+
if (live.length === 0)
|
|
91
|
+
return { decision: "no-shards", shard: null };
|
|
92
|
+
const now = clock();
|
|
93
|
+
const state = ensureTenant(request.tenantId, now);
|
|
94
|
+
if (state.active >= cap) {
|
|
95
|
+
return { decision: "capped", shard: null };
|
|
96
|
+
}
|
|
97
|
+
refill(state, now);
|
|
98
|
+
if (state.tokens < 1) {
|
|
99
|
+
return { decision: "rate-limited", shard: null };
|
|
100
|
+
}
|
|
101
|
+
state.tokens -= 1;
|
|
102
|
+
const key = request.channelId === undefined ? request.tenantId : `${request.tenantId}:${request.channelId}`;
|
|
103
|
+
const index = strategy(key, live);
|
|
104
|
+
const chosen = live[Math.max(0, Math.min(index, live.length - 1))];
|
|
105
|
+
return { decision: "allow", shard: chosen };
|
|
106
|
+
};
|
|
107
|
+
const acquire = (tenantId) => {
|
|
108
|
+
const now = clock();
|
|
109
|
+
const state = ensureTenant(tenantId, now);
|
|
110
|
+
state.active += 1;
|
|
111
|
+
let released = false;
|
|
112
|
+
return {
|
|
113
|
+
active: state.active,
|
|
114
|
+
release: () => {
|
|
115
|
+
if (released)
|
|
116
|
+
return;
|
|
117
|
+
released = true;
|
|
118
|
+
const current = tenants.get(tenantId);
|
|
119
|
+
if (current && current.active > 0)
|
|
120
|
+
current.active -= 1;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
acquire,
|
|
126
|
+
addShard: (shard) => {
|
|
127
|
+
if (shardList.some((existing) => existing.id === shard.id))
|
|
128
|
+
return;
|
|
129
|
+
shardList.push(shard);
|
|
130
|
+
healthy.set(shard.id, true);
|
|
131
|
+
},
|
|
132
|
+
dispose: () => {
|
|
133
|
+
disposed = true;
|
|
134
|
+
shardList.length = 0;
|
|
135
|
+
healthy.clear();
|
|
136
|
+
tenants.clear();
|
|
137
|
+
},
|
|
138
|
+
markHealthy: (id) => {
|
|
139
|
+
if (healthy.has(id))
|
|
140
|
+
healthy.set(id, true);
|
|
141
|
+
},
|
|
142
|
+
markUnhealthy: (id) => {
|
|
143
|
+
if (healthy.has(id))
|
|
144
|
+
healthy.set(id, false);
|
|
145
|
+
},
|
|
146
|
+
removeShard: (id) => {
|
|
147
|
+
const at = shardList.findIndex((shard) => shard.id === id);
|
|
148
|
+
if (at >= 0)
|
|
149
|
+
shardList.splice(at, 1);
|
|
150
|
+
healthy.delete(id);
|
|
151
|
+
},
|
|
152
|
+
route,
|
|
153
|
+
shards: () => shardList.map((shard) => ({ ...shard })),
|
|
154
|
+
snapshot: () => ({
|
|
155
|
+
shards: shardList.map((shard) => ({
|
|
156
|
+
...shard,
|
|
157
|
+
active: 0,
|
|
158
|
+
healthy: healthy.get(shard.id) === true
|
|
159
|
+
})),
|
|
160
|
+
tenants: tenants.size
|
|
161
|
+
})
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
export {
|
|
165
|
+
createRouter
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
//# debugId=E88C919511EA465E64756E2164756E21
|
|
169
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
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, whether the tenant is over its connection\n * cap, and whether the chosen shard is healthy.\n *\n * v0.0.1 is intentionally bun/elysia-agnostic — pure logic, no `Bun.serve`\n * or proxy code. The caller wires the routing decision into whichever HTTP/WS\n * layer they 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 * healthy shards (with their weights), return the index in `shards` of the\n * 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\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/** Override `Date.now` for tests. */\n\tclock?: () => number;\n};\n\nexport type RouteDecision = 'allow' | 'rate-limited' | 'capped' | 'no-shards';\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};\n\nexport type RouteResult = {\n\tdecision: RouteDecision;\n\t/** The chosen shard. `null` only when `decision === 'no-shards'`. */\n\tshard: Shard | null;\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\tshards: Array<Shard & { healthy: boolean; active: number }>;\n\ttenants: number;\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\taddShard: (shard: Shard) => void;\n\tremoveShard: (shardId: string) => void;\n\tshards: () => Shard[];\n\tsnapshot: () => RouterSnapshot;\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. We mix a 32-bit FNV-1a into a\n * 64-bit-ish state via bigint to satisfy the 64-bit arithmetic the algorithm\n * expects.\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\t// (b + 1) * (1 << 31) / ((k >> 33) + 1)\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\n/**\n * Rendezvous (highest-random-weight) hash. For each shard, compute\n * `score = weight * -ln(rand(hash(key + shard.id)))`; pick the shard with\n * the highest score. Properties: supports per-shard weighting, single-shard\n * change moves only that shard's \"winning\" keys, O(N) per lookup.\n */\nconst rendezvousStrategy: 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 weight = shard.weight ?? 1;\n\t\tif (weight <= 0) continue;\n\t\tconst seed = fnv1a32(`${key}|${shard.id}`);\n\t\t// Map [0, 2^32) to (0, 1) and take -ln. Heavier weight = higher expected score.\n\t\tconst u = (seed + 1) / 0x1_0000_0000;\n\t\tconst score = weight * -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): HashStrategyFn => {\n\tif (strategy === 'jump') return jumpStrategy;\n\tif (strategy === 'rendezvous') return rendezvousStrategy;\n\treturn strategy;\n};\n\n// -----------------------------------------------------------------------------\n// Router\n// -----------------------------------------------------------------------------\n\ntype TenantState = {\n\tactive: number;\n\ttokens: number;\n\tlastRefillAt: number;\n};\n\nexport const createRouter = (options: RouterOptions): Router => {\n\tconst clock = options.clock ?? Date.now;\n\tconst strategy = resolveStrategy(options.hashStrategy ?? 'jump');\n\tconst cap = options.perTenantConnectionCap ?? Infinity;\n\tconst rate = options.perTenantRateLimit ?? { refillPerSecond: 0, tokens: Infinity };\n\n\tconst shardList: Shard[] = [...options.shards];\n\tconst healthy = new Map<string, boolean>();\n\tfor (const shard of shardList) healthy.set(shard.id, true);\n\n\tconst tenants = new Map<string, TenantState>();\n\tlet disposed = false;\n\n\tconst freshTenant = (now: number): TenantState => ({\n\t\tactive: 0,\n\t\tlastRefillAt: now,\n\t\ttokens: 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 refill = (state: TenantState, now: number) => {\n\t\tif (rate.refillPerSecond <= 0) return;\n\t\tconst elapsedMs = now - state.lastRefillAt;\n\t\tif (elapsedMs <= 0) return;\n\t\tconst added = (elapsedMs / 1000) * rate.refillPerSecond;\n\t\tstate.tokens = Math.min(rate.tokens, state.tokens + added);\n\t\tstate.lastRefillAt = now;\n\t};\n\n\tconst healthyShards = (): Shard[] => shardList.filter((shard) => healthy.get(shard.id) === true);\n\n\tconst route: Router['route'] = (request) => {\n\t\tif (disposed) return { decision: 'no-shards', shard: null };\n\n\t\tconst live = healthyShards();\n\t\tif (live.length === 0) return { decision: 'no-shards', shard: null };\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\trefill(state, now);\n\t\tif (state.tokens < 1) {\n\t\t\treturn { decision: 'rate-limited', shard: null };\n\t\t}\n\t\tstate.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\ttenants.clear();\n\t\t},\n\t\tmarkHealthy: (id) => {\n\t\t\tif (healthy.has(id)) healthy.set(id, true);\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},\n\t\troute,\n\t\tshards: () => shardList.map((shard) => ({ ...shard })),\n\t\tsnapshot: () => ({\n\t\t\tshards: shardList.map((shard) => ({\n\t\t\t\t...shard,\n\t\t\t\tactive: 0,\n\t\t\t\thealthy: healthy.get(shard.id) === true,\n\t\t\t})),\n\t\t\ttenants: tenants.size,\n\t\t}),\n\t};\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "E88C919511EA465E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/router",
|
|
3
|
+
"version": "0.0.1",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/absolutejs/router.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"license": "BSL-1.1",
|
|
14
|
+
"author": "Alex Kahn",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"absolutejs",
|
|
20
|
+
"bun",
|
|
21
|
+
"paas",
|
|
22
|
+
"multi-tenant",
|
|
23
|
+
"router",
|
|
24
|
+
"consistent-hash",
|
|
25
|
+
"rate-limit",
|
|
26
|
+
"websocket"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
|
|
30
|
+
"test": "bun test tests/",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
33
|
+
"check:package": "bun run typecheck && bun run build && bun run test",
|
|
34
|
+
"release": "bun run format && bun run check:package && bun publish"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"bun-types": "^1.3.14"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "^1.3.14",
|
|
41
|
+
"prettier": "^3.8.3",
|
|
42
|
+
"typescript": "^6.0.3"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"default": "./dist/index.js"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": ["dist", "README.md"]
|
|
52
|
+
}
|