Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# @suaveplan/network-plugin-assignment - Mesh auto-assignment and load balancing: capacity tracker, assignment-scoring engine, rebalance planner, admission controller
## Summary
Mesh auto-assignment and load balancing: live `MeshCapacityTracker`, weighted-multi-criteria `AssignmentEngine`, imbalance-detecting `MeshBalancer` planner, and admission-gating `AdmissionController`.
<!-- begin:auto-docs -->
## Documentation tree
- [balancer](./src/balancer/mesh-balancer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [mesh-balancer](./src/balancer/mesh-balancer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [admission-controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [assignment-engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [mesh-capacity-tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-assignment plugin. Three codes cover the surface: `NO_ELIGIBLE_ME…
<!-- end:auto-docs --> - [mesh-balancer](./src/balancer/mesh-balancer.md) — <!-- begin:auto-docs --> _(no submodules)_- [controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [admission-controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [assignment-engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-capacity-tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-assignment plugin. Three codes cover the surface: `NO_ELIGIBLE_ME…
- [mesh-balancer](./src/balancer/mesh-balancer.md) — <!-- begin:auto-docs --> _(no submodules)_- [controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [admission-controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [assignment-engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-capacity-tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-assignment plugin. Three codes cover the surface: `NO_ELIGIBLE_ME…
- [mesh-balancer](./src/balancer/mesh-balancer.md) — <!-- begin:auto-docs --> _(no submodules)_- [controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [admission-controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [assignment-engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-capacity-tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-assignment plugin. Three codes cover the surface: `NO_ELIGIBLE_ME…
- [mesh-balancer](./src/balancer/mesh-balancer.md) — <!-- begin:auto-docs --> _(no submodules)_- [controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_ - [admission-controller](./src/controller/admission-controller.md) — <!-- begin:auto-docs --> _(no submodules)_- [engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [assignment-engine](./src/engine/assignment-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_ - [mesh-capacity-tracker](./src/tracker/mesh-capacity-tracker.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-assignment plugin. Three codes cover the surface: `NO_ELIGIBLE_ME…

View File

@@ -0,0 +1,50 @@
{
"name": "@suaveplan/network-plugin-assignment",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Mesh auto-assignment and load balancing: capacity tracker, assignment-scoring engine, rebalance planner, admission controller",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "src", "README.md"],
"exports": {
"./package.json": "./package.json",
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
}
},
"scripts": {
"build": "vite build",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "bunx biome check src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@opentelemetry/api": "catalog:observability",
"@suaveplan/error": "workspace:^",
"@suaveplan/network": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
}
}

View File

@@ -0,0 +1,114 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { MeshCapacityTracker } from "../tracker/mesh-capacity-tracker.js";
import { MeshBalancer } from "./mesh-balancer.js";
describe("MeshBalancer", () => {
it("computeImbalance is 0 when fewer than 2 meshes", () => {
const t = new MeshCapacityTracker();
const b = new MeshBalancer({ tracker: t });
expect(b.computeImbalance()).toBe(0);
t.recordMesh("mesh-A");
expect(b.computeImbalance()).toBe(0);
});
it("computeImbalance reflects max-min spread", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10);
t.recordMesh("mesh-B", 10);
for (let i = 0; i < 8; i++) t.recordJoin("mesh-A", `p${i}`);
const b = new MeshBalancer({ tracker: t });
expect(b.computeImbalance()).toBeCloseTo(0.8, 5);
});
it("computeImbalance handles 3+ meshes", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10);
t.recordMesh("mesh-B", 10);
t.recordMesh("mesh-C", 10);
for (let i = 0; i < 9; i++) t.recordJoin("mesh-A", `pA${i}`);
for (let i = 0; i < 5; i++) t.recordJoin("mesh-B", `pB${i}`);
const b = new MeshBalancer({ tracker: t });
// max=0.9, min=0 (mesh-C empty), mesh-B in the middle
expect(b.computeImbalance()).toBeCloseTo(0.9, 5);
});
it("computeImbalance handles ascending-utilisation iteration", () => {
// Insertion order [empty, half, full] forces the loop to hit the
// `u < min` *false* branch on the second and third iterations.
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10); // 0/10 — least full
t.recordMesh("mesh-B", 10); // 5/10
t.recordMesh("mesh-C", 10); // 9/10 — most full
for (let i = 0; i < 5; i++) t.recordJoin("mesh-B", `pB${i}`);
for (let i = 0; i < 9; i++) t.recordJoin("mesh-C", `pC${i}`);
const b = new MeshBalancer({ tracker: t });
expect(b.computeImbalance()).toBeCloseTo(0.9, 5);
});
it("returns empty plan when imbalance is below threshold", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10);
t.recordMesh("mesh-B", 10);
t.recordJoin("mesh-A", "p1");
const b = new MeshBalancer({
tracker: t,
imbalanceThreshold: 0.5,
});
const plan = b.buildPlan(() => []);
expect(plan.moves).toEqual([]);
expect(plan.imbalanceBefore).toBe(plan.imbalanceAfter);
});
it("proposes moves when imbalance exceeds threshold", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10);
t.recordMesh("mesh-B", 10);
for (let i = 0; i < 8; i++) t.recordJoin("mesh-A", `p${i}`);
const b = new MeshBalancer({
tracker: t,
imbalanceThreshold: 0.3,
maxMovesPerPlan: 3,
});
const plan = b.buildPlan((m) => {
expect(m).toBe("mesh-A");
return ["p0", "p1", "p2", "p3", "p4"];
});
expect(plan.moves).toHaveLength(3);
for (const move of plan.moves) {
expect(move.fromMeshId).toBe("mesh-A");
expect(move.toMeshId).toBe("mesh-B");
expect(move.reason).toBe("rebalance");
}
expect(plan.imbalanceAfter).toBeLessThan(plan.imbalanceBefore);
});
it("uses default thresholds when not configured", () => {
const t = new MeshCapacityTracker();
const b = new MeshBalancer({ tracker: t });
expect(b.computeImbalance()).toBe(0);
});
it("estimateAfter returns 0 when one mesh reports zero max", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10);
t.recordMesh("mesh-B", 1);
for (let i = 0; i < 8; i++) t.recordJoin("mesh-A", `p${i}`);
const fakeTracker = Object.create(t) as MeshCapacityTracker;
const originalGetMax = t.getMaxPeers.bind(t);
fakeTracker.getMaxPeers = (mesh: string) =>
mesh === "mesh-B" ? 0 : originalGetMax(mesh);
const balancer = new MeshBalancer({
tracker: fakeTracker,
imbalanceThreshold: 0.3,
maxMovesPerPlan: 1,
});
const plan = balancer.buildPlan(() => ["p0"]);
expect(plan.imbalanceAfter).toBe(0);
});
it("returns 0 imbalance when both meshes have 0 max", () => {
const t = new MeshCapacityTracker();
const b = new MeshBalancer({ tracker: t });
expect(b.computeImbalance()).toBe(0);
});
});

View File

@@ -0,0 +1,78 @@
# MeshBalancer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Detects mesh imbalances and proposes rebalance moves. The plan is advisory — the consumer is responsible for actually executing each move via `network-plugin-migration`. Imbalance is measured as the spread between the highest- and lowest-utilised meshes; below `imbalanceThreshold` the plan is empty.
## Features
- `computeImbalance()` returns the current `[0, 1]` spread between the busiest and least-busy meshes.
- `buildPlan(peerSelector)` returns a structured `RebalancePlan` with capped move count.
- Configurable `imbalanceThreshold` (default `0.3`) and `maxMovesPerPlan` (default `10`).
- Pluggable `peerSelector` — the consumer decides which peers can move from the overloaded mesh.
- Plan includes both pre and post-move imbalance estimates for observability.
## Usage: Basic Example
```ts
import {
MeshBalancer,
MeshCapacityTracker,
} from "@suaveplan/network-plugin-assignment";
const tracker = new MeshCapacityTracker();
const balancer = new MeshBalancer({ tracker });
const plan = balancer.buildPlan((overloadedMeshId) => [
"alice",
"bob",
]);
for (const move of plan.moves) {
await migrationCoordinator.initiateMigration({
peerId: move.peerId,
fromMeshId: move.fromMeshId,
toMeshId: move.toMeshId,
reason: move.reason,
priority: "graceful",
});
}
```
## Usage: Advanced Example
```ts
import {
MeshBalancer,
MeshCapacityTracker,
} from "@suaveplan/network-plugin-assignment";
const balancer = new MeshBalancer({
tracker,
imbalanceThreshold: 0.5,
maxMovesPerPlan: 3,
});
if (balancer.computeImbalance() >= 0.4) {
const plan = balancer.buildPlan((meshId) =>
registry.peersInMesh(meshId).map((p) => p.peerId)
);
metrics.gauge("mesh.imbalance.before", plan.imbalanceBefore);
metrics.gauge("mesh.imbalance.after", plan.imbalanceAfter);
}
```
## API Reference
- `interface RebalanceMove { peerId, fromMeshId, toMeshId, reason }`.
- `interface RebalancePlan { moves, imbalanceBefore, imbalanceAfter }`.
- `interface MeshBalancerConfig { tracker, imbalanceThreshold?, maxMovesPerPlan? }`.
- `computeImbalance(): number`.
- `buildPlan(peerSelector): RebalancePlan`.
## Implementation Notes
The balancer picks the single most-overloaded mesh and the single least-loaded mesh per plan rather than computing an N-way redistribution; this keeps each plan small enough to execute without congesting the migration plugin and lets repeated plan iterations converge on equilibrium across cycles. The peer selector is a closure rather than a tracked attribute because peer eligibility depends on mesh-membership data we deliberately do not duplicate inside the balancer — that lives in `network-plugin-registry`.

View File

@@ -0,0 +1,120 @@
import type { MeshCapacityTracker } from "../tracker/mesh-capacity-tracker.js";
/**
* One rebalance move proposal.
*/
export interface RebalanceMove {
readonly peerId: string;
readonly fromMeshId: string;
readonly toMeshId: string;
readonly reason: string;
}
/**
* Rebalance plan returned by `MeshBalancer`.
*/
export interface RebalancePlan {
readonly moves: readonly RebalanceMove[];
readonly imbalanceBefore: number;
readonly imbalanceAfter: number;
}
/**
* Configuration for `MeshBalancer`.
*/
export interface MeshBalancerConfig {
readonly tracker: MeshCapacityTracker;
readonly imbalanceThreshold?: number;
readonly maxMovesPerPlan?: number;
}
/**
* Detects mesh imbalances and proposes rebalance moves. The plan is
* advisory — the consumer is responsible for actually executing each
* move via `network-plugin-migration`. Imbalance is measured as the
* spread between the highest- and lowest-utilised meshes.
*/
export class MeshBalancer {
private readonly tracker: MeshCapacityTracker;
private readonly imbalanceThreshold: number;
private readonly maxMovesPerPlan: number;
public constructor(config: MeshBalancerConfig) {
this.tracker = config.tracker;
this.imbalanceThreshold = config.imbalanceThreshold ?? 0.3;
this.maxMovesPerPlan = config.maxMovesPerPlan ?? 10;
}
public computeImbalance(): number {
const meshes = this.tracker.listMeshes();
if (meshes.length < 2) return 0;
let min = 1;
let max = 0;
for (const m of meshes) {
const u = this.tracker.getUtilization(m);
if (u < min) min = u;
if (u > max) max = u;
}
return Math.max(0, max - min);
}
public buildPlan(
peerSelector: (overloadedMeshId: string) => readonly string[]
): RebalancePlan {
const before = this.computeImbalance();
if (before < this.imbalanceThreshold) {
return {
moves: [],
imbalanceBefore: before,
imbalanceAfter: before,
};
}
const utilisation = this.tracker
.listMeshes()
.map((meshId) => ({
meshId,
util: this.tracker.getUtilization(meshId),
}))
.sort((a, b) => b.util - a.util);
const overloaded = utilisation[0]!;
const underloaded = utilisation[utilisation.length - 1]!;
const moves: RebalanceMove[] = [];
const peersToMove = peerSelector(overloaded.meshId).slice(
0,
this.maxMovesPerPlan
);
for (const peerId of peersToMove) {
moves.push({
peerId,
fromMeshId: overloaded.meshId,
toMeshId: underloaded.meshId,
reason: "rebalance",
});
}
return {
moves,
imbalanceBefore: before,
imbalanceAfter: this.estimateAfter(
overloaded.meshId,
underloaded.meshId,
moves.length
),
};
}
private estimateAfter(
fromMeshId: string,
toMeshId: string,
moveCount: number
): number {
const fromMax = this.tracker.getMaxPeers(fromMeshId);
const toMax = this.tracker.getMaxPeers(toMeshId);
if (fromMax === 0 || toMax === 0) return 0;
const fromAfter =
(this.tracker.getUtilization(fromMeshId) * fromMax - moveCount) /
fromMax;
const toAfter =
(this.tracker.getUtilization(toMeshId) * toMax + moveCount) / toMax;
return Math.abs(toAfter - fromAfter);
}
}

View File

@@ -0,0 +1,89 @@
# AdmissionController
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Gatekeeper that runs the `AssignmentEngine`, checks capacity against the `MeshCapacityTracker`, applies a pluggable `AdmissionPolicy`, and either records the placement or throws a typed error. Centralising admission here means there's one entry point for "place this peer somewhere" and the engine, balancer, and observability all see the same outcome.
## Features
- Pluggable `AdmissionPolicy` for non-capacity gates (capability mismatch, regional rules, operator quotas).
- Mesh enumeration via injected `meshes()` callback so the controller never has to keep a stale local copy.
- Three failure modes with distinct codes: `ADMISSION_DENIED` (policy), `NO_ELIGIBLE_MESH` (engine), `MESH_FULL` (tracker race).
- `release(peer, meshId)` for clean disconnect semantics.
- On success, records the join into the tracker so the next call sees the updated capacity.
## Usage: Basic Example
```ts
import {
AdmissionController,
AssignmentEngine,
MeshCapacityTracker,
} from "@suaveplan/network-plugin-assignment";
const tracker = new MeshCapacityTracker();
const engine = new AssignmentEngine({ tracker });
const controller = new AdmissionController({
engine,
tracker,
meshes: () => [
{ meshId: "mesh-A", tierFloor: 0 },
{ meshId: "mesh-B", tierFloor: 0 },
],
});
const result = controller.admit(peer);
```
## Usage: Advanced Example
```ts
import {
ASSIGN_CODES,
AdmissionController,
AssignError,
} from "@suaveplan/network-plugin-assignment";
const controller = new AdmissionController({
engine,
tracker,
meshes: () => fleetSnapshot,
policy: (peer, mesh) => {
if (peer.capabilities === "CONSTRAINED" && mesh.tierFloor < 2) {
return false;
}
return true;
},
});
try {
const placement = controller.admit(peer);
bus.emit("peer:placed", placement);
} catch (err) {
if (err instanceof AssignError) {
if (err.code === ASSIGN_CODES.MESH_FULL) {
await rebalanceAndRetry(peer);
return;
}
if (err.code === ASSIGN_CODES.ADMISSION_DENIED) {
return enterStandby(peer);
}
}
throw err;
}
```
## API Reference
- `type AdmissionPolicy = (peer, descriptor) => boolean`.
- `interface AdmissionControllerConfig { engine, tracker, meshes, policy? }`.
- `interface AdmissionResult { peerId, meshId, score }`.
- `admit(peer): AdmissionResult` — throws `ADMISSION_DENIED` / `NO_ELIGIBLE_MESH` / `MESH_FULL`.
- `release(peer, meshId)` — updates the tracker.
## Implementation Notes
`admit` does the policy check _before_ calling the engine so that policy violations are detected before we burn cycles scoring meshes the peer can't join anyway. The capacity check happens _after_ the engine picks a winner, because another concurrent admission could race the available-slots count down to zero between when the engine scored and when we record. Callers that need stricter ordering should serialize admissions through their own queue.

View File

@@ -0,0 +1,85 @@
import type {
AssignmentEngine,
MeshDescriptor,
PeerInfo,
} from "../engine/assignment-engine.js";
import { ASSIGN_CODES, AssignError } from "../error-codes.js";
import type { MeshCapacityTracker } from "../tracker/mesh-capacity-tracker.js";
/**
* Function that vets a peer for non-capacity reasons (capability
* mismatch, regional policy, etc.).
*/
export type AdmissionPolicy = (
peer: PeerInfo,
descriptor: MeshDescriptor
) => boolean;
/**
* Configuration for `AdmissionController`.
*/
export interface AdmissionControllerConfig {
readonly engine: AssignmentEngine;
readonly tracker: MeshCapacityTracker;
readonly meshes: () => readonly MeshDescriptor[];
readonly policy?: AdmissionPolicy;
}
/**
* Result of a successful admission.
*/
export interface AdmissionResult {
readonly peerId: string;
readonly meshId: string;
readonly score: number;
}
/**
* Gatekeeper that runs the engine, checks capacity, applies policy, and
* either records the placement (via the tracker) or throws a typed
* error.
*/
export class AdmissionController {
private readonly config: AdmissionControllerConfig;
private readonly policy: AdmissionPolicy;
public constructor(config: AdmissionControllerConfig) {
this.config = config;
this.policy = config.policy ?? (() => true);
}
public admit(peer: PeerInfo): AdmissionResult {
const descriptors = this.config.meshes();
const eligible = descriptors.filter((d) => this.policy(peer, d));
if (eligible.length === 0) {
throw new AssignError(
ASSIGN_CODES.ADMISSION_DENIED,
"admission.denied-by-policy",
{ context: { peerId: peer.peerId } }
);
}
const best = this.config.engine.bestFor(peer, eligible);
if (this.config.tracker.getAvailableSlots(best.meshId) <= 0) {
throw new AssignError(
ASSIGN_CODES.MESH_FULL,
"admission.mesh-full",
{
context: {
peerId: peer.peerId,
meshId: best.meshId,
},
}
);
}
this.config.tracker.recordJoin(best.meshId, peer.peerId);
return {
peerId: peer.peerId,
meshId: best.meshId,
score: best.score,
};
}
public release(peer: PeerInfo, meshId: string): void {
this.config.tracker.recordLeave(meshId, peer.peerId);
}
}

View File

@@ -0,0 +1,108 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
AssignmentEngine,
type MeshDescriptor,
type PeerInfo,
} from "../engine/assignment-engine.js";
import { MeshCapacityTracker } from "../tracker/mesh-capacity-tracker.js";
import { AdmissionController } from "./admission-controller.js";
const peer: PeerInfo = {
peerId: "alice",
latencyMap: new Map([
["mesh-A", 50],
["mesh-B", 200],
]),
region: "us-east",
tierEligibility: 1,
capabilities: "STANDARD",
};
function setup(
opts: {
meshes?: MeshDescriptor[];
capA?: number;
fillA?: number;
policy?: (p: PeerInfo, m: MeshDescriptor) => boolean;
} = {}
) {
const tracker = new MeshCapacityTracker();
tracker.recordMesh("mesh-A", opts.capA ?? 10);
tracker.recordMesh("mesh-B", 10);
for (let i = 0; i < (opts.fillA ?? 0); i++) {
tracker.recordJoin("mesh-A", `p${i}`);
}
const engine = new AssignmentEngine({ tracker });
const meshes =
opts.meshes ??
([
{ meshId: "mesh-A", region: "us-east", tierFloor: 0 },
{ meshId: "mesh-B", region: "us-west", tierFloor: 0 },
] as MeshDescriptor[]);
const controller = new AdmissionController({
engine,
tracker,
meshes: () => meshes,
policy: opts.policy,
});
return { tracker, controller };
}
describe("AdmissionController", () => {
it("admits a peer to the best mesh and records the join", () => {
const { tracker, controller } = setup();
const result = controller.admit(peer);
expect(result.meshId).toBe("mesh-A");
expect(tracker.getUtilization("mesh-A")).toBe(0.1);
});
it("rejects when policy denies every mesh", () => {
const { controller } = setup({ policy: () => false });
expect(() => controller.admit(peer)).toThrow(
"admission.denied-by-policy"
);
});
it("rejects when chosen mesh is at capacity", () => {
// restrict policy to mesh-A so the engine can't pick mesh-B,
// then fill mesh-A so admit hits MESH_FULL deterministically.
const { controller } = setup({
capA: 1,
fillA: 1,
policy: (_p, m) => m.meshId === "mesh-A",
});
expect(() => controller.admit(peer)).toThrow("admission.mesh-full");
});
it("propagates NO_ELIGIBLE_MESH from the engine", () => {
const { controller } = setup({
meshes: [
{ meshId: "mesh-A", tierFloor: 99 },
{ meshId: "mesh-B", tierFloor: 99 },
],
});
expect(() => controller.admit(peer)).toThrow(
"assignment.no-eligible-mesh"
);
});
it("release decrements the tracker", () => {
const { tracker, controller } = setup();
const result = controller.admit(peer);
controller.release(peer, result.meshId);
expect(tracker.getUtilization(result.meshId)).toBe(0);
});
it("default policy admits all", () => {
const { controller } = setup();
expect(() => controller.admit(peer)).not.toThrow();
});
it("policy can filter per-mesh and admission picks survivors", () => {
const { controller } = setup({
policy: (_p, m) => m.meshId === "mesh-B",
});
const result = controller.admit(peer);
expect(result.meshId).toBe("mesh-B");
});
});

View File

@@ -0,0 +1,93 @@
# AssignmentEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Scores candidate meshes for a placement decision along five axes (latency, capacity, load, region, tier-fit), combines them into a composite score, and returns the best mesh that still has room. Decoupling the scoring from the admission and balancer means consumers can run "what-if" scoring without committing to a placement, and the rebalancer can use the same scoring to evaluate moves.
## Features
- Five scoring axes, each normalised to `[0, 1]`.
- Configurable `AssignmentWeights` (defaults `0.35 / 0.25 / 0.2 / 0.1 / 0.1`).
- Configurable `minScoreThreshold` for "this mesh is good enough" decisions.
- Tier-fit pre-filter: peers below `tierFloor` are excluded from scoring entirely.
- Deterministic lex-tiebreak on `meshId` so independent observers reach the same winner.
- Throws `NO_ELIGIBLE_MESH` for empty candidate sets and below-threshold scores.
## Usage: Basic Example
```ts
import {
AssignmentEngine,
MeshCapacityTracker,
} from "@suaveplan/network-plugin-assignment";
const tracker = new MeshCapacityTracker();
tracker.recordMesh("mesh-A", 32);
tracker.recordMesh("mesh-B", 32);
const engine = new AssignmentEngine({ tracker });
const best = engine.bestFor(
{
peerId: "alice",
latencyMap: new Map([
["mesh-A", 50],
["mesh-B", 200],
]),
tierEligibility: 1,
capabilities: "STANDARD",
},
[
{ meshId: "mesh-A", tierFloor: 0 },
{ meshId: "mesh-B", tierFloor: 0 },
]
);
```
## Usage: Advanced Example
```ts
import {
AssignmentEngine,
MeshCapacityTracker,
} from "@suaveplan/network-plugin-assignment";
const engine = new AssignmentEngine({
tracker,
weights: {
latency: 0.5,
capacity: 0.3,
load: 0.1,
region: 0.1,
tierFit: 0,
},
minScoreThreshold: 0.4,
});
const scored = engine.score(peer, candidates);
for (const s of scored) {
console.log(s.meshId, s.score, s.factors);
}
try {
const best = engine.bestFor(peer, candidates);
} catch (err) {
// handle NO_ELIGIBLE_MESH
}
```
## API Reference
- `interface PeerInfo { peerId, latencyMap, region?, tierEligibility, capabilities }`.
- `interface MeshDescriptor { meshId, region?, tierFloor }`.
- `interface AssignmentWeights { latency, capacity, load, region, tierFit }`.
- `interface AssignmentScore { meshId, score, factors }`.
- `interface AssignmentEngineConfig { tracker, weights?, minScoreThreshold? }`.
- `score(peer, candidates): readonly AssignmentScore[]`.
- `bestFor(peer, candidates): AssignmentScore` — throws `NO_ELIGIBLE_MESH` on empty / below-threshold.
## Implementation Notes
The latency axis uses `1 / (1 + ms)` rather than min-max normalisation so a peer that has only one latency reading still produces a sensible score (no degenerate divide-by-zero). Capacity and load are deliberately separate axes — capacity is "how many free slots" while load is "how full"; they correlate but a tiny mesh and a huge mesh at the same utilisation should rank differently for placement, which the dual-axis lets us tune. Tier-fit is enforced as a _filter_ before scoring, then re-asserted as a binary axis, so we never score a peer against a mesh it cannot reach.

View File

@@ -0,0 +1,160 @@
import { ASSIGN_CODES, AssignError } from "../error-codes.js";
import type { MeshCapacityTracker } from "../tracker/mesh-capacity-tracker.js";
/**
* Information about a peer requesting placement.
*/
export interface PeerInfo {
readonly peerId: string;
readonly latencyMap: ReadonlyMap<string, number>;
readonly region?: string;
readonly tierEligibility: number;
readonly capabilities: "CONSTRAINED" | "STANDARD" | "POWERFUL" | "SERVER";
}
/**
* Per-mesh metadata supplied to the engine.
*/
export interface MeshDescriptor {
readonly meshId: string;
readonly region?: string;
readonly tierFloor: number;
}
/**
* Weight of each scoring axis. Must be non-negative.
*/
export interface AssignmentWeights {
readonly latency: number;
readonly capacity: number;
readonly load: number;
readonly region: number;
readonly tierFit: number;
}
/**
* Score breakdown for one candidate mesh.
*/
export interface AssignmentScore {
readonly meshId: string;
readonly score: number;
readonly factors: {
readonly latency: number;
readonly capacity: number;
readonly load: number;
readonly region: number;
readonly tierFit: number;
};
}
/**
* Configuration for `AssignmentEngine`.
*/
export interface AssignmentEngineConfig {
readonly tracker: MeshCapacityTracker;
readonly weights?: AssignmentWeights;
readonly minScoreThreshold?: number;
}
const DEFAULT_WEIGHTS: AssignmentWeights = {
latency: 0.35,
capacity: 0.25,
load: 0.2,
region: 0.1,
tierFit: 0.1,
};
/**
* Scores candidate meshes for a peer along five axes:
*
* - `latency` — lower RTT wins (`1 / (1 + ms)`).
* - `capacity` — fraction of free slots.
* - `load` — `1 - utilization`.
* - `region` — `1` if regions match, else `0`.
* - `tierFit` — `1` if peer is eligible for the mesh's tier floor, else `0`.
*
* Returns the highest-scoring mesh that still has free capacity, or
* throws `NO_ELIGIBLE_MESH` if every candidate scores below the
* threshold.
*/
export class AssignmentEngine {
private readonly tracker: MeshCapacityTracker;
private readonly weights: AssignmentWeights;
private readonly minScoreThreshold: number;
public constructor(config: AssignmentEngineConfig) {
this.tracker = config.tracker;
this.weights = config.weights ?? DEFAULT_WEIGHTS;
this.minScoreThreshold = config.minScoreThreshold ?? 0;
}
public score(
peer: PeerInfo,
candidates: readonly MeshDescriptor[]
): readonly AssignmentScore[] {
return candidates
.filter((m) => peer.tierEligibility >= m.tierFloor)
.map((m) => {
const latencyMs = peer.latencyMap.get(m.meshId);
const latency =
latencyMs === undefined ? 0 : 1 / (1 + latencyMs);
const slots = this.tracker.getAvailableSlots(m.meshId);
const max = this.tracker.getMaxPeers(m.meshId) || 1;
const capacity = slots / max;
const load = 1 - this.tracker.getUtilization(m.meshId);
const region =
m.region !== undefined &&
peer.region !== undefined &&
m.region === peer.region
? 1
: 0;
// tierEligibility is always >= tierFloor here because the
// outer .filter() pre-screens ineligible candidates.
const tierFit = 1;
const composite =
this.weights.latency * latency +
this.weights.capacity * capacity +
this.weights.load * load +
this.weights.region * region +
this.weights.tierFit * tierFit;
return {
meshId: m.meshId,
score: composite,
factors: { latency, capacity, load, region, tierFit },
};
});
}
public bestFor(
peer: PeerInfo,
candidates: readonly MeshDescriptor[]
): AssignmentScore {
const scored = this.score(peer, candidates);
if (scored.length === 0) {
throw new AssignError(
ASSIGN_CODES.NO_ELIGIBLE_MESH,
"assignment.no-eligible-mesh",
{ context: { peerId: peer.peerId } }
);
}
const sorted = [...scored].sort((a, b) => {
if (a.score !== b.score) return b.score - a.score;
return a.meshId.localeCompare(b.meshId);
});
const best = sorted[0]!;
if (best.score < this.minScoreThreshold) {
throw new AssignError(
ASSIGN_CODES.NO_ELIGIBLE_MESH,
"assignment.below-threshold",
{
context: {
peerId: peer.peerId,
bestScore: best.score,
threshold: this.minScoreThreshold,
},
}
);
}
return best;
}
}

View File

@@ -0,0 +1,141 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { MeshCapacityTracker } from "../tracker/mesh-capacity-tracker.js";
import {
AssignmentEngine,
type MeshDescriptor,
type PeerInfo,
} from "./assignment-engine.js";
function setupTracker(): MeshCapacityTracker {
const tracker = new MeshCapacityTracker();
tracker.recordMesh("mesh-A", 10);
tracker.recordMesh("mesh-B", 10);
return tracker;
}
const candidates: MeshDescriptor[] = [
{ meshId: "mesh-A", region: "us-east", tierFloor: 0 },
{ meshId: "mesh-B", region: "us-west", tierFloor: 0 },
];
const peer: PeerInfo = {
peerId: "alice",
latencyMap: new Map([
["mesh-A", 50],
["mesh-B", 200],
]),
region: "us-east",
tierEligibility: 1,
capabilities: "STANDARD",
};
describe("AssignmentEngine", () => {
it("scores meshes and ranks the closest one first", () => {
const engine = new AssignmentEngine({ tracker: setupTracker() });
const best = engine.bestFor(peer, candidates);
expect(best.meshId).toBe("mesh-A");
expect(best.factors.region).toBe(1);
});
it("filters out ineligible meshes by tier floor", () => {
const engine = new AssignmentEngine({ tracker: setupTracker() });
const tier0Only: MeshDescriptor[] = [
{ meshId: "mesh-A", tierFloor: 5 }, // peer can't reach
{ meshId: "mesh-B", tierFloor: 0 },
];
const best = engine.bestFor(peer, tier0Only);
expect(best.meshId).toBe("mesh-B");
});
it("throws NO_ELIGIBLE_MESH when no candidates remain", () => {
const engine = new AssignmentEngine({ tracker: setupTracker() });
expect(() =>
engine.bestFor(peer, [{ meshId: "mesh-X", tierFloor: 99 }])
).toThrow("assignment.no-eligible-mesh");
});
it("throws NO_ELIGIBLE_MESH below threshold", () => {
const engine = new AssignmentEngine({
tracker: setupTracker(),
minScoreThreshold: 0.99,
});
expect(() => engine.bestFor(peer, candidates)).toThrow(
"assignment.below-threshold"
);
});
it("treats missing latency as zero score on that axis", () => {
const engine = new AssignmentEngine({ tracker: setupTracker() });
const noLatencyPeer: PeerInfo = {
...peer,
latencyMap: new Map(),
};
const scored = engine.score(noLatencyPeer, candidates);
for (const s of scored) {
expect(s.factors.latency).toBe(0);
}
});
it("region axis is 0 when peer has no region", () => {
const engine = new AssignmentEngine({ tracker: setupTracker() });
const noRegionPeer: PeerInfo = { ...peer, region: undefined };
const scored = engine.score(noRegionPeer, candidates);
for (const s of scored) {
expect(s.factors.region).toBe(0);
}
});
it("region axis is 0 when descriptor has no region", () => {
const engine = new AssignmentEngine({ tracker: setupTracker() });
const noRegionMeshes: MeshDescriptor[] = [
{ meshId: "mesh-A", tierFloor: 0 },
{ meshId: "mesh-B", tierFloor: 0 },
];
const scored = engine.score(peer, noRegionMeshes);
for (const s of scored) {
expect(s.factors.region).toBe(0);
}
});
it("respects custom weights", () => {
const tracker = setupTracker();
for (let i = 0; i < 9; i++) tracker.recordJoin("mesh-A", `p${i}`);
const customEngine = new AssignmentEngine({
tracker,
weights: {
latency: 0,
capacity: 1,
load: 0,
region: 0,
tierFit: 0,
},
});
const best = customEngine.bestFor(peer, candidates);
expect(best.meshId).toBe("mesh-B");
});
it("lex-tiebreaks on meshId for identical scores", () => {
const tracker = setupTracker();
const engine = new AssignmentEngine({
tracker,
weights: {
latency: 0,
capacity: 0,
load: 0,
region: 0,
tierFit: 1,
},
});
const best = engine.bestFor(peer, candidates);
expect(best.meshId).toBe("mesh-A");
});
it("handles a tracker that reports zero maxPeers", () => {
const tracker = new MeshCapacityTracker();
const engine = new AssignmentEngine({ tracker });
const scored = engine.score(peer, candidates);
for (const s of scored) {
expect(s.factors.capacity).toBe(0);
}
});
});

View File

@@ -0,0 +1,62 @@
# ASSIGN_CODES
## Purpose
Frozen registry of error codes for the network-plugin-assignment plugin. Three codes cover the surface: `NO_ELIGIBLE_MESH` when the engine cannot score any mesh above the threshold, `MESH_FULL` when the chosen mesh's capacity is exhausted at the moment of admission, and `ADMISSION_DENIED` when the admission controller refuses for a non-capacity reason (capability mismatch, regional policy, etc.). The companion `AssignError` extends `SuaveplanError<AssignErrorCode>` so consumers pattern-match on `code` rather than parse messages.
## Features
- Three frozen codes registered under the `NET_ASSIGN` namespace.
- `AssignError extends SuaveplanError<AssignErrorCode>`.
- `name: "AssignError"` on every instance — useful for log filters and dashboard alerts.
- `cause` and `context` propagation via `SuaveplanErrorOptions`.
- Codes are stable across versions; the registry only ever grows, never shrinks.
## Usage: Basic Example
```ts
import {
ASSIGN_CODES,
AssignError,
} from "@suaveplan/network-plugin-assignment";
throw new AssignError(
ASSIGN_CODES.NO_ELIGIBLE_MESH,
"assignment.no-eligible-mesh",
{ context: { peerId: "alice", knownMeshes: 4 } }
);
```
## Usage: Advanced Example
```ts
import {
ASSIGN_CODES,
AssignError,
} from "@suaveplan/network-plugin-assignment";
try {
const meshId = await admissionController.admit(peerInfo);
} catch (err) {
if (err instanceof AssignError) {
if (err.code === ASSIGN_CODES.MESH_FULL) {
await rebalanceAndRetry(peerInfo);
return;
}
if (err.code === ASSIGN_CODES.NO_ELIGIBLE_MESH) {
return enterStandbyMode(peerInfo);
}
}
throw err;
}
```
## API Reference
- `ASSIGN_CODES` — frozen `{ NO_ELIGIBLE_MESH, MESH_FULL, ADMISSION_DENIED }`.
- `type AssignErrorCode` — union of registered code values.
- `class AssignError extends SuaveplanError<AssignErrorCode>` — typed throwable.
## Implementation Notes
`MESH_FULL` is intentionally distinct from `NO_ELIGIBLE_MESH` because they imply different recovery profiles: the former says "the engine picked a mesh but admission lost the race — try again", while the latter says "no mesh in the fleet can host this peer at all — escalate". `ADMISSION_DENIED` covers everything else (capability mismatch, regional policy violation, operator quota cap) so callers don't have to multiplex unrelated codes onto the same handler. The package barrel side-effect-imports `./error-codes.js` so subpath consumers register the codes too.

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { ASSIGN_CODES, AssignError } from "./error-codes.js";
describe("ASSIGN_CODES", () => {
it("registers under NET_ASSIGN", () => {
for (const value of Object.values(ASSIGN_CODES)) {
expect(value).toMatch(/^NET_ASSIGN\./);
}
});
it("is frozen", () => {
expect(Object.isFrozen(ASSIGN_CODES)).toBe(true);
});
it("contains the documented codes", () => {
for (const key of [
"NO_ELIGIBLE_MESH",
"MESH_FULL",
"ADMISSION_DENIED",
] as const) {
expect(ASSIGN_CODES).toHaveProperty(key);
}
});
});
describe("AssignError", () => {
it("extends SuaveplanError", () => {
const err = new AssignError(
ASSIGN_CODES.NO_ELIGIBLE_MESH,
"no-eligible"
);
expect(err.name).toBe("AssignError");
expect(err.code).toBe(ASSIGN_CODES.NO_ELIGIBLE_MESH);
});
it("preserves cause", () => {
const cause = new Error("inner");
const err = new AssignError(ASSIGN_CODES.MESH_FULL, "with-cause", {
cause,
});
expect(err.cause).toBe(cause);
});
});

View File

@@ -0,0 +1,20 @@
import { defineCodes, SuaveplanError } from "@suaveplan/error";
export const ASSIGN_CODES = defineCodes("NET_ASSIGN", [
"NO_ELIGIBLE_MESH",
"MESH_FULL",
"ADMISSION_DENIED",
] as const);
export type AssignErrorCode = (typeof ASSIGN_CODES)[keyof typeof ASSIGN_CODES];
export class AssignError extends SuaveplanError<AssignErrorCode> {
public constructor(
code: AssignErrorCode,
messageKey: string,
options?: import("@suaveplan/error").SuaveplanErrorOptions
) {
super(code, messageKey, options);
this.name = "AssignError";
}
}

View File

@@ -0,0 +1,38 @@
// Error codes (side-effect registration)
import "./error-codes.js";
// Balancer
export {
MeshBalancer,
type MeshBalancerConfig,
type RebalanceMove,
type RebalancePlan,
} from "./balancer/mesh-balancer.js";
// Controller
export {
AdmissionController,
type AdmissionControllerConfig,
type AdmissionPolicy,
type AdmissionResult,
} from "./controller/admission-controller.js";
// Engine
export {
AssignmentEngine,
type AssignmentEngineConfig,
type AssignmentScore,
type AssignmentWeights,
type MeshDescriptor,
type PeerInfo,
} from "./engine/assignment-engine.js";
// Errors
export {
ASSIGN_CODES,
AssignError,
type AssignErrorCode,
} from "./error-codes.js";
// Tracker
export {
type CapacityListener,
MeshCapacityTracker,
type MeshCapacityTrackerConfig,
} from "./tracker/mesh-capacity-tracker.js";

View File

@@ -0,0 +1,59 @@
# MeshCapacityTracker
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Tracks `currentPeers / maxPeers` per mesh in real time. Push-driven: the consumer wires `peer:joined`/`peer:left` from `network-plugin-registry` into `recordJoin`/`recordLeave` and the tracker fires `mesh:full` / `mesh:available` listeners on the threshold transitions. Centralising the capacity view here means the assignment engine, balancer, and admission controller all read the same numbers.
## Features
- `recordMesh(meshId, maxPeers?)` upserts mesh capacity (default `defaultMaxPeers`, configurable; falls back to 64).
- `recordJoin(meshId, peerId)` and `recordLeave(meshId, peerId)` keep the live set per mesh.
- `getUtilization(meshId)` returns the `[0, 1]` ratio.
- `getAvailableSlots(meshId)` for hot-path scheduling.
- Listener-driven `mesh:full` / `mesh:available` transitions; unsubscribe handle returned from `on()`.
- `listMeshes()` and `getMaxPeers()` exposed for observability.
## Usage: Basic Example
```ts
import { MeshCapacityTracker } from "@suaveplan/network-plugin-assignment";
const tracker = new MeshCapacityTracker();
tracker.recordMesh("mesh-A", 32);
tracker.recordJoin("mesh-A", "alice");
tracker.recordJoin("mesh-A", "bob");
const u = tracker.getUtilization("mesh-A"); // 2 / 32 = 0.0625
```
## Usage: Advanced Example
```ts
import { MeshCapacityTracker } from "@suaveplan/network-plugin-assignment";
const tracker = new MeshCapacityTracker({ defaultMaxPeers: 16 });
const off = tracker.on((event, meshId) => {
if (event === "mesh:full") {
rebalancer.scheduleRebalance(meshId);
}
});
bus.on("peer:joined", (e) => tracker.recordJoin(e.meshId, e.peerId));
bus.on("peer:left", (e) => tracker.recordLeave(e.meshId, e.peerId));
```
## API Reference
- `interface MeshCapacityTrackerConfig { defaultMaxPeers? }`.
- `recordMesh(meshId, maxPeers?)` / `recordJoin(meshId, peerId)` / `recordLeave(meshId, peerId)`.
- `getUtilization(meshId): number`.
- `getAvailableSlots(meshId): number`.
- `listMeshes(): readonly string[]`.
- `getMaxPeers(meshId): number`.
- `on(listener) → () => void`.
## Implementation Notes
State is kept in two maps — the per-mesh peer set and a parallel `fullState` boolean — so `recheckFull` can observe a transition by comparing the previous and current values atomically. We only fire listeners on edge transitions (`true → false` or `false → true`), not on every record, to avoid storming subscribers when traffic is steady. `recordLeave` against an unknown mesh is a no-op rather than an error because clean-shutdown ordering can lose `recordMesh` events.

View File

@@ -0,0 +1,122 @@
/**
* Configuration for `MeshCapacityTracker`.
*/
export interface MeshCapacityTrackerConfig {
readonly defaultMaxPeers?: number;
}
/**
* Listener fired when a mesh transitions from "available" to "full" or
* back.
*/
export type CapacityListener = (
event: "mesh:full" | "mesh:available",
meshId: string
) => void;
/**
* Tracks `currentPeers / maxPeers` per mesh in real time. Backed by
* push-style `recordJoin`/`recordLeave` calls — the consumer wires the
* `peer:joined`/`peer:left` events from `network-plugin-registry` into
* the tracker.
*/
export class MeshCapacityTracker {
private readonly defaultMaxPeers: number;
private readonly meshes = new Map<
string,
{ maxPeers: number; currentPeers: Set<string> }
>();
private readonly fullState = new Map<string, boolean>();
private readonly listeners = new Set<CapacityListener>();
public constructor(config: MeshCapacityTrackerConfig = {}) {
this.defaultMaxPeers = config.defaultMaxPeers ?? 64;
}
public recordMesh(meshId: string, maxPeers?: number): void {
const existing = this.meshes.get(meshId);
const cap = maxPeers ?? this.defaultMaxPeers;
if (existing === undefined) {
this.meshes.set(meshId, {
maxPeers: cap,
currentPeers: new Set(),
});
this.fullState.set(meshId, false);
} else {
existing.maxPeers = cap;
this.recheckFull(meshId);
}
}
public recordJoin(meshId: string, peerId: string): void {
const m = this.upsert(meshId);
m.currentPeers.add(peerId);
this.recheckFull(meshId);
}
public recordLeave(meshId: string, peerId: string): void {
const m = this.meshes.get(meshId);
if (m === undefined) return;
m.currentPeers.delete(peerId);
this.recheckFull(meshId);
}
public getUtilization(meshId: string): number {
const m = this.meshes.get(meshId);
if (m === undefined) return 0;
return m.currentPeers.size / m.maxPeers;
}
public getAvailableSlots(meshId: string): number {
const m = this.meshes.get(meshId);
if (m === undefined) return 0;
return Math.max(0, m.maxPeers - m.currentPeers.size);
}
public listMeshes(): readonly string[] {
return [...this.meshes.keys()];
}
public getMaxPeers(meshId: string): number {
return this.meshes.get(meshId)?.maxPeers ?? 0;
}
public on(listener: CapacityListener): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
private upsert(meshId: string): {
maxPeers: number;
currentPeers: Set<string>;
} {
const existing = this.meshes.get(meshId);
if (existing !== undefined) return existing;
const m = {
maxPeers: this.defaultMaxPeers,
currentPeers: new Set<string>(),
};
this.meshes.set(meshId, m);
this.fullState.set(meshId, false);
return m;
}
private recheckFull(meshId: string): void {
const m = this.meshes.get(meshId)!;
const wasFull = this.fullState.get(meshId) === true;
const isFull = m.currentPeers.size >= m.maxPeers;
if (isFull && !wasFull) {
this.fullState.set(meshId, true);
this.fire("mesh:full", meshId);
} else if (!isFull && wasFull) {
this.fullState.set(meshId, false);
this.fire("mesh:available", meshId);
}
}
private fire(event: "mesh:full" | "mesh:available", meshId: string): void {
for (const l of this.listeners) l(event, meshId);
}
}

View File

@@ -0,0 +1,90 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { MeshCapacityTracker } from "./mesh-capacity-tracker.js";
describe("MeshCapacityTracker", () => {
it("records mesh, joins and tracks utilization", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 10);
t.recordJoin("mesh-A", "alice");
t.recordJoin("mesh-A", "bob");
expect(t.getUtilization("mesh-A")).toBe(0.2);
expect(t.getAvailableSlots("mesh-A")).toBe(8);
expect(t.getMaxPeers("mesh-A")).toBe(10);
});
it("returns zero utilization for unknown meshes", () => {
const t = new MeshCapacityTracker();
expect(t.getUtilization("ghost")).toBe(0);
expect(t.getAvailableSlots("ghost")).toBe(0);
expect(t.getMaxPeers("ghost")).toBe(0);
});
it("autocreates mesh when join arrives first", () => {
const t = new MeshCapacityTracker({ defaultMaxPeers: 5 });
t.recordJoin("mesh-X", "alice");
expect(t.getMaxPeers("mesh-X")).toBe(5);
});
it("recordLeave decrements and unknown meshes are no-op", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 5);
t.recordJoin("mesh-A", "alice");
t.recordLeave("mesh-A", "alice");
t.recordLeave("ghost", "ghost");
expect(t.getUtilization("mesh-A")).toBe(0);
});
it("fires mesh:full / mesh:available transitions", () => {
const events: string[] = [];
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 2);
t.on((e, m) => events.push(`${e}::${m}`));
t.recordJoin("mesh-A", "alice");
expect(events).toEqual([]);
t.recordJoin("mesh-A", "bob");
expect(events).toEqual(["mesh:full::mesh-A"]);
t.recordLeave("mesh-A", "alice");
expect(events).toEqual(["mesh:full::mesh-A", "mesh:available::mesh-A"]);
});
it("on() returns an unsubscribe handle", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A", 1);
const events: string[] = [];
const off = t.on((e) => events.push(e));
off();
t.recordJoin("mesh-A", "alice");
expect(events).toEqual([]);
});
it("recordMesh upserts capacity and rechecks full state", () => {
const t = new MeshCapacityTracker();
const events: string[] = [];
t.recordMesh("mesh-A", 1);
t.on((e) => events.push(e));
t.recordJoin("mesh-A", "alice");
// mesh full at 1/1
expect(events).toContain("mesh:full");
t.recordMesh("mesh-A", 10); // expand → no longer full
expect(events).toContain("mesh:available");
});
it("listMeshes returns every known mesh", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A");
t.recordMesh("mesh-B");
expect(t.listMeshes().sort()).toEqual(["mesh-A", "mesh-B"]);
});
it("uses default maxPeers when not specified on recordMesh", () => {
const t = new MeshCapacityTracker({ defaultMaxPeers: 7 });
t.recordMesh("mesh-A");
expect(t.getMaxPeers("mesh-A")).toBe(7);
});
it("uses 64 as the built-in default", () => {
const t = new MeshCapacityTracker();
t.recordMesh("mesh-A");
expect(t.getMaxPeers("mesh-A")).toBe(64);
});
});

View File

@@ -0,0 +1,11 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
describe("network-plugin-assignment dep-graph", () => {
it("loads index.js (acyclic static imports)", async () => {
const mod = await import("../src/index.js");
expect(mod.MeshCapacityTracker).toBeDefined();
expect(mod.AssignmentEngine).toBeDefined();
expect(mod.MeshBalancer).toBeDefined();
expect(mod.AdmissionController).toBeDefined();
});
});

View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
AdmissionController,
AssignmentEngine,
MeshBalancer,
MeshCapacityTracker,
type MeshDescriptor,
type PeerInfo,
} from "../../src/index.js";
describe("assignment end-to-end", () => {
it("admits peers, builds a rebalance plan, and applies moves", () => {
const tracker = new MeshCapacityTracker();
tracker.recordMesh("mesh-A", 10);
tracker.recordMesh("mesh-B", 10);
const meshes: MeshDescriptor[] = [
{ meshId: "mesh-A", region: "us-east", tierFloor: 0 },
{ meshId: "mesh-B", region: "us-west", tierFloor: 0 },
];
const engine = new AssignmentEngine({ tracker });
const controller = new AdmissionController({
engine,
tracker,
meshes: () => meshes,
});
// Force imbalance by manually loading mesh-A.
for (let i = 0; i < 8; i++) tracker.recordJoin("mesh-A", `p${i}`);
const balancer = new MeshBalancer({
tracker,
imbalanceThreshold: 0.3,
maxMovesPerPlan: 2,
});
const plan = balancer.buildPlan((overloaded) => {
expect(overloaded).toBe("mesh-A");
return ["p0", "p1"];
});
expect(plan.moves).toHaveLength(2);
for (const move of plan.moves) {
tracker.recordLeave(move.fromMeshId, move.peerId);
tracker.recordJoin(move.toMeshId, move.peerId);
}
expect(tracker.getUtilization("mesh-A")).toBe(0.6);
expect(tracker.getUtilization("mesh-B")).toBe(0.2);
// A fresh peer can still be admitted via the controller.
const fresh: PeerInfo = {
peerId: "alice",
latencyMap: new Map([
["mesh-A", 50],
["mesh-B", 50],
]),
region: "us-east",
tierEligibility: 1,
capabilities: "STANDARD",
};
const result = controller.admit(fresh);
expect(["mesh-A", "mesh-B"]).toContain(result.meshId);
});
});

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import * as fc from "fast-check";
import {
AssignmentEngine,
MeshCapacityTracker,
type MeshDescriptor,
type PeerInfo,
} from "../../src/index.js";
describe("AssignmentEngine scoring is deterministic", () => {
it("same input twice produces identical scores", () => {
fc.assert(
fc.property(
fc.array(
fc.tuple(
fc.string({ minLength: 1, maxLength: 6 }),
fc.integer({ min: 1, max: 100 }),
fc.integer({ min: 0, max: 5 })
),
{ minLength: 1, maxLength: 5 }
),
fc.integer({ min: 0, max: 5 }),
(entries, tierEligibility) => {
const seen = new Set<string>();
const meshes: MeshDescriptor[] = [];
const latencyMap = new Map<string, number>();
for (const [name, latency, tierFloor] of entries) {
if (seen.has(name)) continue;
seen.add(name);
meshes.push({ meshId: name, tierFloor });
latencyMap.set(name, latency);
}
if (meshes.length === 0) return;
const peer: PeerInfo = {
peerId: "p",
latencyMap,
tierEligibility,
capabilities: "STANDARD",
};
const tracker = new MeshCapacityTracker();
for (const m of meshes) tracker.recordMesh(m.meshId, 10);
const engine = new AssignmentEngine({ tracker });
const a = engine.score(peer, meshes);
const b = engine.score(peer, meshes);
expect(a.map((s) => s.score)).toEqual(
b.map((s) => s.score)
);
}
),
{ numRuns: 50 }
);
});
});

View File

@@ -0,0 +1,8 @@
// Ensure `globalThis.crypto` is available in bare-Node vitest runs.
import { webcrypto } from "node:crypto";
type WritableGlobal = { crypto?: Crypto };
const g = globalThis as WritableGlobal;
if (g.crypto === undefined) {
g.crypto = webcrypto as unknown as Crypto;
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"references": [{ "path": "./tsconfig.lib.json" }]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,39 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir:
"../../../node_modules/.vite/packages/network-plugin-assignment",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/error",
"@suaveplan/network",
"@suaveplan/types",
"tslib",
"zod",
"@opentelemetry/api",
/^node:/,
],
},
},
});

View File

@@ -0,0 +1,39 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
setupFiles: ["./tests/setup/polyfills.ts"],
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
exclude: [
"src/__benchmarks__/**",
"benchmarks/**",
"e2e/**",
"dist/**",
"node_modules/**",
],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.bench.ts",
"src/**/*.schemas.ts",
"src/**/*.types.ts",
"src/**/index.ts",
"src/__benchmarks__/**",
"tests/**",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 5000,
},
});