first commit
This commit is contained in:
26
docs/networking/network/network-plugin-simulation/README.md
Normal file
26
docs/networking/network/network-plugin-simulation/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# @suaveplan/network-plugin-simulation - Deterministic virtual-network simulation framework for the Suaveplan mesh: NetworkSimulator (seeded RNG + time control + topology factories), VirtualNode, ScenarioRunner, NetworkConditionInjector, ChaosEngine. Dev/test only.
|
||||
|
||||
## Summary
|
||||
|
||||
Deterministic virtual-network simulation framework for the Suaveplan mesh. `NetworkSimulator` runs scenarios against a seeded RNG with simulated time (acceleration / deceleration vs wall clock) and four built-in topology factories (star / mesh / tiered / custom); `VirtualNode` wraps the same `NetworkNode` + plugin code that real peers use, so behaviour is bit-identical between simulation and production; `ScenarioRunner` executes declarative scenarios (setup → ordered events → success criteria → metric collection) with checkpointing for long runs; `NetworkConditionInjector` applies latency / loss / bandwidth / jitter / connection-disruption models per peer-pair; `ChaosEngine` injects random failures (peer crashes, transport faults, partitions, message drops) for chaos testing. Dev/test only — the production wrapper is a no-op when `NODE_ENV === "production"`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [chaos](./src/chaos/chaos-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [chaos-engine](./src/chaos/chaos-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [conditions](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [network-condition-injector](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [simulation-plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [scenario](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [scenario-runner](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [network-simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the simulation package. Codes are namespaced under `NET_SIM` and register…
|
||||
<!-- end:auto-docs --> - [chaos-engine](./src/chaos/chaos-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditions](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-condition-injector](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario-runner](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the simulation package. Codes are namespaced under `NET_SIM` and register…
|
||||
- [chaos-engine](./src/chaos/chaos-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditions](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-condition-injector](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario-runner](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the simulation package. Codes are namespaced under `NET_SIM` and register…
|
||||
- [chaos-engine](./src/chaos/chaos-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditions](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-condition-injector](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario-runner](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the simulation package. Codes are namespaced under `NET_SIM` and register…
|
||||
- [chaos-engine](./src/chaos/chaos-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditions](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-condition-injector](./src/conditions/network-condition-injector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [simulation-plugin](./src/plugin/simulation-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [scenario](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [scenario-runner](./src/scenario/scenario-runner.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [network-simulator](./src/simulator/network-simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_ - [virtual-node](./src/virtual-node/virtual-node.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the simulation package. Codes are namespaced under `NET_SIM` and register…
|
||||
@@ -0,0 +1,81 @@
|
||||
# `ChaosEngine`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Probabilistic failure injector for chaos testing. Each call to `step(nodes, edges)` rolls four independent dice (crash / partition / message-drop / transport-fault) using the simulator's seeded RNG, applies the relevant mutation, and appends a `ChaosEvent` to the engine's event log. Used to push the mesh past the predefined-scenario surface and surface bugs that only appear under noisy real-world conditions.
|
||||
|
||||
## Features
|
||||
|
||||
- Four configurable per-step probabilities — set any to `0` (or omit) to disable that chaos type.
|
||||
- Crash and transport-fault both pick a random victim from the live `nodes` array; partition picks a random victim edge from the supplied edge list.
|
||||
- Message-drop is decoupled from a specific message — it fires based on the per-step probability so callers can pair drops with arbitrary downstream traffic.
|
||||
- Event log is per-engine and survives `step` calls so test assertions can read the full chaos history in one shot.
|
||||
- Pure RNG via the simulator means deterministic chaos under a fixed seed.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ChaosEngine,
|
||||
NetworkSimulator,
|
||||
VirtualNode,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 1 });
|
||||
const nodes = [new VirtualNode({ id: "n1", tier: 0 }, sim)];
|
||||
const chaos = new ChaosEngine(sim, { crashProbability: 0.5 });
|
||||
const events = chaos.step(nodes, []);
|
||||
console.log(events);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ChaosEngine,
|
||||
NetworkSimulator,
|
||||
VirtualNode,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 99 });
|
||||
const nodes = Array.from(
|
||||
{ length: 5 },
|
||||
(_, i) => new VirtualNode({ id: `n${i}`, tier: i === 0 ? 0 : 1 }, sim)
|
||||
);
|
||||
const edges = nodes
|
||||
.slice(1)
|
||||
.map((n) => ({ from: "n0", to: n.id }));
|
||||
|
||||
const chaos = new ChaosEngine(sim, {
|
||||
crashProbability: 0.05,
|
||||
partitionProbability: 0.02,
|
||||
messageDropProbability: 0.10,
|
||||
transportFaultProbability: 0.01,
|
||||
});
|
||||
|
||||
for (let tick = 0; tick < 100; tick += 1) {
|
||||
sim.advanceBy(1);
|
||||
chaos.step(nodes, edges);
|
||||
}
|
||||
console.log(chaos.eventLog().length);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ChaosEngine(simulator, config?)`
|
||||
- `update(config: ChaosConfig): void`.
|
||||
- `step(nodes, edges): ReadonlyArray<ChaosEvent>` — events emitted this step.
|
||||
- `eventLog(): ReadonlyArray<ChaosEvent>` — full log since construction.
|
||||
- `reset(): void` — clears the event log.
|
||||
- `interface ChaosConfig` — `{ crashProbability?, partitionProbability?, messageDropProbability?, transportFaultProbability? }`.
|
||||
- `interface ChaosEvent` — `{ atMs, type, target?, target2? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The engine never throws — chaos is intentionally noisy and the simulator should not abort because of an unlucky roll. Caller code that wants to refuse particular events should filter the event log post-hoc.
|
||||
- Crash skips victims that are already not-running; transport-fault does not, because a transport fault on a downed node is still a meaningful test event for downstream dispatchers.
|
||||
- `messageDropProbability` is not paired with a specific message because the engine does not know about message identities — pair it with a downstream layer that consults the event log for "should I drop the next outbound?" decisions.
|
||||
- The four probabilities are independent — every step can emit between zero and four events.
|
||||
@@ -0,0 +1,83 @@
|
||||
# `NetworkConditionInjector`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Per-message decision engine that applies the configured `NetworkConditionConfig` (latency / loss / bandwidth / jitter / disruption) to each simulated send. Returns an `InjectionResult` describing the resulting latency, drop verdict, and effective bandwidth so the simulator's transport layer can decide whether and when to deliver the message.
|
||||
|
||||
## Features
|
||||
|
||||
- Three latency models (`constant` / `random` / `geographic`) selected via discriminated `kind`.
|
||||
- Three loss patterns (`random` / `burst` / `connection-based`) — burst is approximated as `burstProbability × burstLength` per attempt for simplicity.
|
||||
- Three bandwidth limits (`fixed` / `variable` / `congestion`).
|
||||
- Sinusoidal jitter added to the resolved latency on every call.
|
||||
- Per-edge disruption tracker remembers active drops until the configured `durationMs` elapses (or forever if `permanent: true`).
|
||||
- Pure: every random decision goes through the simulator's seeded RNG so two runs with identical configs produce identical decisions.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkConditionInjector,
|
||||
NetworkSimulator,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 1 });
|
||||
const injector = new NetworkConditionInjector(sim, {
|
||||
latency: { kind: "constant", latencyMs: 50 },
|
||||
loss: { kind: "random", probability: 0.01 },
|
||||
});
|
||||
const result = injector.inject({ from: "n1", to: "n2", sizeBytes: 1024 });
|
||||
result.latencyMs; // 50 + jitter (0 here)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkConditionInjector,
|
||||
NetworkSimulator,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 99 });
|
||||
const injector = new NetworkConditionInjector(sim, {
|
||||
latency: {
|
||||
kind: "geographic",
|
||||
perRegionMs: { "us-east": 30, "eu-west": 80 },
|
||||
},
|
||||
bandwidth: {
|
||||
kind: "congestion",
|
||||
baseBytesPerSec: 5_000_000,
|
||||
congestionFactor: 0.5,
|
||||
},
|
||||
jitter: { amplitudeMs: 20, periodMs: 5_000 },
|
||||
disruption: { probability: 0.001, durationMs: 1_000 },
|
||||
});
|
||||
|
||||
sim.advanceBy(2);
|
||||
const r = injector.inject({
|
||||
from: "n1",
|
||||
to: "n2",
|
||||
fromRegion: "us-east",
|
||||
toRegion: "eu-west",
|
||||
sizeBytes: 4096,
|
||||
});
|
||||
console.log(r.latencyMs, r.bandwidthBytesPerSec);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class NetworkConditionInjector(simulator, config?)`
|
||||
- `update(config: NetworkConditionConfig): void`.
|
||||
- `inject(message: MessageDescriptor): InjectionResult`.
|
||||
- `interface MessageDescriptor` — `{ from, to, fromRegion?, toRegion?, sizeBytes }`.
|
||||
- `interface InjectionResult` — `{ latencyMs, droppedByLoss, droppedByDisruption, dropped, bandwidthBytesPerSec }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- All decisions consume one or more values from the simulator's seeded RNG, so byte-identical runs with the same seed produce byte-identical injection results.
|
||||
- Disruption is sticky: once an edge drops, every subsequent inject for that edge returns dropped until either `durationMs` expires or the disruption is `permanent`. This matches the TCP-style "the link is down" experience consumers want to test against.
|
||||
- Geographic latency adds the `fromRegion` and `toRegion` values; missing regions contribute zero so consumers can mix region-tagged and untagged messages without bespoke handling.
|
||||
- The jitter wave uses `sin(2πt/period)` so amplitude is symmetric around zero — large negative jitter can subtract from constant latency, modelling under-load fast paths as well as congested ones.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Error codes — `@suaveplan/network-plugin-simulation`
|
||||
|
||||
## Purpose
|
||||
|
||||
Frozen registry of every error code raised by the simulation package. Codes are namespaced under `NET_SIM` and registered into the global error registry on first import via the side-effect import in `src/index.ts`. Each code maps to a single failure class so consumers branch on `error.code` without parsing message text.
|
||||
|
||||
## Features
|
||||
|
||||
- Four codes covering topology validation, node lookup, scenario validation, and time-control validation. The previous registry shipped with `CRITERION_FAILED` reserved for predefined-scenario integration code; the placeholder was removed because the current dispatcher reports criterion failures inline via the `ScenarioResult.criteriaResults` map rather than throwing on the failure path.
|
||||
- `NetworkSimError` extends `SuaveplanError<NetworkSimErrorCode>` for cause-chain traversal, redaction, and OTel adapter compatibility.
|
||||
- Frozen registry — runtime mutation throws.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NETWORK_SIM_CODES,
|
||||
NetworkSimError,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
throw new NetworkSimError(
|
||||
NETWORK_SIM_CODES.NODE_NOT_FOUND,
|
||||
"simulator.unknown-node",
|
||||
{ context: { nodeId: "n42" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
try {
|
||||
runner.run(scenario);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NetworkSimError &&
|
||||
err.code === NETWORK_SIM_CODES.SCENARIO_INVALID
|
||||
) {
|
||||
return reportInvalidScenario(scenario);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NETWORK_SIM_CODES` — frozen `KEY -> "NET_SIM.KEY"` map.
|
||||
- `NetworkSimErrorCode` — TypeScript union of every code value.
|
||||
- `NetworkSimError` — `SuaveplanError<NetworkSimErrorCode>` subclass; `name === "NetworkSimError"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry is constructed via `defineCodes("NET_SIM", […] as const)` so the type system enumerates the value union; adding a code requires bumping the `as const` tuple.
|
||||
- Codes are deliberately coarse (one per failure class) so consumers build stable retry / observability policies without churn.
|
||||
- `TIME_INVALID` covers both negative time-factors and `advanceBy(Δ)` calls with non-positive deltas; the messageKey distinguishes the two situations.
|
||||
- Predefined-scenario integration code (`server-failure`, `partition`, `client-surge`, `cascading-failure` modules) ships in follow-up changes and may add its own codes there.
|
||||
@@ -0,0 +1,62 @@
|
||||
# `SimulationPlugin`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Single `NetworkPlugin` that bundles the simulation toolkit (`NetworkSimulator`, `NetworkConditionInjector`, `ChaosEngine`) and ties their lifecycle to a host `NetworkNode`. Every subsystem is exposed as a public field so consumers extend behaviour by reaching past the plugin facade. No-op in production builds so accidentally bundling the plugin into a release does not run chaos against real peers.
|
||||
|
||||
## Features
|
||||
|
||||
- `disabled` defaults to `process.env.NODE_ENV === "production"`; explicit `disabled: false` overrides for staging environments.
|
||||
- `install` / `start` short-circuit when disabled — the plugin remains constructed but inert.
|
||||
- `dispose` resets the chaos event log so a re-install starts clean.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkBus, NetworkNode } from "@suaveplan/network";
|
||||
import { SimulationPlugin } from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const node = new NetworkNode({ id: "n", networkBus: new NetworkBus() });
|
||||
const plugin = new SimulationPlugin({ simulation: { seed: 1 } });
|
||||
node.use(plugin);
|
||||
await node.start();
|
||||
plugin.simulator.advanceBy(10);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SimulationPlugin } from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const plugin = new SimulationPlugin({
|
||||
simulation: { seed: 42, timeFactor: 1_000 },
|
||||
conditions: {
|
||||
latency: { kind: "constant", latencyMs: 50 },
|
||||
loss: { kind: "random", probability: 0.01 },
|
||||
},
|
||||
chaos: { crashProbability: 0.001 },
|
||||
disabled: false,
|
||||
});
|
||||
plugin.isActive(); // true
|
||||
plugin.simulator.now();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class SimulationPlugin implements NetworkPlugin`
|
||||
- Public fields: `simulator`, `conditions`, `chaos`.
|
||||
- `constructor(config: SimulationPluginConfig)`.
|
||||
- `isActive(): boolean`.
|
||||
- `install(node): void` / `start(): void` / `stop(): void` / `dispose(): void`.
|
||||
- `host(): NetworkNode | null` / `isStarted(): boolean`.
|
||||
- `interface SimulationPluginConfig` — `{ simulation, conditions?, chaos?, disabled? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plugin holds _references_ to subsystems rather than re-exporting their methods so refactors inside any subsystem do not reshape the plugin facade.
|
||||
- The default-disabled-in-production guard reads `process.env.NODE_ENV` defensively (`process` may be undefined in the browser); browser callers wanting simulation explicitly pass `disabled: false`.
|
||||
- `install` is the only method that gates on `disabled`; the simulator / conditions / chaos fields remain non-null and usable so test code can still exercise them via `plugin.simulator.advanceBy(...)` even while the plugin is "disabled".
|
||||
- The four predefined integration scenarios (`server-failure`, `network-partition`, `client-surge`, `cascading-failure`) ship as follow-up changes — they require coordination with `failover`, `topology`, `circuit-breaker`, etc., which lives outside this package's contract.
|
||||
@@ -0,0 +1,102 @@
|
||||
# `ScenarioRunner`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Executes a declarative `SimulationScenario` against a `NetworkSimulator`. Sorts events by `timeMs`, advances the simulator clock to each event, dispatches the event to a caller-supplied handler, and records checkpoints + final criterion evaluation. The handler is the boundary where consumers wire scenario events to virtual-node failures, condition changes, and chaos injection.
|
||||
|
||||
## Features
|
||||
|
||||
- Time-sorted event execution so authors can declare events in any order.
|
||||
- Per-criterion `evaluateAtCheckpoints` opt-in records criteria mid-run for debugging.
|
||||
- Metric collection via `bumpMetric` / `setMetric` so handlers report counts / averages back into the result.
|
||||
- `validateScenario` exposed as a free helper for pre-flight CI checks.
|
||||
- Seed cross-check between scenario and simulator so determinism is enforced before any work runs.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkSimulator,
|
||||
ScenarioRunner,
|
||||
type SimulationScenario,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 1 });
|
||||
const runner = new ScenarioRunner({
|
||||
simulator: sim,
|
||||
handler: (event) => console.log("event", event.type, event.timeMs),
|
||||
});
|
||||
|
||||
const scenario: SimulationScenario = {
|
||||
name: "smoke",
|
||||
seed: 1,
|
||||
initialTopology: { kind: "mesh", nodes: [{ id: "n1", tier: 0 }] },
|
||||
nodeCount: 1,
|
||||
events: [{ timeMs: 100, type: "fail", targetPeerIds: ["n1"] }],
|
||||
successCriteria: [],
|
||||
};
|
||||
const result = runner.run(scenario);
|
||||
result.succeeded; // true (no criteria)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkSimulator,
|
||||
ScenarioRunner,
|
||||
validateScenario,
|
||||
type SimulationScenario,
|
||||
type SuccessCriterion,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 7 });
|
||||
const allEventsRan: SuccessCriterion = {
|
||||
id: "ran",
|
||||
description: "scenario reached completion",
|
||||
evaluate: () => true,
|
||||
evaluateAtCheckpoints: true,
|
||||
};
|
||||
|
||||
const scenario: SimulationScenario = {
|
||||
name: "checkpointed",
|
||||
seed: 7,
|
||||
initialTopology: { kind: "star", nodes: [{ id: "h", tier: 0 }, { id: "s", tier: 1 }] },
|
||||
nodeCount: 2,
|
||||
events: [
|
||||
{ timeMs: 100, type: "join", targetPeerIds: ["s"] },
|
||||
{ timeMs: 200, type: "checkpoint", label: "after-join" },
|
||||
{ timeMs: 300, type: "leave", targetPeerIds: ["s"] },
|
||||
],
|
||||
successCriteria: [allEventsRan],
|
||||
};
|
||||
validateScenario(scenario);
|
||||
|
||||
const runner = new ScenarioRunner({
|
||||
simulator: sim,
|
||||
handler: (event) => runner.bumpMetric(`event.${event.type}`),
|
||||
});
|
||||
const result = runner.run(scenario);
|
||||
result.checkpoints[0]?.label; // "after-join"
|
||||
result.metrics["event.join"]; // 1
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ScenarioRunner(config: ScenarioRunnerConfig)`
|
||||
- `bumpMetric(name, delta?): void`.
|
||||
- `setMetric(name, value): void`.
|
||||
- `run(scenario: SimulationScenario): ScenarioResult`.
|
||||
- `function validateScenario(scenario): void` — throws `SCENARIO_INVALID` for empty name / negative node count / duplicate criterion id.
|
||||
- `interface ScenarioRunnerConfig` — `{ simulator, handler }`.
|
||||
- `type EventHandler` — `(event: ScenarioEvent) => void`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Events with `type: "checkpoint"` are NOT dispatched to the handler — the runner records the snapshot directly. Other event types always reach the handler.
|
||||
- The runner advances time _to_ each event's `timeMs` (no-op when the simulator is already at or past it). This keeps timing deterministic when scenarios mix close-together and far-apart events.
|
||||
- Final-run criteria are always evaluated, regardless of `evaluateAtCheckpoints`. The flag only controls whether mid-run snapshots include the criterion.
|
||||
- `metrics` and `criteriaResults` are frozen on the returned result so consumers cannot mutate the report after the fact.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `NetworkSimulator`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Deterministic, time-controlled simulation core. Owns the seeded Mulberry32 PRNG, the simulated wall clock (with a configurable `timeFactor` multiplier), and the four built-in topology factories (star / mesh / tiered / custom). Every other module in the package — `VirtualNode`, `ScenarioRunner`, `NetworkConditionInjector`, `ChaosEngine` — consumes a single `NetworkSimulator` instance so timing and randomness stay coordinated.
|
||||
|
||||
## Features
|
||||
|
||||
- 32-bit Mulberry32 PRNG seeded from `config.seed` so identical configs produce byte-identical runs.
|
||||
- `advanceBy(wallMs)` multiplies by `timeFactor`; `advanceTo(absMs)` sets the simulated clock directly (rejects backward jumps).
|
||||
- `randInt(min, max)` convenience helper for integer ranges.
|
||||
- Four topology factories (`star` / `mesh` / `tiered` / `custom`) returning normalised `VirtualTopology` `{ nodes, edges }`.
|
||||
- All time / topology validation throws typed `NetworkSimError`s so caller bugs fail loudly.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkSimulator } from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 42, timeFactor: 100 });
|
||||
sim.now(); // 0
|
||||
sim.advanceBy(10); // 1000 ms simulated (10 wall * 100 factor)
|
||||
sim.rand(); // deterministic float in [0,1)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkSimulator,
|
||||
type NetworkTopologyConfig,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 12345, timeFactor: 1000 });
|
||||
|
||||
const topology: NetworkTopologyConfig = {
|
||||
kind: "tiered",
|
||||
nodes: [
|
||||
{ id: "h1", tier: 0 },
|
||||
{ id: "h2", tier: 0 },
|
||||
{ id: "s1", tier: 1 },
|
||||
{ id: "s2", tier: 1 },
|
||||
{ id: "l1", tier: 2 },
|
||||
],
|
||||
};
|
||||
const built = sim.buildTopology(topology);
|
||||
console.log(built.edges.length); // 8 — every tier-0 paired with every tier-1, every tier-1 with every tier-2
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class NetworkSimulator(config: SimulationConfig)`
|
||||
- `getSeed(): number`.
|
||||
- `now(): number` — simulated time.
|
||||
- `timeFactorValue(): number`.
|
||||
- `advanceBy(wallMs): number` — throws `TIME_INVALID` for non-positive deltas.
|
||||
- `advanceTo(absoluteSimulatedMs): number` — throws `TIME_INVALID` for backward jumps.
|
||||
- `rand(): number` — `[0, 1)` deterministic.
|
||||
- `randInt(min, max): number` — `[min, max)` deterministic.
|
||||
- `buildTopology(config): VirtualTopology` — throws `TOPOLOGY_INVALID` for invalid configs.
|
||||
- `function mulberry32(seed): () => number` — exported so callers can spawn additional independent streams.
|
||||
- `interface VirtualTopology` — `{ nodes, edges }`.
|
||||
- `interface VirtualEdge` — `{ from, to }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Mulberry32 is chosen for speed + zero state-leak between runs. Higher-entropy generators (xoshiro256**) would be overkill for scenario replay and would slow seed-replay tests.
|
||||
- `star` topology requires at least 2 nodes (hub + 1 spoke); the factory throws `TOPOLOGY_INVALID` for under-capacity configs.
|
||||
- `tiered` connects every node in tier _N_ with every node in tier _N+1_. Within-tier connections must be added via `custom` if needed.
|
||||
- Backward time-travel is intentionally blocked because the scenario runner relies on monotonic time to schedule events deterministically.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Condition types — `@suaveplan/network-plugin-simulation`
|
||||
|
||||
## Purpose
|
||||
|
||||
Discriminated unions describing how the `NetworkConditionInjector` deforms simulated traffic. `LatencyModel` covers constant / random / geographic latency curves; `LossPattern` covers random / burst / per-connection drop rates; `BandwidthLimit` covers fixed / variable / congestion-based throughput caps; `JitterConfig` adds amplitude + periodic latency variation; `DisruptionConfig` simulates temporary or permanent connection failures. `NetworkConditionConfig` bundles them so callers configure the entire conditions surface in one struct.
|
||||
|
||||
## Features
|
||||
|
||||
- Discriminated `kind` field on each model so injectors exhaustively switch — adding a new model is a one-place change.
|
||||
- All time + bandwidth values use ms / bytes-per-sec so the injector does not perform unit conversions per-call.
|
||||
- Permanent vs temporary disruption distinguishable via the optional `permanent` flag.
|
||||
- All inner records are `Readonly` so configurations cannot mutate after construction.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
LatencyModel,
|
||||
LossPattern,
|
||||
NetworkConditionConfig,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const latency: LatencyModel = { kind: "constant", latencyMs: 50 };
|
||||
const loss: LossPattern = { kind: "random", probability: 0.01 };
|
||||
const conditions: NetworkConditionConfig = { latency, loss };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { NetworkConditionConfig } from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const conditions: NetworkConditionConfig = {
|
||||
latency: {
|
||||
kind: "geographic",
|
||||
perRegionMs: { "us-east": 30, "eu-west": 80, "ap-south": 220 },
|
||||
},
|
||||
loss: {
|
||||
kind: "burst",
|
||||
burstProbability: 0.005,
|
||||
burstLength: 5,
|
||||
},
|
||||
bandwidth: {
|
||||
kind: "congestion",
|
||||
baseBytesPerSec: 5_000_000,
|
||||
congestionFactor: 0.6,
|
||||
},
|
||||
jitter: { amplitudeMs: 20, periodMs: 5_000 },
|
||||
disruption: { probability: 0.001, durationMs: 1_000 },
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type LatencyModel` — `constant | random | geographic`.
|
||||
- `type LossPattern` — `random | burst | connection-based`.
|
||||
- `type BandwidthLimit` — `fixed | variable | congestion`.
|
||||
- `interface JitterConfig` — `{ amplitudeMs, periodMs }`.
|
||||
- `interface DisruptionConfig` — `{ probability, durationMs, permanent? }`.
|
||||
- `interface NetworkConditionConfig` — `{ latency?, loss?, bandwidth?, jitter?, disruption? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `connection-based` loss keys connections by `"<from>:<to>"` strings. Callers must normalise the order if they want bidirectional pairing.
|
||||
- `congestion`'s `congestionFactor` is the fractional multiplier applied to `baseBytesPerSec` while congestion is active; the injector decides when to apply it.
|
||||
- `disruption` does not embed cooldown semantics — when `permanent: false`, the injector simply restores the connection after `durationMs`. Patterns like "X% disruption per minute" should compose multiple disruption configs across scenario events.
|
||||
- `jitter.periodMs` is the wave period — amplitude is added on a sine curve so latency oscillates smoothly rather than stepping abruptly.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Scenario types — `@suaveplan/network-plugin-simulation`
|
||||
|
||||
## Purpose
|
||||
|
||||
Declarative shapes the `ScenarioRunner` consumes. `SimulationScenario` describes setup + ordered events + success criteria; `ScenarioEvent` is the unit of mid-run mutation; `SuccessCriterion` is a callable predicate the runner evaluates at scenario end (and optionally at every checkpoint); `ScenarioResult` is the report the runner returns.
|
||||
|
||||
## Features
|
||||
|
||||
- Closed `ScenarioEventType` union covers join / leave / fail / partition / heal / condition-change / checkpoint.
|
||||
- `SuccessCriterion.evaluate` is a function so callers express arbitrary assertions without inventing a DSL.
|
||||
- `ScenarioCheckpoint` records the per-criterion result snapshot at a moment in simulated time, useful for debugging long runs.
|
||||
- `ScenarioResult.metrics` is `Record<string, number>` so runners report event counts / latency averages without a richer schema.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ScenarioEvent,
|
||||
SimulationScenario,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const scenario: SimulationScenario = {
|
||||
name: "ping-storm",
|
||||
seed: 1,
|
||||
initialTopology: { kind: "mesh", nodes: [{ id: "n1", tier: 0 }] },
|
||||
nodeCount: 1,
|
||||
events: [
|
||||
{ timeMs: 100, type: "fail", targetPeerIds: ["n1"] },
|
||||
],
|
||||
successCriteria: [],
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ScenarioCheckpoint,
|
||||
ScenarioResult,
|
||||
SuccessCriterion,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const allHealthy: SuccessCriterion = {
|
||||
id: "all-healthy",
|
||||
description: "every peer remains reachable",
|
||||
evaluate: (ctx) => ctx.simulatedTimeMs > 0,
|
||||
evaluateAtCheckpoints: true,
|
||||
};
|
||||
|
||||
declare const result: ScenarioResult;
|
||||
result.checkpoints.forEach((cp) => console.log(cp.atMs, cp.criteriaResults));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type ScenarioEventType` — closed union of seven event kinds.
|
||||
- `interface ScenarioEvent` — `{ timeMs, type, targetPeerIds?, condition?, label? }`.
|
||||
- `interface SuccessCriterion` — `{ id, description, evaluate(ctx), evaluateAtCheckpoints? }`.
|
||||
- `interface SimulationScenario` — `{ name, seed, initialTopology, nodeCount, events, networkConditions?, successCriteria }`.
|
||||
- `interface ScenarioCheckpoint` — `{ atMs, label?, criteriaResults }`.
|
||||
- `interface ScenarioResult` — `{ scenario, succeeded, checkpoints, criteriaResults, metrics, executedEvents }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `ScenarioEvent.targetPeerIds` is optional because some event types (e.g. `condition-change`) apply globally, while others (e.g. `fail`) target a specific peer set.
|
||||
- `SuccessCriterion.evaluate` is sync — the runner does not await asynchronous predicates because the simulator already lives in simulated time and async-await would re-introduce wall-clock dependence.
|
||||
- `ScenarioCheckpoint.criteriaResults` only includes criteria that opted in via `evaluateAtCheckpoints: true`. Final-run results live in `ScenarioResult.criteriaResults`.
|
||||
- `ScenarioResult.executedEvents` echoes the event list the runner actually fired (useful when scenarios are programmatically generated and the caller wants to confirm which events ran).
|
||||
@@ -0,0 +1,68 @@
|
||||
# Simulation types — `@suaveplan/network-plugin-simulation`
|
||||
|
||||
## Purpose
|
||||
|
||||
Wire shapes for the `NetworkSimulator` constructor and topology factories. `SimulationConfig` carries the seed + time-factor + initial-time tuple every other module reads from. `VirtualNodeConfig` describes the per-peer hints topology factories consume. `NetworkTopologyConfig` selects between built-in topologies or supplies custom edges.
|
||||
|
||||
## Features
|
||||
|
||||
- Closed `TopologyKind` union (`star` / `mesh` / `tiered` / `custom`) so factories exhaustively switch.
|
||||
- `seed` is required so simulations cannot accidentally diverge between runs.
|
||||
- Optional `transports` list per virtual node so condition injectors know which connection-types exist.
|
||||
- `timeFactor` defaults to `1` (real-time); test runs typically pick `100` or higher to compress long scenarios.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SimulationConfig,
|
||||
VirtualNodeConfig,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const config: SimulationConfig = { seed: 42, timeFactor: 100 };
|
||||
const nodes: VirtualNodeConfig[] = [
|
||||
{ id: "n1", tier: 0 },
|
||||
{ id: "n2", tier: 1, transports: ["websocket"] },
|
||||
];
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
NetworkTopologyConfig,
|
||||
SimulationConfig,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const config: SimulationConfig = {
|
||||
seed: 12345,
|
||||
timeFactor: 1000,
|
||||
initialTimeMs: 1_700_000_000_000,
|
||||
};
|
||||
|
||||
const topology: NetworkTopologyConfig = {
|
||||
kind: "custom",
|
||||
nodes: Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `n${i}`,
|
||||
tier: i === 0 ? 0 : 1,
|
||||
})),
|
||||
edges: Array.from({ length: 9 }, (_, i) => ({
|
||||
from: "n0",
|
||||
to: `n${i + 1}`,
|
||||
})),
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface SimulationConfig` — `{ seed, timeFactor?, initialTimeMs? }`.
|
||||
- `interface VirtualNodeConfig` — `{ id, tier, transports? }`.
|
||||
- `type TopologyKind` — `"star" | "mesh" | "tiered" | "custom"`.
|
||||
- `interface NetworkTopologyConfig` — `{ kind, nodes, edges? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `seed` is `number` rather than `bigint` so consumers serialise to JSON without conversion. The simulator uses a 32-bit Mulberry32 PRNG so 53-bit numbers are downcast safely.
|
||||
- `edges` on `NetworkTopologyConfig` is optional for built-in topologies because the factories derive the edge set from `nodes`. For `custom`, the factory rejects when `edges` is missing.
|
||||
- `VirtualNodeConfig.transports` lists transport identifiers, not concrete instances — keeps the type independent of the transport-package surface.
|
||||
- `timeFactor` is intentionally a multiplier (not a target FPS) so callers reason about it as "how much faster than real time".
|
||||
@@ -0,0 +1,75 @@
|
||||
# `VirtualNode`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Simulated peer that wraps the same `NetworkNode` + plugin code real peers run, plus a status flag (`running` / `crashed` / `frozen` / `disconnected`) the scenario runner and chaos engine toggle to inject failures. Records every tick / send / receive / fail / recover event with the simulator's current simulated time, so test assertions can replay the timeline deterministically.
|
||||
|
||||
## Features
|
||||
|
||||
- Status state-machine with explicit `crash` / `freeze` / `disconnect` / `recover` transitions; idempotent (re-entering a state does not double-record).
|
||||
- `tick` / `recordSend` / `recordReceive` push events into a per-node history buffer.
|
||||
- `tick` throws `NODE_NOT_FOUND` when called against a non-running node so callers cannot observe ticks they did not actually run.
|
||||
- Read-only `events()` snapshot returns a clone so callers cannot mutate the underlying buffer.
|
||||
- `clearHistory()` for long scenarios that need to free memory between checkpoints.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkSimulator,
|
||||
VirtualNode,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 1 });
|
||||
const node = new VirtualNode({ id: "n1", tier: 0 }, sim);
|
||||
node.tick();
|
||||
node.crash();
|
||||
node.events().length; // 2
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkSimulator,
|
||||
VirtualNode,
|
||||
} from "@suaveplan/network-plugin-simulation";
|
||||
|
||||
const sim = new NetworkSimulator({ seed: 7, timeFactor: 1_000 });
|
||||
const node = new VirtualNode(
|
||||
{ id: "n2", tier: 1, transports: ["websocket", "webrtc"] },
|
||||
sim
|
||||
);
|
||||
sim.advanceBy(1);
|
||||
node.recordSend({ kind: "ping" });
|
||||
sim.advanceBy(1);
|
||||
node.recordReceive({ kind: "pong" });
|
||||
|
||||
node.crash();
|
||||
sim.advanceBy(5);
|
||||
node.recover();
|
||||
node.tick();
|
||||
console.log(node.events().map((e) => `${e.atMs}:${e.type}`));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class VirtualNode(config, simulator)`
|
||||
- `getStatus(): VirtualNodeStatus`.
|
||||
- `crash(): void` / `freeze(): void` / `disconnect(): void` / `recover(): void` — idempotent transitions.
|
||||
- `tick(payload?): VirtualNodeEvent` — throws `NODE_NOT_FOUND` when not running.
|
||||
- `recordSend(payload): void` / `recordReceive(payload): void`.
|
||||
- `events(): ReadonlyArray<VirtualNodeEvent>` — non-destructive snapshot.
|
||||
- `clearHistory(): void`.
|
||||
- `type VirtualNodeStatus` — `"running" | "crashed" | "frozen" | "disconnected"`.
|
||||
- `interface VirtualNodeEvent` — `{ atMs, type, payload? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The class is intentionally thin — it only tracks status + history. Wiring to a real `NetworkNode` instance happens in integration tests / predefined scenarios so the core simulator stays free of plugin-specific logic.
|
||||
- `crash` / `freeze` / `disconnect` all collapse to a single "fail" event in the history buffer; consumers wanting to distinguish should also read `getStatus()` after the failure.
|
||||
- `tick` rejects when not running because typical scenarios advance the simulator clock independently of node ticks; ticking a crashed node would produce a misleading "still alive" event in the history.
|
||||
- `recover` is idempotent on `running` so consumers can safely call it from event handlers without checking current status.
|
||||
Reference in New Issue
Block a user