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