first commit
This commit is contained in:
22
docs/networking/network/network-plugin-assignment/README.md
Normal file
22
docs/networking/network/network-plugin-assignment/README.md
Normal 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…
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user