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,32 @@
# @suaveplan/network-plugin-state - Mesh-wide synchronized state with delta compression, path-specific conflict resolution, Bloom-filter divergence detection, and resource-aware sync
## Summary
Mesh-wide synchronized state for the Suaveplan network. Path-based `StateManager` (versioned history with sequence numbers + per-path subscriptions); `DeltaCompressor` computing structural deltas between snapshots with round-trip verification; `StateReconciler` covering the five strategies (`latest-wins` / `remote-wins` / `local-wins` / `deep-merge` / `custom`) with `PathStrategyRegistry` so different parts of the tree honour different policies; `BloomFilterComparator` for compact divergence detection; `StateSyncEngine` orchestrating full-state sync for new peers and delta sync for existing peers; `ResourceAdapter` mapping the four device profiles (`EDGE` / `MOBILE` / `DESKTOP` / `CLOUD`) to compression / retention / sync-frequency knobs; `StateHistoryPruner` enforcing retention windows and memory bounds. Wrapped in a single `StatePlugin` satisfying the `NetworkPlugin` lifecycle.
<!-- begin:auto-docs -->
## Documentation tree
- [bloom](./src/bloom/bloom-filter-comparator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bloom-filter-comparator](./src/bloom/bloom-filter-comparator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [delta](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [delta-compressor](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-history-pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [resource](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [resource-adapter](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-manager](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [strategy](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [path-strategy-registry](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sync](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-sync-engine](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the state package. Codes are namespaced under `NET_STATE` and registered…
<!-- end:auto-docs --> - [bloom-filter-comparator](./src/bloom/bloom-filter-comparator.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-compressor](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-history-pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource-adapter](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [state](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [strategy](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [path-strategy-registry](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [sync](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-sync-engine](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the state package. Codes are namespaced under `NET_STATE` and registered…
- [bloom-filter-comparator](./src/bloom/bloom-filter-comparator.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-compressor](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-history-pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource-adapter](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [state](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [strategy](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [path-strategy-registry](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [sync](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-sync-engine](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the state package. Codes are namespaced under `NET_STATE` and registered…
- [bloom-filter-comparator](./src/bloom/bloom-filter-comparator.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-compressor](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-history-pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource-adapter](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [state](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [strategy](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [path-strategy-registry](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [sync](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-sync-engine](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the state package. Codes are namespaced under `NET_STATE` and registered…
- [bloom-filter-comparator](./src/bloom/bloom-filter-comparator.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_ - [delta-compressor](./src/delta/delta-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-plugin](./src/plugin/state-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-history-pruner](./src/pruner/state-history-pruner.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-reconciler](./src/reconciler/state-reconciler.md) — <!-- begin:auto-docs --> _(no submodules)_- [resource](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ - [resource-adapter](./src/resource/resource-adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [state](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-manager](./src/state/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [strategy](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [path-strategy-registry](./src/strategy/path-strategy-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [sync](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-sync-engine](./src/sync/state-sync-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the state package. Codes are namespaced under `NET_STATE` and registered…

View File

@@ -0,0 +1,65 @@
# `BloomFilterComparator` + `BloomFilter`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Compact divergence-detection primitive for the sync engine. Peers exchange Bloom-filter byte buffers summarising their known state paths; the comparator computes which local paths are missing from the remote filter so the sender can target only those paths in the next delta sync. Avoids full-state exchange for routine reconciliation.
## Features
- `BloomFilter` sized for a configurable expected item count + false-positive rate (default 1%); bits / hashes derived analytically.
- `BloomFilterComparator.summarise(paths, config)` builds a fresh filter from a path set; `missingFromRemote(localPaths, remote)` returns the list of locals the remote almost-certainly doesn't have.
- `toBytes` / `loadBytes` round-trip the bit store across the wire.
- Pure helpers `computeParams` / `fnv1a` / double-hashing kept internal so the wire format is stable per (`expectedItems`, `falsePositiveRate`) tuple.
## Usage: Basic Example
```ts
import { BloomFilter } from "@suaveplan/network-plugin-state";
const filter = new BloomFilter({ expectedItems: 1_000 });
filter.add("game.players.alice.x");
filter.has("game.players.alice.x"); // true
filter.has("game.players.bob.x"); // false (probably)
```
## Usage: Advanced Example
```ts
import {
BloomFilter,
BloomFilterComparator,
} from "@suaveplan/network-plugin-state";
const cmp = new BloomFilterComparator();
const localPaths = ["a.x", "a.y", "b.z"];
const remote = cmp.summarise(["a.x"], { expectedItems: 16 });
const missing = cmp.missingFromRemote(localPaths, remote);
// missing ⊇ ["a.y", "b.z"] — sender targets exactly these in the next delta
const wire = remote.toBytes();
const restored = new BloomFilter({ expectedItems: 16 });
restored.loadBytes(wire);
```
## API Reference
- `class BloomFilter(config: BloomFilterConfig)`
- `add(value: string): void`.
- `has(value: string): boolean`.
- `size(): number`.
- `toBytes(): Uint8Array`.
- `loadBytes(bytes: Uint8Array): void` — throws on length mismatch.
- `class BloomFilterComparator`
- `summarise(paths, config): BloomFilter`.
- `missingFromRemote(localPaths, remote): ReadonlyArray<string>`.
- `interface BloomFilterConfig` — `{ expectedItems, falsePositiveRate? }`.
## Implementation Notes
- The bit count and hash count are derived from the standard Bloom analytical formulas — `m = ceil(-n ln p / ln 2²)`, `k = round((m/n) ln 2)` — clamped to at least 8 bits and 1 hash so degenerate configs still produce a usable filter.
- Hashing uses double-hashing (`h1 + i * h2`) seeded by two FNV-1a digests of the input + a separator suffix. FNV-1a is fast, hashable, and avoids the cost of allocating arrays per hash.
- `clampFpr` defends against extreme inputs; rates outside `(0, 1)` are clamped to a sensible interval rather than throwing so the comparator stays usable in tests with quirky parameters.
- Both peers MUST construct their filters with the same `(expectedItems, falsePositiveRate)` tuple — otherwise the bit count and hash count differ and `loadBytes` rejects the mismatch.

View File

@@ -0,0 +1,59 @@
# `DeltaCompressor`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Computes the structural delta between two `StateValue` trees and produces a flat list of `DeltaEntry`s the sync engine ships across the wire. Provides the inverse `apply(base, entries)` so receivers reconstruct the new tree by replaying entries against the base. `verifyRoundTrip` is a self-check the test suite (and dev-mode consumers) use to confirm `apply(base, compute(base, next))` structurally equals `next`.
## Features
- Path-aware diff: only changed leaves are emitted; unchanged subtrees produce zero entries.
- Tombstone support: `value: undefined` entries delete the path.
- `$root` sentinel path covers the root-replacement case (when the base and next are scalar / array shapes).
- Round-trip verification via `verifyRoundTrip(base, next)` returns `boolean`.
- Pure helpers — no internal state, safe to share across worker threads.
## Usage: Basic Example
```ts
import { DeltaCompressor } from "@suaveplan/network-plugin-state";
const c = new DeltaCompressor();
const base = { a: 1, b: 2 };
const next = { a: 1, b: 3, c: 4 };
const entries = c.compute(base, next, Date.now());
// entries: [{ path: "b", value: 3, ... }, { path: "c", value: 4, ... }]
const rebuilt = c.apply(base, entries);
```
## Usage: Advanced Example
```ts
import { DeltaCompressor } from "@suaveplan/network-plugin-state";
const c = new DeltaCompressor();
const base = { players: { alice: { x: 1, y: 2 }, bob: { x: 3, y: 4 } } };
const next = { players: { alice: { x: 5, y: 2 }, bob: { x: 3, y: 4 } } };
const delta = c.compute(base, next, Date.now());
// delta has exactly one entry: { path: "players.alice.x", value: 5 }
const ok = c.verifyRoundTrip(base, next);
// ok === true
```
## API Reference
- `class DeltaCompressor`
- `compute(base, next, timestamp): ReadonlyArray<DeltaEntry>`.
- `apply(base, entries): StateValue`.
- `verifyRoundTrip(base, next): boolean`.
## Implementation Notes
- The compressor operates on plain JSON-shaped values only — `StateValue`'s union prevents `Date`/`Map`/cycles, which would otherwise demand bespoke structural-equality logic.
- Arrays are diffed as a single unit (replace-the-whole-array): per-element diffing would balloon the delta entries for typical game-state arrays where shape changes rarely.
- `clone` performs a deep structural copy so apply does not mutate the base. The cost is linear in tree size; for very large trees, callers should diff sub-trees.
- `$root` is the only path that survives the `path.split(".")` round-trip without ambiguity. Real paths cannot start with `$` because `validatePath` rejects empty segments.
- `applyEntry`'s defensive `!isPlainObject(base)` branch handles the `apply($root, ...)` case where the base itself is scalar/array — entries replace the base wholesale.

View File

@@ -0,0 +1,56 @@
# Error codes — `@suaveplan/network-plugin-state`
## Purpose
Frozen registry of every error code raised by the state package. Codes are namespaced under `NET_STATE` 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
- Five codes covering path validation, custom-strategy misconfiguration, delta ordering, resource-profile lookup, and pruner retention.
- `NetworkStateError` extends `SuaveplanError<NetworkStateErrorCode>` for cause-chain traversal, redaction, and OTel adapter compatibility.
- Frozen registry — runtime mutation throws.
## Usage: Basic Example
```ts
import {
NETWORK_STATE_CODES,
NetworkStateError,
} from "@suaveplan/network-plugin-state";
throw new NetworkStateError(
NETWORK_STATE_CODES.PATH_INVALID,
"state.path-empty",
{ context: { path: "" } }
);
```
## Usage: Advanced Example
```ts
try {
syncEngine.applyDelta(delta);
} catch (err) {
if (
err instanceof NetworkStateError &&
err.code === NETWORK_STATE_CODES.DELTA_OUT_OF_ORDER
) {
return syncEngine.requestFullSync();
}
throw err;
}
```
## API Reference
- `NETWORK_STATE_CODES` — frozen `KEY -> "NET_STATE.KEY"` map.
- `NetworkStateErrorCode` — TypeScript union of every code value.
- `NetworkStateError` — `SuaveplanError<NetworkStateErrorCode>` subclass; `name === "NetworkStateError"`.
## Implementation Notes
- The registry is constructed via `defineCodes("NET_STATE", […] as const)` so the type system enumerates the value union; adding a code requires bumping the `as const` tuple.
- Codes are deliberately coarse so consumers build stable retry / observability policies without churn.
- `STRATEGY_MISSING_MERGE` is raised at registration time (not on apply) so misconfiguration fails loudly during boot.
- `DELTA_OUT_OF_ORDER` is the single recovery hint the sync engine raises when a delta cannot stitch onto the local sequence — receivers typically respond by requesting a full sync.
- `PRUNE_INVALID_RETENTION` is raised at construction time so misconfigured pruners cannot silently degrade in production by allowing the manager's history buffer to grow without ever evicting old entries.

View File

@@ -0,0 +1,67 @@
# `StatePlugin`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Single `NetworkPlugin` that bundles every state subsystem (`StateManager`, `DeltaCompressor`, `StateReconciler`, `PathStrategyRegistry`, `StateHistoryPruner`, `ResourceAdapter`, `StateSyncEngine`) and ties their lifecycle to a host `NetworkNode`. Each subsystem is a public field so consumers extend behaviour by reaching past the plugin facade.
## Features
- Profile-aware: the chosen `ResourceProfile` determines the manager's `historyLimit` default; consumer overrides via `config.manager` win.
- All subsystems eagerly constructed so unit tests can build the plugin without a node.
- `dispose` clears the path-strategy registry and detaches the node so a re-install starts clean.
- Public `manager` field is the substantive surface — set / get / subscribe — without going through helper APIs.
## Usage: Basic Example
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import { StatePlugin } from "@suaveplan/network-plugin-state";
const node = new NetworkNode({ id: "n", networkBus: new NetworkBus() });
const plugin = new StatePlugin();
node.use(plugin);
await node.start();
plugin.manager.set("game.score", 0);
plugin.registry.add({ pattern: "game.score", strategy: "latest-wins" });
const delta = plugin.sync.buildDelta();
```
## Usage: Advanced Example
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import { StatePlugin } from "@suaveplan/network-plugin-state";
const plugin = new StatePlugin({
profile: "MOBILE",
pruner: { retentionMs: 60_000, maxEntries: 1_000 },
});
plugin.registry.add({ pattern: "game.inventory.*", strategy: "deep-merge" });
plugin.registry.add({ pattern: "audit.**", strategy: "remote-wins" });
const node = new NetworkNode({ id: "n", networkBus: new NetworkBus() });
node.use(plugin);
await node.start();
plugin.manager.set("game.inventory.coins", 10);
plugin.pruner.prune(plugin.manager);
```
## API Reference
- `class StatePlugin implements NetworkPlugin`
- Public fields: `manager`, `compressor`, `reconciler`, `registry`, `pruner`, `resource`, `sync`.
- `constructor(config?: StatePluginConfig)`.
- `install(node): void` / `start(): void` / `stop(): void` / `dispose(): void`.
- `host(): NetworkNode | null` / `isStarted(): boolean`.
- `interface StatePluginConfig` — `{ profile?, manager?, pruner?, sync? }`.
## 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 profile is `DESKTOP` because that is the most permissive baseline — consumers on edge / mobile devices should pass `profile: "EDGE"` or `"MOBILE"` explicitly.
- `dispose` only clears the path-strategy registry — the manager's state and history persist so a redeploy can resume sync. Callers that want a hard reset should construct a new plugin.
- The plugin does not auto-subscribe to bus events (e.g. `peer:joined` for full-state sync). Higher-level orchestrators wire those because the `peer:*` event semantics vary by deployment.

View File

@@ -0,0 +1,67 @@
# `StateHistoryPruner`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Trims the `StateManager`'s history buffer to a configurable retention window + size cap so long-running peers do not accumulate unbounded history. Operates directly on the manager via a structural cast — no per-pruner pump is required, callers run `prune(manager)` on whatever cadence suits the deployment (e.g. once per sync cycle, every minute on a timer, on every state mutation).
## Features
- Two retention knobs — `retentionMs` (default 5 minutes) and `maxEntries` (default 4096) — applied conjunctively.
- Throws `PRUNE_INVALID_RETENTION` at construction for non-positive values so misconfiguration fails loudly.
- `prune(manager)` returns the count of entries removed so observability can plot eviction rate.
- Injectable clock for deterministic tests.
## Usage: Basic Example
```ts
import {
StateHistoryPruner,
StateManager,
} from "@suaveplan/network-plugin-state";
const manager = new StateManager();
manager.set("a", 1);
manager.set("b", 2);
const pruner = new StateHistoryPruner({ retentionMs: 1_000, maxEntries: 100 });
const removed = pruner.prune(manager);
console.log(removed, manager.history().length);
```
## Usage: Advanced Example
```ts
import {
StateHistoryPruner,
StateManager,
} from "@suaveplan/network-plugin-state";
let now = 1_000;
const manager = new StateManager({ clock: () => now });
for (let i = 0; i < 10_000; i += 1) {
manager.set(`p.${i}`, i);
now += 100;
}
const pruner = new StateHistoryPruner({
retentionMs: 60_000,
maxEntries: 1_000,
clock: () => now,
});
const removed = pruner.prune(manager);
console.log(removed); // ~9_000+
```
## API Reference
- `class StateHistoryPruner(config?)`
- `prune(manager: StateManager): number` — returns the count of entries removed.
## Implementation Notes
- The pruner uses a structural cast (`manager as unknown as MutableManager`) to access the manager's private history buffer. This keeps the manager's public API focused on user concerns while letting the pruner avoid copying the entire history per prune call.
- Pruning is age-then-count: entries past `retentionMs` are dropped first, then the remainder is truncated to `maxEntries` keeping the most recent. This matches the intuition that operators want "keep the last N items, but never older than X minutes".
- The pruner does not implement a timer of its own — running it on a cadence is the caller's responsibility. The plugin wraps it with a per-sync trigger so production deployments do not need to wire a separate scheduler.
- Returning the eviction count lets metric emitters plot a histogram of prune sizes — useful for tuning `retentionMs` against actual workload churn.

View File

@@ -0,0 +1,71 @@
# `StateReconciler`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Resolves the five reconciliation strategies (`latest-wins`, `remote-wins`, `local-wins`, `deep-merge`, `custom`) against a single `ConflictInput` tuple and returns a typed `ConflictResolution`. The path strategy registry and sync engine consult the reconciler — the logic is pure so consumers can also call it directly for tests and audits.
## Features
- Closed `ReconciliationStrategy` switch with `winner` reported on every resolution (`local` / `remote` / `merged`).
- `latest-wins` ties go to the remote so the receiver does not stall when timestamps coincide.
- `deep-merge` recursively combines plain objects; scalar / array conflicts deterministically pick the remote so the merge is reproducible.
- `custom` strategy delegates to a `CustomMergeFn`; absent merge falls back to the local value (caller bug — registry rejects this at registration time so this is defensive only).
## Usage: Basic Example
```ts
import {
StateReconciler,
type ConflictInput,
} from "@suaveplan/network-plugin-state";
const r = new StateReconciler();
const input: ConflictInput = {
path: "score",
localValue: 1,
localTimestamp: 1,
remoteValue: 2,
remoteTimestamp: 2,
};
const out = r.resolve("latest-wins", input);
// out.resolvedValue === 2, out.winner === "remote"
```
## Usage: Advanced Example
```ts
import {
StateReconciler,
deepMerge,
type ConflictInput,
} from "@suaveplan/network-plugin-state";
const r = new StateReconciler();
const input: ConflictInput = {
path: "inventory",
localValue: { coins: 10, gems: 1 },
localTimestamp: 100,
remoteValue: { coins: 5, scrolls: 2 },
remoteTimestamp: 99,
};
const dm = r.resolve("deep-merge", input);
// dm.resolvedValue === { coins: 5, gems: 1, scrolls: 2 } — remote wins on conflict, locals merged in
const merged = deepMerge(input.localValue, input.remoteValue);
```
## API Reference
- `class StateReconciler`
- `resolve(strategy, input, merge?): ConflictResolution`.
- `function deepMerge(local, remote): StateValue | undefined` — exported pure helper for consumers that want the merge result without the wrapper.
## Implementation Notes
- The reconciler is stateless — every `resolve` call is pure given its inputs. Subscribers can run resolutions in parallel without coordination.
- `latest-wins`'s tie-break (remote on equal timestamps) avoids ping-pong cycles where two peers keep "winning" the same conflict at the same time.
- `deep-merge`'s scalar-tie remote-wins rule means the merge is associative — `deepMerge(a, b) === deepMerge(deepMerge(a, b), b)` for all inputs — which is essential for consumers that re-apply the merge under retries.
- `custom` strategy without a `merge` is defensive: the registry's `addStrategy` raises `STRATEGY_MISSING_MERGE` at registration, so the in-resolve fallback only activates if a caller bypasses the registry.

View File

@@ -0,0 +1,62 @@
# `ResourceAdapter`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Maps the four device profiles (`EDGE` / `MOBILE` / `DESKTOP` / `CLOUD`) to per-profile sync knobs (history limit, Bloom-filter sizing, sync interval, compression level, battery-aware flag). Plugins consult the adapter once at boot and can switch profiles at runtime without restarting the manager.
## Features
- Frozen `PROFILE_DEFAULTS` table for the four profiles; `EDGE` is conservative, `CLOUD` is throughput-maximal.
- Per-profile overrides via the constructor / `switchProfile` second arg so deployments tune individual knobs without forking a profile.
- Throws `RESOURCE_PROFILE_UNKNOWN` when an unknown string sneaks past the union (defensive — TypeScript would normally catch this).
- Pure — no timers, no I/O. Caller wires the resolved config into the manager + Bloom comparator + sync engine.
## Usage: Basic Example
```ts
import { ResourceAdapter } from "@suaveplan/network-plugin-state";
const adapter = new ResourceAdapter({ profile: "MOBILE" });
adapter.config().syncIntervalMs; // 1_500
adapter.config().batteryAware; // true
```
## Usage: Advanced Example
```ts
import {
PROFILE_DEFAULTS,
ResourceAdapter,
} from "@suaveplan/network-plugin-state";
const adapter = new ResourceAdapter({
profile: "DESKTOP",
overrides: { syncIntervalMs: 100 }, // override one knob
});
// Switch to mobile when the host battery drops
adapter.switchProfile("MOBILE", { syncIntervalMs: 2_000 });
adapter.currentProfile(); // "MOBILE"
console.log(PROFILE_DEFAULTS.CLOUD.bloomExpectedItems); // 65_536
```
## API Reference
- `type ResourceProfile` — `"EDGE" | "MOBILE" | "DESKTOP" | "CLOUD"`.
- `interface ResourceProfileConfig` — `{ historyLimit, bloomExpectedItems, syncIntervalMs, compressionLevel, batteryAware }`.
- `const PROFILE_DEFAULTS` — frozen per-profile defaults.
- `class ResourceAdapter(config)`
- `switchProfile(profile, overrides?): void`.
- `currentProfile(): ResourceProfile`.
- `config(): ResourceProfileConfig`.
## Implementation Notes
- The defaults are intentionally coarse — enough resolution to differentiate the four profiles, but not so fine-grained that overrides become the common case.
- `switchProfile` rebuilds the active config from defaults + overrides. Stale overrides from the prior profile do not bleed into the new profile — this is intentional so callers can reason about the active config from the constructor + most-recent-switch arguments.
- `RESOURCE_PROFILE_UNKNOWN` is the boundary error for callers that pass a string typed as `string` rather than `ResourceProfile`. Internal callers using the union never trigger it.
- The adapter intentionally does NOT subscribe to host-OS events (battery, thermal). Wire those at a higher layer and call `switchProfile` from there.

View File

@@ -0,0 +1,66 @@
# `StateManager`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Path-based versioned state store for the mesh. Holds the local view of shared state, advances a strictly monotonic sequence on every mutation, records history for audit + reconciliation, and broadcasts snapshots to per-path subscribers. The sync engine wraps it; reconcilers / pruners read the history.
## Features
- `get` / `set` / `delete` over `.`-separated `StatePath`s.
- Subscriptions per path with unsubscribe-via-disposer return value.
- Bounded history (default 1024 entries) so long-running peers don't accumulate unbounded memory.
- `applyExternal` accepts a (path, value, timestamp) tuple from the sync engine without re-emitting through the local clock.
- Pure helpers `readPath` / `writePath` exported for tests + observability.
## Usage: Basic Example
```ts
import { StateManager } from "@suaveplan/network-plugin-state";
const manager = new StateManager();
manager.set("game.score", 0);
manager.subscribe("game.score", (s) => console.log("score=", s.value));
manager.set("game.score", 1);
```
## Usage: Advanced Example
```ts
import { StateManager } from "@suaveplan/network-plugin-state";
const manager = new StateManager({
clock: () => 1_700_000_000_000,
historyLimit: 4_096,
});
manager.set("players.alice", { x: 1, y: 2 });
manager.set("players.alice.x", 5);
manager.delete("players.alice.y");
const recent = manager.history();
const snap = manager.snapshot();
console.log(recent.length, snap.seq, snap.value);
```
## API Reference
- `class StateManager(config?)`
- `currentSeq(): StateSequence`.
- `get(path): StateValue | undefined` — throws `PATH_INVALID` on empty / non-string paths.
- `set(path, value): StateSnapshot` — auto-bumps `seq` and notifies subscribers.
- `delete(path): StateSnapshot`.
- `snapshot(): StateSnapshot` — full-tree snapshot at the current `seq`.
- `history(): ReadonlyArray<StateHistoryEntry>`.
- `subscribe(path, callback): () => void` — returns the unsubscribe disposer.
- `applyExternal(path, value, timestamp): StateSnapshot` — sync-engine path that respects the remote timestamp.
- `validatePath` / `readPath` / `writePath` — pure helpers.
## Implementation Notes
- `StateValue` is restricted to JSON-shaped values; the manager structurally clones along the write path so subscribers can hold references safely without observing mutations.
- The history ring uses `shift()` past the limit — adequate for the default 1024 entries; the pruner module owns the longer-tail eviction policy.
- `applyExternal` does NOT consult the manager's clock for the timestamp — the remote timestamp wins so reconcilers using `latest-wins` do not race themselves.
- `subscribe`'s returned disposer is idempotent: calling it twice just no-ops the second time. Subscribers can stash it without ref-counting.
- Notifications fire synchronously inside the mutation call. If subscribers throw, the manager has already committed the mutation — exception propagates out so observability sees it.

View File

@@ -0,0 +1,71 @@
# `PathStrategyRegistry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Maps `.`-separated path patterns to reconciliation strategies + optional `merge` callbacks. The sync engine consults the registry per delta entry to pick the right `ReconciliationStrategy` without hard-coding policy in the reconciler. First-match-wins ordering lets callers stack specific patterns above generic catch-alls.
## Features
- Closed pattern grammar: literals match exactly, `*` matches a single segment, `**` matches one or more segments greedily.
- Validation at registration: `custom` strategies missing a `merge` callback raise `STRATEGY_MISSING_MERGE` immediately.
- `resolve(path)` returns the first matching `(strategy, merge)` tuple; falls back to `latest-wins` when no pattern matches.
- `matches` exposed as a free function so tests / callers can validate pattern semantics independently.
## Usage: Basic Example
```ts
import {
PathStrategyRegistry,
} from "@suaveplan/network-plugin-state";
const registry = new PathStrategyRegistry();
registry.add({ pattern: "game.positions.*", strategy: "latest-wins" });
registry.add({ pattern: "game.inventory.*", strategy: "deep-merge" });
registry.resolve("game.positions.alice").strategy; // "latest-wins"
registry.resolve("game.unknown.x").strategy; // "latest-wins" (default)
```
## Usage: Advanced Example
```ts
import {
PathStrategyRegistry,
matches,
} from "@suaveplan/network-plugin-state";
const registry = new PathStrategyRegistry();
registry.add({
pattern: "audit.**",
strategy: "remote-wins",
});
registry.add({
pattern: "flags.*",
strategy: "custom",
merge: ({ localValue, remoteValue }) =>
Boolean(localValue) || Boolean(remoteValue),
});
matches("audit.**", "audit.users.alice.event"); // true (** is greedy)
matches("flags.*", "flags.tutorial"); // true
matches("flags.*", "flags.tutorial.completed"); // false (single segment)
```
## API Reference
- `class PathStrategyRegistry`
- `add(strategy: PathStrategy): void` — throws `STRATEGY_MISSING_MERGE` for `custom` without `merge`.
- `list(): ReadonlyArray<PathStrategy>`.
- `clear(): void`.
- `resolve(path): ResolvedStrategy`.
- `function matches(pattern: string, path: string): boolean`.
- `interface ResolvedStrategy` — `{ strategy, merge }`.
## Implementation Notes
- Registration order is the resolution order — there is no priority field. Putting more-specific patterns first is the explicit caller responsibility; the registry stays simple and predictable.
- `**` is greedy and matches one OR more segments (cannot match zero) so `audit.**` does not silently match `audit` itself.
- The default fallback is `latest-wins` because that is the safe / least-surprising strategy for game / collaborative state — most deployments override this with explicit registrations.
- `matches` is implemented recursively for clarity; the typical pattern length is small (≤5 segments) so the recursion depth is bounded.

View File

@@ -0,0 +1,82 @@
# `StateSyncEngine`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wires the manager + delta compressor + reconciler + path strategy registry into a single sync facade. Builds outgoing `DeltaMessage` / `FullSyncMessage`s from the local snapshot, and applies incoming messages by routing each entry through the per-path strategy. Subscribers see resolved values as ordinary `applyExternal` notifications.
## Features
- `buildDelta` advances an internal "last-base" pointer so subsequent calls only emit incremental changes.
- `buildFullSync` produces the bootstrap snapshot for a newly-joined peer.
- `applyDelta` rejects messages whose `fromSeq` is ahead of the local sequence (`DELTA_OUT_OF_ORDER`) — receivers fall back to `applyFullSync` on this error.
- `applyFullSync` runs every changed key through the reconciler so peer-specific path policies still trigger during catch-up.
- Optional injection of `compressor` / `reconciler` / `registry` so tests and customised pipelines stay flexible.
## Usage: Basic Example
```ts
import {
StateManager,
StateSyncEngine,
} from "@suaveplan/network-plugin-state";
const manager = new StateManager();
const sync = new StateSyncEngine({ manager });
manager.set("game.score", 5);
const delta = sync.buildDelta();
// delta.entries[0].path === "game.score"
```
## Usage: Advanced Example
```ts
import {
PathStrategyRegistry,
StateManager,
StateReconciler,
StateSyncEngine,
} from "@suaveplan/network-plugin-state";
const manager = new StateManager();
const registry = new PathStrategyRegistry();
registry.add({ pattern: "game.inventory.*", strategy: "deep-merge" });
const sync = new StateSyncEngine({
manager,
reconciler: new StateReconciler(),
registry,
});
manager.set("game.inventory.coins", 10);
sync.applyDelta({
kind: "delta",
fromSeq: manager.currentSeq(),
toSeq: manager.currentSeq() + 1,
entries: [
{
path: "game.inventory.gems",
value: 1,
timestamp: Date.now(),
},
],
});
manager.get("game.inventory.coins"); // 10 (preserved by deep-merge)
manager.get("game.inventory.gems"); // 1 (added)
```
## API Reference
- `class StateSyncEngine(config: StateSyncEngineConfig)`
- `buildDelta(): DeltaMessage`.
- `buildFullSync(): FullSyncMessage`.
- `applyDelta(message): void` — throws `DELTA_OUT_OF_ORDER` when `message.fromSeq > local.seq`.
- `applyFullSync(message): void`.
## Implementation Notes
- `lastBaseSeq` is private state — clones of the engine cannot share a base pointer. Production deployments use one engine per local manager.
- `buildDelta` always advances the base pointer even when zero entries change, so callers can poll the engine without re-shipping the same entries.
- `applyEntry` reads the local value via `manager.get(path)` rather than walking the snapshot so subscribers observe the post-resolution value via `applyExternal`'s notification path — keeping the reactive surface uniform.
- `applyFullSync` does NOT bypass the reconciler — even bootstrap traffic respects path strategies so a new peer joining a deep-merge zone observes the merge semantics on the first message.

View File

@@ -0,0 +1,63 @@
# Reconciliation types — `@suaveplan/network-plugin-state`
## Purpose
Shared types for the conflict-resolution layer. `ReconciliationStrategy` is the closed five-strategy union the reconciler and registry both consume; `ConflictInput` and `ConflictResolution` describe the canonical I/O shape; `PathStrategy` lets the registry map glob patterns to per-strategy + optional custom merge.
## Features
- Closed union `ReconciliationStrategy` so exhaustive switches stay honest.
- `ConflictInput` carries both timestamps so `latest-wins` decides without leaking timing logic into the reconciler signature.
- `ConflictResolution.winner` reports whether the result came from the local snapshot, the remote snapshot, or a merge — useful for observability + audit.
- `CustomMergeFn` typed so the registry can validate the merge presence at registration time.
## Usage: Basic Example
```ts
import type {
ConflictInput,
ConflictResolution,
ReconciliationStrategy,
} from "@suaveplan/network-plugin-state";
const strategy: ReconciliationStrategy = "latest-wins";
declare const input: ConflictInput;
declare const out: ConflictResolution;
console.log(out.winner, out.strategy);
```
## Usage: Advanced Example
```ts
import type {
CustomMergeFn,
PathStrategy,
} from "@suaveplan/network-plugin-state";
const mergeArrays: CustomMergeFn = ({ localValue, remoteValue }) => {
const a = Array.isArray(localValue) ? localValue : [];
const b = Array.isArray(remoteValue) ? remoteValue : [];
return Array.from(new Set([...a, ...b]));
};
const strategies: PathStrategy[] = [
{ pattern: "game.positions.*", strategy: "latest-wins" },
{ pattern: "game.inventory.*", strategy: "deep-merge" },
{ pattern: "game.flags.*", strategy: "custom", merge: mergeArrays },
];
```
## API Reference
- `type ReconciliationStrategy` — `"latest-wins" | "remote-wins" | "local-wins" | "deep-merge" | "custom"`.
- `interface ConflictInput` — `{ path, localValue, localTimestamp, remoteValue, remoteTimestamp }`.
- `interface ConflictResolution` — `{ path, resolvedValue, winner, strategy }`.
- `type CustomMergeFn` — `(input: ConflictInput) => StateValue | undefined`.
- `interface PathStrategy` — `{ pattern, strategy, merge? }`.
## Implementation Notes
- `localValue` and `remoteValue` are `StateValue | undefined` so the same shape describes deletes (one side `undefined`) without a separate flag.
- The registry stores patterns in registration order and matches first-wins; consumers can stack increasingly-specific patterns at the top so generic catch-alls do not eclipse per-path overrides.
- `winner: "merged"` is the only outcome where `resolvedValue` is computed rather than copied; subscribers can short-circuit on `local`/`remote` by reading the matching original snapshot.
- `CustomMergeFn` MAY return `undefined` to signal "delete the path" — the manager treats this as a regular delete in the next sync cycle.

View File

@@ -0,0 +1,61 @@
# State types — `@suaveplan/network-plugin-state`
## Purpose
Shared structural types for the state layer. Every other module — manager, delta compressor, reconciler, sync engine, pruner — speaks the same `StateValue` / `StateSnapshot` / `StateHistoryEntry` shapes so callers compose pipelines without inventing per-module conversions.
## Features
- Closed `StateValue` recursive union restricted to JSON-serialisable shapes; reconcilers can compare values structurally without worrying about cycles.
- `StateSequence` typed as `number` rather than a branded type so consumers can compare with `>`/`<` directly while still expressing intent.
- `StateHistoryEntry` captures the canonical "before / after / when" tuple the reconciler and pruner consume.
## Usage: Basic Example
```ts
import type {
StateHistoryEntry,
StatePath,
StateValue,
} from "@suaveplan/network-plugin-state";
const path: StatePath = "game.players.alice.position";
const value: StateValue = { x: 1, y: 2, z: 3 };
declare const entry: StateHistoryEntry;
console.log(entry.seq, entry.timestamp, entry.next);
```
## Usage: Advanced Example
```ts
import type {
StateSnapshot,
StateValue,
} from "@suaveplan/network-plugin-state";
const snapshot: StateSnapshot = {
seq: 42,
value: {
players: { alice: { position: [1, 2, 3] }, bob: { position: [4, 5, 6] } },
} satisfies StateValue,
timestamp: 1_700_000_000_000,
};
declare function broadcast(s: StateSnapshot): void;
broadcast(snapshot);
```
## API Reference
- `type StatePath` — `string` (`.`-separated segments; empty string is root).
- `type StateSequence` — `number`.
- `type StateValue` — `null | boolean | number | string | ReadonlyArray<StateValue> | { [k]: StateValue }`.
- `interface StateHistoryEntry` — `{ seq, path, previous, next, timestamp }`.
- `interface StateSnapshot` — `{ seq, value, timestamp }`.
## Implementation Notes
- `StateValue` is restricted to JSON-shaped values so the delta compressor can serialise without bespoke handling of `Date` / `BigInt` / `Map` types. Callers that need richer shapes should wrap them in `{ kind, payload }` envelopes.
- `previous` and `next` on `StateHistoryEntry` are `StateValue | undefined` so the same record describes set, update, and delete operations without an extra discriminator.
- `StateSnapshot.seq` is the sequence number at the time the snapshot was taken; subscribers use it to skip older snapshots on out-of-order delivery.
- Types live in dedicated files so cross-module type-only imports stay zero-cost at runtime.

View File

@@ -0,0 +1,60 @@
# Sync types — `@suaveplan/network-plugin-state`
## Purpose
Wire shapes the sync engine exchanges between peers. `DeltaMessage` covers the steady-state case (apply only the changed paths between two sequence numbers); `FullSyncMessage` covers the catch-up case (a new peer needs the whole snapshot). The discriminator `kind` lets receivers branch with exhaustive type checks.
## Features
- Discriminated `SyncMessage` union so receivers MUST handle both arms.
- `DeltaMessage` carries `fromSeq`/`toSeq` so consumers detect gaps and request full sync if needed.
- `FullSyncMessage.snapshot` holds the entire `StateValue` tree so receivers can rebuild without back-references.
- `DeltaEntry` accepts `value: StateValue | undefined` so deletes piggyback on the same wire shape as updates.
## Usage: Basic Example
```ts
import type {
DeltaMessage,
FullSyncMessage,
SyncMessage,
} from "@suaveplan/network-plugin-state";
declare const msg: SyncMessage;
if (msg.kind === "delta") {
msg.entries.forEach((e) => console.log(e.path, e.value));
} else {
console.log("full sync at seq", msg.seq);
}
```
## Usage: Advanced Example
```ts
import type { DeltaMessage } from "@suaveplan/network-plugin-state";
const delta: DeltaMessage = {
kind: "delta",
fromSeq: 42,
toSeq: 47,
entries: [
{ path: "game.players.alice.x", value: 5, timestamp: 1_700_000_001_000 },
{ path: "game.flags.tutorial", value: undefined, timestamp: 1_700_000_002_000 },
],
};
```
## API Reference
- `type SyncMode` — `"delta" | "full"`.
- `interface DeltaEntry` — `{ path, value, timestamp }`.
- `interface DeltaMessage` — `{ kind: "delta", fromSeq, toSeq, entries }`.
- `interface FullSyncMessage` — `{ kind: "full", seq, snapshot, timestamp }`.
- `type SyncMessage` — `DeltaMessage | FullSyncMessage`.
## Implementation Notes
- The discriminator (`kind`) lives on the message rather than relying on field-presence checks because TypeScript's narrowing is more reliable on closed strings than on optional-property tests.
- `fromSeq < toSeq` is enforced by the sync engine; receivers can assert it for defensive observability.
- `FullSyncMessage` does not carry a `from` because the receiver is bootstrapping — there is no prior sequence to anchor against.
- `DeltaEntry.value` is intentionally optional-ish (`StateValue | undefined`) rather than a separate `tombstone: true` flag: this keeps the wire shape narrow and the apply path branchless.