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,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…

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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".

View File

@@ -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.