first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# @suaveplan/network-plugin-topology - Tiered network topology: hierarchical 3-tier model, latency-based clustering, group manager, plugin wrapper
## Summary
Tiered mesh topology plugin: 3-tier `HierarchicalTopology` (Host / SubHost / Leaf) with promote/demote, latency-bucket-based `LatencyAssigner`, `GroupManager`, plus stability primitives (reorganization circuit breaker, promotion hysteresis, stability-threshold gate) and empty-tier handling.
<!-- begin:auto-docs -->
## Documentation tree
- [group](./src/group/group-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [group-manager](./src/group/group-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [latency](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [latency-assigner](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [topology-plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [hierarchical-topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-topology plugin. Three codes cover the surface: `INVALID_TIER` fo…
<!-- end:auto-docs --> - [group-manager](./src/group/group-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency-assigner](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology-plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [hierarchical-topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-topology plugin. Three codes cover the surface: `INVALID_TIER` fo…
- [group-manager](./src/group/group-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency-assigner](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology-plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [hierarchical-topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-topology plugin. Three codes cover the surface: `INVALID_TIER` fo…
- [group-manager](./src/group/group-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency-assigner](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology-plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [hierarchical-topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-topology plugin. Three codes cover the surface: `INVALID_TIER` fo…
- [group-manager](./src/group/group-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [latency](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [latency-assigner](./src/latency/latency-assigner.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [topology-plugin](./src/plugin/topology-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hierarchical-topology](./src/topology/hierarchical-topology.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-topology plugin. Three codes cover the surface: `INVALID_TIER` fo…

View File

@@ -0,0 +1,62 @@
# NETWORK_TOPO_CODES
## Purpose
Frozen registry of error codes for the network-plugin-topology plugin. Three codes cover the surface: `INVALID_TIER` for out-of-range or non-integer tier inputs (and for nonsensical promote / demote directions), `PEER_NOT_FOUND` when a topology operation references an unknown peer, and `ALREADY_AT_TIER` when promote / demote is asked for a no-op. The companion `NetworkTopoError` extends `SuaveplanError<NetworkTopoErrorCode>` so consumers pattern-match on `code` rather than parse messages.
## Features
- Three frozen codes registered under the `NET_TOPO` namespace.
- `NetworkTopoError extends SuaveplanError<NetworkTopoErrorCode>`.
- `name: "NetworkTopoError"` 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. `INVALID_TIER` is intentionally broad and reused across the topology, latency, and group sub-modules so the code surface stays small.
## Usage: Basic Example
```ts
import {
NETWORK_TOPO_CODES,
NetworkTopoError,
} from "@suaveplan/network-plugin-topology";
throw new NetworkTopoError(
NETWORK_TOPO_CODES.INVALID_TIER,
"topology.invalid-tier",
{ context: { tier: 5 } }
);
```
## Usage: Advanced Example
```ts
import {
NETWORK_TOPO_CODES,
NetworkTopoError,
} from "@suaveplan/network-plugin-topology";
try {
topology.promote("alice", 0);
} catch (err) {
if (err instanceof NetworkTopoError) {
if (err.code === NETWORK_TOPO_CODES.ALREADY_AT_TIER) {
return; // benign
}
if (err.code === NETWORK_TOPO_CODES.PEER_NOT_FOUND) {
await registerAndRetry(err);
return;
}
}
throw err;
}
```
## API Reference
- `NETWORK_TOPO_CODES` — frozen `{ INVALID_TIER, PEER_NOT_FOUND, ALREADY_AT_TIER }`.
- `type NetworkTopoErrorCode` — union of registered code values.
- `class NetworkTopoError extends SuaveplanError<NetworkTopoErrorCode>` — typed throwable.
## Implementation Notes
`ALREADY_AT_TIER` is a benign-but-distinct code: a caller asking to promote a peer that is already at the target tier is almost certainly a redundant request, but it should not silently succeed because that masks driver bugs in upstream election logic. Future stretch features (the proposal's resource / partition / geo / stability / empty-tier sub-systems) will likely want their own codes; until those land we keep the registry tight rather than register codes that never fire. The package barrel side-effect-imports `./error-codes.js` so subpath consumers register the codes too.

View File

@@ -0,0 +1,78 @@
# GroupManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Tracks group membership across the mesh. Each group has an operator-supplied capacity — there is no built-in default group size. A peer can belong to many groups simultaneously, and the manager publishes `group:membership` on every join / leave so downstream observers (the topology plugin, observability dashboards) can react.
## Features
- `defineGroup(groupId, capacity)` is the only way to create a group; capacity must be a positive integer or the call throws.
- `join(groupId, peerId)` rejects with `group.full` when the group has reached its capacity, and is a no-op (no event) for already-joined peers so callers can re-join idempotently on heartbeats.
- `leave(groupId, peerId)` returns `boolean` — `true` if the peer was actually removed, `false` for unknown groups or peers.
- `membersOf` / `groupsOf` / `list` / `getCapacity` / `size` for observability.
- `removeGroup(groupId)` fires a `leave` event for every member before deleting the group, so dashboards see a clean teardown.
- `clear()` for full reset (e.g. mesh restart).
## Usage: Basic Example
```ts
import { GroupManager } from "@suaveplan/network-plugin-topology";
const groups = new GroupManager({ bus });
groups.defineGroup("region-A", 32);
groups.join("region-A", "alice");
groups.join("region-A", "bob");
const members = groups.membersOf("region-A");
```
## Usage: Advanced Example
```ts
import {
GroupManager,
NETWORK_TOPO_CODES,
NetworkTopoError,
} from "@suaveplan/network-plugin-topology";
const groups = new GroupManager({ bus });
groups.defineGroup("hosts", 4);
groups.defineGroup("subhosts", 16);
bus.on("election:result", ({ winnerId }) => {
try {
groups.join("hosts", winnerId);
} catch (err) {
if (
err instanceof NetworkTopoError &&
err.code === NETWORK_TOPO_CODES.INVALID_TIER
) {
// group full → demote the longest-tenured host first
const oldest = groups.membersOf("hosts")[0];
if (oldest !== undefined) {
groups.leave("hosts", oldest);
groups.join("hosts", winnerId);
}
}
}
});
```
## API Reference
- `interface Group { groupId, capacity, members }`.
- `interface GroupBus { emit("group:membership", { groupId, peerId, change }) }`.
- `interface GroupManagerConfig { bus }`.
- `defineGroup(groupId, capacity)` — capacity must be `> 0`.
- `removeGroup(groupId)` — emits `leave` for every member before deletion.
- `join(groupId, peerId)` — throws `group.full` on capacity breach; no-op on duplicate.
- `leave(groupId, peerId): boolean`.
- `membersOf(groupId): readonly string[]` / `groupsOf(peerId): readonly string[]` / `list(): readonly Group[]`.
- `getCapacity(groupId): number | undefined` / `size(groupId): number`.
- `clear()`.
## Implementation Notes
We deliberately reuse `NETWORK_TOPO_CODES.INVALID_TIER` for both invalid-capacity and group-full failures rather than introducing dedicated codes — both are operator-visible misconfigurations and the small surface doesn't justify a separate registry. The `defineGroup` upsert path lets operators resize a group at runtime by calling `defineGroup` again with a new capacity; existing members are not evicted even if the new capacity is smaller, so the manager temporarily reports `size > capacity` until peers leave naturally. This is the right shape for elastic deployments where shrinking a group should be a soft signal, not a hard eviction.

View File

@@ -0,0 +1,75 @@
# LatencyAssigner
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Assigns peers to a latency bucket given their measured RTT and publishes `latency:rebalance` whenever a peer transitions between buckets. Bucket boundaries, names, and the overflow label are all operator-supplied — the assigner makes no assumption about what "fast" or "slow" means for any particular deployment.
## Features
- Operator-supplied bucket list with explicit ascending validation; misconfigured buckets throw at construction time.
- Operator-supplied `overflowLabel` for latencies above every bucket; the assigner never defaults to a name on the operator's behalf.
- `record(peerId, latencyMs)` returns the assigned label and emits `latency:rebalance` only on bucket changes (no churn for jitter inside one bucket).
- `bucketFor(latencyMs)` is a pure classifier — no side effects, useful for "what-if" scoring in `network-plugin-assignment`.
- `getBucket` / `listInBucket` / `bucketLabels` / `forget` / `clear` for observability and lifecycle.
## Usage: Basic Example
```ts
import { LatencyAssigner } from "@suaveplan/network-plugin-topology";
const assigner = new LatencyAssigner({
bus,
buckets: [
{ label: "local", maxLatencyMs: 30 },
{ label: "regional", maxLatencyMs: 100 },
{ label: "continental", maxLatencyMs: 250 },
],
overflowLabel: "global",
});
assigner.record("alice", 12); // → "local"
assigner.record("alice", 80); // → "regional", fires latency:rebalance
```
## Usage: Advanced Example
```ts
import { LatencyAssigner } from "@suaveplan/network-plugin-topology";
const assigner = new LatencyAssigner({
bus,
buckets: [
{ label: "rack", maxLatencyMs: 1 },
{ label: "row", maxLatencyMs: 5 },
{ label: "dc", maxLatencyMs: 25 },
{ label: "region", maxLatencyMs: 80 },
],
overflowLabel: "wan",
});
bus.on("peer:latency", ({ peerId, rttMs }) => {
assigner.record(peerId, rttMs);
});
const wanPeers = assigner.listInBucket("wan");
metrics.gauge("topology.wan_peers", wanPeers.length);
```
## API Reference
- `interface LatencyBucket { label, maxLatencyMs }`.
- `interface LatencyBus { emit("latency:rebalance", { peerId, from, to, latencyMs }) }`.
- `interface LatencyAssignerConfig { bus, buckets, overflowLabel }` — every field required.
- `bucketFor(latencyMs): string` — pure classifier.
- `record(peerId, latencyMs): string` — gated emit.
- `getBucket(peerId): string | undefined`.
- `listInBucket(label): readonly string[]`.
- `bucketLabels(): readonly string[]` — every configured bucket plus the overflow label.
- `forget(peerId)` / `clear()`.
## Implementation Notes
The assigner validates ascending order at construction (rather than runtime) so a misconfigured bucket list surfaces as a constructor exception rather than as quietly-wrong classification across many records. `record` only emits on transition because measurement noise produces frequent micro-changes inside a bucket; downstream subscribers (the topology plugin, observability dashboards) want stable signals, not jitter. The overflow label is required (no default) because operators that intend "treat everything above X as one bucket" should name that bucket explicitly — silent fallback to `"unknown"` or similar would mask a configuration gap.

View File

@@ -0,0 +1,75 @@
# TopologyPlugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NetworkPlugin` that owns the topology, latency-assigner, and group manager and wires them to the node's bus. Subscribes to `election:result` and `peer:latency` so upstream signals automatically drive topology promotions and bucket reassignments — the consuming application does not have to write any glue between the election engine and the topology.
## Features
- All three sub-managers are fully operator-configurable; if `latency` config is omitted, the plugin runs with no latency bucketing (some deployments don't need it).
- `topology.bus` / `latency.bus` / `group.bus` are filled in automatically from the host node's `networkBus` so config doesn't have to repeat the bus three times.
- Election winners auto-register at tier 0 (or are promoted to it) — operators can opt out by not subscribing to `election:result` (the plugin still installs the listener; consumers that need a different policy should configure their own bus topic).
- `peer:latency` events automatically feed `LatencyAssigner.record`.
- `dispose()` clears every sub-manager and nulls them out so a re-install starts fresh.
## Usage: Basic Example
```ts
import { TopologyPlugin } from "@suaveplan/network-plugin-topology";
const plugin = new TopologyPlugin({
topology: { maxDepth: 3 },
latency: {
buckets: [
{ label: "local", maxLatencyMs: 30 },
{ label: "regional", maxLatencyMs: 100 },
],
overflowLabel: "global",
},
});
node.use(plugin);
await node.start();
```
## Usage: Advanced Example
```ts
import { TopologyPlugin } from "@suaveplan/network-plugin-topology";
const plugin = new TopologyPlugin({
topology: {
maxDepth: 5,
tierLabels: ["RootHost", "Region", "Cluster", "Group", "Leaf"],
},
latency: {
buckets: [
{ label: "rack", maxLatencyMs: 1 },
{ label: "row", maxLatencyMs: 5 },
{ label: "dc", maxLatencyMs: 25 },
{ label: "region", maxLatencyMs: 80 },
],
overflowLabel: "wan",
},
});
node.use(plugin);
await node.start();
// downstream subsystems can manipulate the managers directly
plugin.group?.defineGroup("hosts", 4);
plugin.topology?.register("alice", 4);
```
## API Reference
- `interface TopologyPluginConfig { topology?, latency?, group? }` — every sub-section optional; bus fields are auto-filled.
- Public properties: `topology: HierarchicalTopology | null`, `latency: LatencyAssigner | null`, `group: GroupManager | null` (all populated after `install`).
- `name: "topology"`.
- `install(node)` / `start()` / `stop()` / `dispose()` — standard `NetworkPlugin` lifecycle.
## Implementation Notes
The bus type is widened with `as never` because `NetworkBus` is generically typed and the plugin's topics (`topology:change`, `latency:rebalance`, `group:membership`, `election:result`, `peer:latency`) are not in its compile-time map. Sub-managers are typed nullable because they only exist after `install` runs — callers that need them before install must construct a fresh plugin instance. The two listeners are intentionally defensive: a malformed payload is silently dropped rather than throwing, because bus traffic from other plugins should not be able to crash the topology plugin via type confusion.

View File

@@ -0,0 +1,71 @@
# HierarchicalTopology
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
N-tier mesh topology. Tracks each peer's current tier (a non-negative integer where `0` is the root and higher numbers are deeper), validates promote / demote moves, and publishes `topology:change` on every transition. The class has no compile-time tier ceiling and no built-in tier names — the structure is entirely operator-driven.
## Features
- Optional `maxDepth` upper bound. When supplied, tiers must be in `[0, maxDepth)`; when omitted, any non-negative integer is accepted, so the topology can grow without re-instantiation as the mesh scales.
- Optional `tierLabels` for dashboards / logging. When omitted, `tierLabel(n)` returns `"Tier<n>"` — there are no built-in labels (no implicit `"Host" / "SubHost" / "Leaf"` assumption).
- `register` / `unregister` / `getTier` / `listAtTier` / `list` / `sizeAtTier` / `size` for ordinary lifecycle.
- `observedMaxTier()` reports the deepest currently-occupied tier so the consumer can grow or shrink supporting subsystems (latency buckets, group manager) without a hard-coded depth.
- `promote(peerId, toTier)` and `demote(peerId, toTier)` enforce direction (promote must move up, demote must move down) and surface `INVALID_TIER` / `PEER_NOT_FOUND` / `ALREADY_AT_TIER` distinctly.
- Every successful state change emits `topology:change` with `from` / `to` / `reason` so downstream observers (election engine, latency assigner, group manager) can react.
## Usage: Basic Example
```ts
import { HierarchicalTopology } from "@suaveplan/network-plugin-topology";
// no maxDepth, no labels — accept any non-negative tier
const topo = new HierarchicalTopology({ bus });
topo.register("alice", 2);
topo.promote("alice", 1, "election:result");
topo.promote("alice", 0, "host-vacancy");
```
## Usage: Advanced Example
```ts
import { HierarchicalTopology } from "@suaveplan/network-plugin-topology";
// Five-tier deployment with operator-supplied labels.
const topo = new HierarchicalTopology({
bus,
maxDepth: 5,
tierLabels: ["RootHost", "Region", "Cluster", "Group", "Leaf"],
});
topo.register("alice", 4);
topo.register("bob", 3);
const deepest = topo.observedMaxTier(); // 4
const leaves = topo.listAtTier(deepest!);
for (const peerId of leaves.slice(0, 2)) {
topo.promote(peerId, deepest! - 1, "rebalance");
}
```
## API Reference
- `type Tier = number` — `0` is root.
- `interface TopologyNode { peerId, tier }`.
- `interface TopologyBus { emit("topology:change", { peerId, from, to, reason }) }`.
- `interface HierarchicalTopologyConfig { bus, maxDepth?, tierLabels? }` — both bounds and labels are optional and operator-supplied.
- `getMaxDepth(): number | undefined`.
- `tierLabel(tier): string` — returns the configured label or `"Tier<n>"`.
- `register(peerId, tier, reason?)` / `unregister(peerId)`.
- `getTier(peerId): Tier | undefined`.
- `listAtTier(tier): readonly string[]` / `list(): readonly TopologyNode[]`.
- `size(): number` / `sizeAtTier(tier): number` / `observedMaxTier(): Tier | undefined`.
- `promote(peerId, toTier, reason?)` — `toTier` must be strictly less than current tier.
- `demote(peerId, toTier, reason?)` — `toTier` must be strictly greater than current tier.
- `clear()`.
## Implementation Notes
`promote` and `demote` route through one private `move(direction)` helper so the direction-validation logic lives in exactly one place. Throwing `INVALID_TIER` for "promote that doesn't go up" is deliberate — a caller that intends a sideways move is almost certainly a bug, and silent acceptance would let the FSM drift over time. `register(peer, sameTier)` is a no-op (no event) so callers can re-register on every heartbeat without churning the bus. `unregister` does not emit a topology change either; consumers who want disconnection signals should subscribe to `peer:disconnected` on the bus directly. The `assertTier` helper splits its check into two distinct error contexts (`invalid-tier` for negative/non-integer, `tier-out-of-bounds` for over-`maxDepth`) so dashboards can tell "operator passed a bad value" apart from "mesh hit its configured ceiling".