first commit
This commit is contained in:
123
docs/networking/network/network-bootstrap/README.md
Normal file
123
docs/networking/network/network-bootstrap/README.md
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,103 @@
|
||||
# DiscoveryChain
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`DiscoveryChain` orchestrates an ordered list of `DiscoveryStrategy`
|
||||
implementations, running them in concurrency-bounded batches with retry,
|
||||
jitter, and per-strategy timeout, then merging the results into a deduped
|
||||
peer set. It is the engine the `BootstrapPlugin` delegates to when a freshly
|
||||
started node needs to find its first peers.
|
||||
|
||||
## Features
|
||||
|
||||
- Pluggable, ordered `DiscoveryStrategy` chain — `addStrategy` /
|
||||
`removeStrategy` / `getStrategies` for runtime mutation.
|
||||
- `isAvailable()` gating: unavailable strategies are skipped without
|
||||
consuming a timeout slot.
|
||||
- Concurrency-bounded execution (`maxConcurrent`, default 3) — strategies
|
||||
run in parallel batches, not all-at-once.
|
||||
- Per-strategy retry via `@suaveplan/retry` exponential backoff + full
|
||||
jitter; configurable via `BootstrapRetryConfig`.
|
||||
- Per-attempt timeout that aborts an in-flight strategy via an inner
|
||||
`AbortController` chained to the caller's `signal`.
|
||||
- Deduplication by `peerId` across all strategy results.
|
||||
- Early stop once `maxCollectedPeers` has been collected (saves work when
|
||||
the head of the chain produces enough peers).
|
||||
- Emits `bootstrap:started`, `bootstrap:peer-found`, `bootstrap:exhausted`
|
||||
on an optional `NetworkBus`.
|
||||
- OTel: `bootstrap.chain.execute` span wraps `discover()`;
|
||||
`bootstrap.peers_found_total`, `bootstrap.failures_total`,
|
||||
`bootstrap.chain_duration_ms` instruments are recorded.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { DiscoveryChain } from "@suaveplan/network-bootstrap";
|
||||
|
||||
const chain = new DiscoveryChain({
|
||||
strategies: [seedStrategy, mdnsStrategy],
|
||||
maxConcurrent: 2,
|
||||
});
|
||||
const peers = await chain.discover(2000);
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkBus } from "@suaveplan/network";
|
||||
import { DiscoveryChain } from "@suaveplan/network-bootstrap";
|
||||
import type { BootstrapEventMap } from "@suaveplan/network-bootstrap";
|
||||
|
||||
const bus = new NetworkBus<BootstrapEventMap>();
|
||||
bus.on("bootstrap:peer-found", (ev) => {
|
||||
console.log(`found ${ev.peer.peerId} via ${ev.strategy}`);
|
||||
});
|
||||
|
||||
const chain = new DiscoveryChain({
|
||||
strategies: [dhtStrategy, mdnsStrategy, seedStrategy],
|
||||
maxConcurrent: 3,
|
||||
maxCollectedPeers: 20,
|
||||
retry: {
|
||||
maxAttempts: 4,
|
||||
initialDelayMs: 250,
|
||||
maxDelayMs: 4000,
|
||||
factor: 2,
|
||||
},
|
||||
bus,
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
const peers = await chain.discover(2000, controller.signal);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new DiscoveryChain(options?: DiscoveryChainOptions)`.
|
||||
- `discover(timeoutMs: number, signal?: AbortSignal): Promise<PeerInfo[]>`
|
||||
— runs the chain; always resolves with the collected peer list (failure
|
||||
is reported via OTel counters and bus events, not thrown).
|
||||
- `addStrategy(strategy: DiscoveryStrategy): void`.
|
||||
- `removeStrategy(name: string): boolean`.
|
||||
- `getStrategies(): string[]`.
|
||||
|
||||
`DiscoveryChainOptions`: `strategies`, `maxConcurrent`,
|
||||
`maxCollectedPeers`, `retry`, `clock`, `bus`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Promise.race` between the strategy and a timeout promise is the cleanest
|
||||
way to enforce a per-attempt budget without forking another microtask. The
|
||||
inner `AbortController` is unconditionally aborted on timeout so
|
||||
strategies that wire it correctly can free resources. Retry uses
|
||||
`createExponentialBackoff` + `applyJitter("full")` from `@suaveplan/retry`
|
||||
to keep the package consistent with every other backoff site in the
|
||||
project. The chain swallows the last error and returns an empty list per
|
||||
strategy — failure surfaces through `bootstrap.failures_total{strategy,
|
||||
code}` and `bootstrap:exhausted` rather than via `throw`, so callers do not
|
||||
need a try/catch around the bootstrap fast path. Early-stop on
|
||||
`maxCollectedPeers` is checked before each batch, not mid-batch, to keep
|
||||
the strategies-running-in-parallel invariant honest.
|
||||
@@ -0,0 +1,97 @@
|
||||
# PeerCache
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`PeerCache` stores discovered peer records keyed by `peerId`, with TTL-based
|
||||
expiration and capacity-based eviction. It is the persistent memory of the
|
||||
bootstrap subsystem — strategies populate it on discovery, and downstream
|
||||
transports query it to find candidate peers without re-running discovery.
|
||||
|
||||
Persistence is opt-in: pass a `storage` adapter (anything implementing the
|
||||
narrow `PeerCacheStorageLike` surface — `AbstractStorage` from
|
||||
`@suaveplan/storage` qualifies) plus a `persistKey` and the cache can flush
|
||||
itself to durable storage and reload on next start.
|
||||
|
||||
## Features
|
||||
|
||||
- In-memory `Map<peerId, PeerInfo>` with read-time TTL pruning.
|
||||
- Capacity-bounded with oldest-first eviction (smallest `lastSeen`).
|
||||
- Injectable `Clock` for deterministic tests.
|
||||
- `flush(signal?)` / `load(signal?)` for opt-in JSON persistence through any
|
||||
`PeerCacheStorageLike` backend.
|
||||
- `CACHE_CORRUPTED` thrown when persistent state is unparseable, with the
|
||||
underlying `cause` preserved.
|
||||
- Zod-validated options at construction (`INVALID_OPTIONS` on bad input).
|
||||
- `AbortSignal` threaded through both persistence methods.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { PeerCache } from "@suaveplan/network-bootstrap";
|
||||
|
||||
const cache = new PeerCache({ maxPeers: 50, ttlMs: 60_000 });
|
||||
cache.add({
|
||||
peerId: "p1",
|
||||
addresses: ["/ip4/127.0.0.1/tcp/9001"],
|
||||
lastSeen: Date.now(),
|
||||
source: "seed",
|
||||
});
|
||||
const all = cache.getAll();
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { MemoryStorage } from "@suaveplan/storage";
|
||||
import { PeerCache } from "@suaveplan/network-bootstrap";
|
||||
|
||||
const storage = new MemoryStorage();
|
||||
const clock = () => 1_700_000_000_000;
|
||||
|
||||
const cache = new PeerCache({
|
||||
maxPeers: 200,
|
||||
ttlMs: 300_000,
|
||||
persistKey: "net:peers",
|
||||
storage,
|
||||
clock,
|
||||
});
|
||||
|
||||
await cache.load();
|
||||
cache.add({
|
||||
peerId: "p1",
|
||||
addresses: ["/ip4/10.0.0.1/tcp/9001"],
|
||||
lastSeen: clock(),
|
||||
source: "mdns",
|
||||
});
|
||||
await cache.flush();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new PeerCache(config?: Partial<PeerCacheConfig>)` — validates options via
|
||||
`PeerCacheConfigSchema`; throws `NetBootstrapError(INVALID_OPTIONS)` on
|
||||
malformed input.
|
||||
- `add(peer: PeerInfo): void` — insert-or-update; evicts oldest when at
|
||||
`maxPeers`.
|
||||
- `get(peerId: string): PeerInfo | undefined` — returns a defensive copy or
|
||||
`undefined` if absent/expired.
|
||||
- `getAll(): PeerInfo[]` — returns defensive copies sorted by `lastSeen`
|
||||
descending; prunes expired entries as a side-effect.
|
||||
- `remove(peerId: string): boolean` — true if removed.
|
||||
- `clear(): void`.
|
||||
- `flush(signal?: AbortSignal): Promise<void>` — no-op without persistence.
|
||||
- `load(signal?: AbortSignal): Promise<void>` — throws
|
||||
`NetBootstrapError(CACHE_CORRUPTED)` on undecodable payloads.
|
||||
- `readonly size: number`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Eviction is O(n) on insert at capacity — the cache is sized for hundreds of
|
||||
peers, not millions, so a min-heap is overkill. Defensive copies on `add` /
|
||||
`get` / `getAll` ensure callers cannot mutate cache internals. Persistence
|
||||
serialises only the non-expired snapshot — expired entries are pruned in
|
||||
`flush()` before encoding. The cache emits no events directly; bus events
|
||||
are owned by the `BootstrapPlugin` so they correlate with discovery rounds.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the frozen error-code registry and `NetBootstrapError` class used by
|
||||
every public boundary in `@suaveplan/network-bootstrap`. The codes are
|
||||
declared via `defineCodes("NET_BOOTSTRAP", [...])` so they participate in the
|
||||
repo-wide error-registry contract and are guaranteed to be stable across
|
||||
versions.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `NET_BOOTSTRAP_CODES` registry with four canonical codes:
|
||||
`CHAIN_EXHAUSTED`, `CACHE_CORRUPTED`, `STRATEGY_TIMEOUT`, `INVALID_OPTIONS`.
|
||||
- `NetBootstrapErrorCode` discriminated-union type for exhaustive switches.
|
||||
- `NetBootstrapError` extending `SuaveplanError<NetBootstrapErrorCode>` —
|
||||
carries the code, a message key for i18n resolvers, and an optional
|
||||
`cause` chain.
|
||||
- Compatible with the cause-traversal helpers in `@suaveplan/error`.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_BOOTSTRAP_CODES,
|
||||
NetBootstrapError,
|
||||
} from "@suaveplan/network-bootstrap";
|
||||
|
||||
throw new NetBootstrapError(
|
||||
NET_BOOTSTRAP_CODES.CHAIN_EXHAUSTED,
|
||||
"bootstrap.chain.exhausted"
|
||||
);
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_BOOTSTRAP_CODES,
|
||||
NetBootstrapError,
|
||||
} from "@suaveplan/network-bootstrap";
|
||||
|
||||
try {
|
||||
await plugin.discover();
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NetBootstrapError &&
|
||||
err.code === NET_BOOTSTRAP_CODES.STRATEGY_TIMEOUT
|
||||
) {
|
||||
// Surface a user-facing toast or retry with a longer budget.
|
||||
retryWithLongerBudget();
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const NET_BOOTSTRAP_CODES` — frozen registry with keys
|
||||
`CHAIN_EXHAUSTED` / `CACHE_CORRUPTED` / `STRATEGY_TIMEOUT` /
|
||||
`INVALID_OPTIONS`.
|
||||
- `type NetBootstrapErrorCode` — discriminated union of the registry values.
|
||||
- `class NetBootstrapError extends SuaveplanError<NetBootstrapErrorCode>` —
|
||||
constructor `(code, messageKey, options?)`. Inherits `cause`, `chain()`,
|
||||
and `toJSON()` from `SuaveplanError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is declared via `defineCodes` so a duplicate code at module
|
||||
import time is a hard error — codes cannot accidentally clash across the
|
||||
project. The `messageKey` argument is not the human-readable message; it is
|
||||
the i18n key that callers can resolve with `@suaveplan/i18n`. The four codes
|
||||
cover every failure path documented in the proposal — there is no
|
||||
`UNKNOWN` catch-all because every throw site in the package maps to exactly
|
||||
one of these four conditions.
|
||||
82
docs/networking/network/network-bootstrap/src/otel/otel.md
Normal file
82
docs/networking/network/network-bootstrap/src/otel/otel.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# OTel surface
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Centralises every OpenTelemetry instrument used by
|
||||
`@suaveplan/network-bootstrap`. Routing all spans, counters, and histograms
|
||||
through one module keeps cardinality bounded and makes dashboards trivial to
|
||||
wire up: subscribers only need to discover one tracer and one meter name.
|
||||
|
||||
## Features
|
||||
|
||||
- `withChainSpan(fn)` — opens `bootstrap.chain.execute`, passes the live span
|
||||
to the body so the caller can add attributes, and ends the span in a
|
||||
`finally` block (no leaked spans on throw).
|
||||
- `recordPeersFound(strategy, count)` — increments
|
||||
`bootstrap.peers_found_total{strategy}` by `count` (no-op when zero).
|
||||
- `recordFailure(strategy, code)` — increments
|
||||
`bootstrap.failures_total{strategy, code}` by 1.
|
||||
- `recordChainDuration(ms)` — records into
|
||||
`bootstrap.chain_duration_ms` histogram, clamping negative inputs to 0.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordChainDuration,
|
||||
recordPeersFound,
|
||||
withChainSpan,
|
||||
} from "@suaveplan/network-bootstrap";
|
||||
|
||||
await withChainSpan(async (span) => {
|
||||
span.setAttribute("strategy.count", 3);
|
||||
const start = Date.now();
|
||||
// ...run chain...
|
||||
recordPeersFound("mdns", 5);
|
||||
recordChainDuration(Date.now() - start);
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_BOOTSTRAP_CODES,
|
||||
recordFailure,
|
||||
withChainSpan,
|
||||
} from "@suaveplan/network-bootstrap";
|
||||
|
||||
await withChainSpan(async (span) => {
|
||||
try {
|
||||
await runStrategy("dht");
|
||||
} catch (err) {
|
||||
recordFailure("dht", NET_BOOTSTRAP_CODES.STRATEGY_TIMEOUT);
|
||||
span.recordException(err as Error);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `withChainSpan<T>(fn: (span: Span) => Promise<T>): Promise<T>` — runs `fn`
|
||||
inside `bootstrap.chain.execute`. Always ends the span. Re-throws on
|
||||
failure after ending.
|
||||
- `recordPeersFound(strategy: string, count: number): void` — no-op when
|
||||
`count <= 0`.
|
||||
- `recordFailure(strategy: string, code: string): void` — `code` SHOULD be
|
||||
one of the `NET_BOOTSTRAP_CODES` values.
|
||||
- `recordChainDuration(ms: number): void` — negative inputs are clamped to 0.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All instruments are lazily acquired at module load via
|
||||
`metrics.getMeter()` / `trace.getTracer()` so test environments without an
|
||||
SDK installed still work — the no-op tracer/meter is returned. Labels are
|
||||
restricted to `{strategy}` and `{strategy, code}` to keep cardinality
|
||||
predictable in production dashboards: strategy names are caller-controlled
|
||||
and bounded, error codes are bounded to the four-entry registry. The
|
||||
histogram unit is declared as `ms` per OTel semantic-convention guidance.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Bootstrap middleware
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`createBootstrapMiddleware` builds a `MessageMiddleware` that intercepts
|
||||
incoming peer-discovery requests on a message pipeline and replies with the
|
||||
current cached peer set. It is how a node serves its neighbours' bootstrap
|
||||
queries without needing a separate request/response loop — the same
|
||||
pipeline that handles ordinary messages also handles bootstrap requests.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated configuration at construction.
|
||||
- Default intercept type: `BOOTSTRAP_REQUEST` — overridable to any string
|
||||
list (e.g. multi-version protocols).
|
||||
- On match: runs `plugin.discover()` and returns a `BOOTSTRAP_RESPONSE`
|
||||
message with `payload: { peers }` and metadata enrichment
|
||||
(`bootstrapPeerCount`, `bootstrapTimestamp`).
|
||||
- Non-matching incoming messages pass through unchanged.
|
||||
- Outgoing messages always pass through unchanged.
|
||||
- Injectable `clock` for deterministic timestamp tests.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BootstrapPlugin,
|
||||
createBootstrapMiddleware,
|
||||
} from "@suaveplan/network-bootstrap";
|
||||
|
||||
const plugin = new BootstrapPlugin({ strategies: [seedStrategy] });
|
||||
const middleware = createBootstrapMiddleware(plugin);
|
||||
pipeline.use(middleware);
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
BootstrapPlugin,
|
||||
createBootstrapMiddleware,
|
||||
} from "@suaveplan/network-bootstrap";
|
||||
|
||||
const plugin = new BootstrapPlugin({
|
||||
strategies: [seedStrategy],
|
||||
discoveryTimeoutMs: 1500,
|
||||
});
|
||||
|
||||
const middleware = createBootstrapMiddleware(plugin, {
|
||||
interceptTypes: ["BOOTSTRAP_REQUEST", "PEER_DISCOVERY_REQUEST"],
|
||||
clock: () => fixedTimestamp,
|
||||
});
|
||||
|
||||
pipeline.use(middleware);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createBootstrapMiddleware(plugin: BootstrapPlugin, config?:
|
||||
Partial<BootstrapMiddlewareConfig>): MessageMiddleware` — throws
|
||||
`NetBootstrapError(INVALID_OPTIONS)` on Zod parse failure. The returned
|
||||
middleware has `name === "bootstrap-middleware"` and `priority === 2`.
|
||||
|
||||
`MessageMiddleware`:
|
||||
|
||||
- `onIncoming(message, context): Promise<MiddlewareMessage | null>`.
|
||||
- `onOutgoing(message, context): Promise<MiddlewareMessage | null>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The middleware is a thin lambda over `plugin.discover()`; it deliberately
|
||||
does not maintain its own state so two consumers can mount the same plugin
|
||||
behind two separate pipelines without coordination. Priority `2` slots it
|
||||
after authentication / decoding middleware (which typically run at
|
||||
priorities 0 and 1) but before application-level handlers — bootstrap
|
||||
should never reach domain logic. The Zod schema rejects empty
|
||||
`interceptTypes` strings so callers cannot accidentally disable the
|
||||
middleware via configuration silently.
|
||||
@@ -0,0 +1,109 @@
|
||||
# BootstrapPlugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`BootstrapPlugin` implements the `@suaveplan/network` `NetworkPlugin`
|
||||
contract and wires the bootstrap subsystem (peer cache + discovery chain)
|
||||
into a `NetworkNode`. It is the canonical entry point consumers register
|
||||
with `node.use(new BootstrapPlugin(...))` to give a freshly started node a
|
||||
way to find its first peers.
|
||||
|
||||
## Features
|
||||
|
||||
- `NetworkPlugin` lifecycle: `install` / `start` / `stop` / `dispose`,
|
||||
every method idempotent.
|
||||
- Zod-validated configuration at construction (throws
|
||||
`NetBootstrapError(INVALID_OPTIONS)` on malformed input).
|
||||
- Builds an internal `PeerCache` and `DiscoveryChain` from config; both are
|
||||
retrievable via accessors for inspection.
|
||||
- Auto-emits `bootstrap:*` events onto the node's `NetworkBus`.
|
||||
- `discover(signal?)` runs a manual discovery round and merges new peers
|
||||
into the cache.
|
||||
- `dispose()` clears the cache and severs all references — re-install on a
|
||||
disposed plugin is rejected with a structured error.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { BootstrapPlugin } from "@suaveplan/network-bootstrap";
|
||||
|
||||
const plugin = new BootstrapPlugin({
|
||||
strategies: [seedStrategy],
|
||||
discoveryTimeoutMs: 2000,
|
||||
});
|
||||
const node = new NetworkNode().use(plugin);
|
||||
await node.start();
|
||||
const peers = plugin.peerCache?.getAll() ?? [];
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { MemoryStorage } from "@suaveplan/storage";
|
||||
import { BootstrapPlugin } from "@suaveplan/network-bootstrap";
|
||||
|
||||
const storage = new MemoryStorage();
|
||||
const plugin = new BootstrapPlugin({
|
||||
strategies: [dhtStrategy, mdnsStrategy, seedStrategy],
|
||||
cache: {
|
||||
maxPeers: 500,
|
||||
ttlMs: 600_000,
|
||||
persistKey: "net:peers",
|
||||
storage,
|
||||
},
|
||||
discoveryTimeoutMs: 2500,
|
||||
maxConcurrentDiscoveries: 4,
|
||||
retry: {
|
||||
maxAttempts: 4,
|
||||
initialDelayMs: 200,
|
||||
maxDelayMs: 5000,
|
||||
factor: 2,
|
||||
},
|
||||
maxCollectedPeers: 20,
|
||||
});
|
||||
|
||||
const node = new NetworkNode().use(plugin);
|
||||
node.networkBus.on("bootstrap:exhausted", (ev) => {
|
||||
console.log(`bootstrap done — ${ev.collected} peers`);
|
||||
});
|
||||
await node.start();
|
||||
|
||||
// Manual re-discovery later:
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
await plugin.discover(controller.signal);
|
||||
|
||||
await node.stop();
|
||||
await node.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new BootstrapPlugin(config?: Partial<BootstrapPluginConfig>)` — throws
|
||||
`NetBootstrapError(INVALID_OPTIONS)` on Zod parse failure.
|
||||
- `install(node: NetworkNode): void` — idempotent.
|
||||
- `start(): Promise<void>` — throws `INVALID_OPTIONS` if called before
|
||||
`install` or after `dispose`.
|
||||
- `stop(): void` — idempotent.
|
||||
- `dispose(): void` — idempotent; releases internal references.
|
||||
- `discover(signal?: AbortSignal): Promise<PeerInfo[]>` — manual discovery
|
||||
round; merges results into the cache.
|
||||
- `get peerCache(): PeerCache | undefined`.
|
||||
- `get discoveryChain(): DiscoveryChain | undefined`.
|
||||
- `get isStarted(): boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The plugin keeps `install`, `start`, `stop`, and `dispose` strictly
|
||||
idempotent so it composes with the `PluginManager`'s reverse-order
|
||||
teardown without surprises. Disposed plugins reject `install` and `start`
|
||||
with `INVALID_OPTIONS` rather than silently no-op — silent failure on a
|
||||
disposed plugin is a footgun. The `NetworkBus` cast at install time is
|
||||
narrowed to `NetworkBus<BootstrapEventMap>` because the node's bus is
|
||||
generic — the cast is safe because the bus accepts any subset of its key
|
||||
union via the structural `EventBus` shape.
|
||||
119
docs/networking/network/network-bootstrap/src/types/types.md
Normal file
119
docs/networking/network/network-bootstrap/src/types/types.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# network-bootstrap types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
This module owns the shared type vocabulary for `@suaveplan/network-bootstrap`.
|
||||
Every public option struct, event payload, and pluggable contract lives here so
|
||||
the rest of the package can stay focused on behaviour. Types are
|
||||
implementation-agnostic — they describe shapes, not strategies, so consumers
|
||||
can supply their own discovery strategies (mDNS, DHT, hardcoded seeds, cached
|
||||
peers, ...) and still satisfy the `BootstrapPlugin` contract.
|
||||
|
||||
## Features
|
||||
|
||||
- `PeerInfo` — discovered-peer record (id, addresses, lastSeen, source).
|
||||
- `DiscoveryStrategy` — pluggable discovery contract with `AbortSignal` on
|
||||
every async method.
|
||||
- `PeerCacheStorageLike` — minimal storage subset the cache needs (subset of
|
||||
`@suaveplan/storage` `AbstractStorage` so the cache does not couple to every
|
||||
storage capability).
|
||||
- `PeerCacheConfig`, `BootstrapPluginConfig`, `BootstrapRetryConfig` — public
|
||||
option structs (each has a matching Zod schema in `types.schemas.ts`).
|
||||
- `BootstrapMiddlewareConfig`, `MessageMiddleware`, `MiddlewareMessage`,
|
||||
`MiddlewareContext` — bootstrap middleware contract.
|
||||
- `BootstrapEventMap` — typed event payloads emitted on the `NetworkBus`:
|
||||
`bootstrap:started`, `bootstrap:peer-found`, `bootstrap:exhausted`.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
BootstrapPluginConfig,
|
||||
DiscoveryStrategy,
|
||||
} from "@suaveplan/network-bootstrap/types";
|
||||
|
||||
const seedStrategy: DiscoveryStrategy = {
|
||||
name: "seed",
|
||||
async discover() {
|
||||
return [
|
||||
{
|
||||
peerId: "p1",
|
||||
addresses: ["/ip4/127.0.0.1/tcp/9001"],
|
||||
lastSeen: Date.now(),
|
||||
source: "seed",
|
||||
},
|
||||
];
|
||||
},
|
||||
async isAvailable() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const config: Partial<BootstrapPluginConfig> = {
|
||||
strategies: [seedStrategy],
|
||||
discoveryTimeoutMs: 2000,
|
||||
};
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
BootstrapPluginConfig,
|
||||
PeerCacheConfig,
|
||||
} from "@suaveplan/network-bootstrap/types";
|
||||
|
||||
const cache: Partial<PeerCacheConfig> = {
|
||||
maxPeers: 500,
|
||||
ttlMs: 60_000,
|
||||
persistKey: "net:bootstrap:peers",
|
||||
clock: () => Date.now(),
|
||||
};
|
||||
|
||||
const config: BootstrapPluginConfig = {
|
||||
strategies: [],
|
||||
cache,
|
||||
discoveryTimeoutMs: 3000,
|
||||
maxConcurrentDiscoveries: 4,
|
||||
retry: {
|
||||
maxAttempts: 3,
|
||||
initialDelayMs: 200,
|
||||
maxDelayMs: 5000,
|
||||
factor: 2,
|
||||
},
|
||||
maxCollectedPeers: 20,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface PeerInfo` — `{ peerId, addresses, lastSeen, source }`.
|
||||
- `interface DiscoveryStrategy` — `discover(timeoutMs, signal?)` +
|
||||
`isAvailable(signal?)`.
|
||||
- `interface PeerCacheStorageLike` — `get` / `set` / `delete`.
|
||||
- `interface PeerCacheConfig` — `maxPeers`, `ttlMs`, optional `persistKey`,
|
||||
`storage`, `clock`.
|
||||
- `interface BootstrapRetryConfig` — `maxAttempts`, `initialDelayMs`,
|
||||
`maxDelayMs`, `factor`.
|
||||
- `interface BootstrapPluginConfig` — `strategies`, `cache`,
|
||||
`discoveryTimeoutMs`, `maxConcurrentDiscoveries`, `retry`,
|
||||
`maxCollectedPeers`, optional `clock`.
|
||||
- `interface BootstrapMiddlewareConfig` — `interceptTypes`, optional `clock`.
|
||||
- `interface MessageMiddleware` — `name`, `priority`, `onIncoming?`,
|
||||
`onOutgoing?`.
|
||||
- `interface BootstrapEventMap` — `bootstrap:started` /
|
||||
`bootstrap:peer-found` / `bootstrap:exhausted`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every async method on these contracts accepts an `AbortSignal` — bootstrap
|
||||
must be cancellable end-to-end. `PeerCacheStorageLike` is intentionally narrow:
|
||||
`AbstractStorage` from `@suaveplan/storage` is a structural superset and slots
|
||||
in directly without an adapter. `BootstrapEventMap` keys match the bus topics
|
||||
defined in the proposal so consumers can subscribe with the same string
|
||||
literals. Types here are pure — no runtime code — so the file is excluded from
|
||||
docs-check via the standard `*.types.ts` rule by being placed under
|
||||
`src/types/` flat layout (which the docs-check rule explicitly allows).
|
||||
Reference in New Issue
Block a user