first commit
This commit is contained in:
114
docs/networking/network/network-discovery-peerjs/README.md
Normal file
114
docs/networking/network/network-discovery-peerjs/README.md
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
|
||||
# peer-connection-manager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Owns the lifecycle of the `PeerLike` instance and the per-room `Map` of
|
||||
WebRTC `DataConnectionLike` channels. Wraps the lower-level helpers in
|
||||
`peerjs-connection.ts` behind a class so the orchestrator and the interval
|
||||
loops can share one canonical view of the connection registry, the local
|
||||
peer id, and the broker handle. Adds explicit `DISPOSED` semantics so a
|
||||
caller cannot accidentally re-use a torn-down manager.
|
||||
|
||||
## Features
|
||||
|
||||
- `initialize(configuredPeerId, signal?)` resolves on broker `open`, threads
|
||||
`AbortSignal` through, rejects with `SIGNALING_FAILED` on error.
|
||||
- Incoming `peer.on("connection")` events flow through `acceptIncoming` →
|
||||
`handleConnection` and emit the orchestrator's `onIncomingConnection`
|
||||
callback for any per-peer bookkeeping (e.g. emitting `peerjs:peer-found`).
|
||||
- `connectToPeer(peerId)` is idempotent on the registry and throws typed
|
||||
errors instead of returning early.
|
||||
- `broadcast(message)` fans out via `broadcastMessage`.
|
||||
- `destroy()` closes every channel and destroys the `PeerLike`, swallowing
|
||||
per-channel `close()` errors so disposal never throws halfway through.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PeerConnectionManager } from "@suaveplan/network-discovery-peerjs/peer-connection-manager";
|
||||
|
||||
const mgr = new PeerConnectionManager(factory, peerOpts, {
|
||||
onIncomingConnection: (c) => console.log("inbound", c.peer),
|
||||
onMessage: (raw, c) => handleMessage(raw, c, deps),
|
||||
onPeerLost: (id) => deps.handlePeerLost(id),
|
||||
});
|
||||
await mgr.initialize(undefined);
|
||||
mgr.broadcast({ type: "announce", from: mgr.getMyPeerId()!, payload: meta, timestamp: Date.now() });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PeerConnectionManager } from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
const controller = new AbortController();
|
||||
const mgr = new PeerConnectionManager(factory, peerOpts, callbacks);
|
||||
try {
|
||||
const myPeerId = await mgr.initialize(configured, controller.signal);
|
||||
for (const target of seedPeers) {
|
||||
try {
|
||||
mgr.connectToPeer(target);
|
||||
} catch (e) {
|
||||
if ((e as { code?: string }).code === "NET_DISC_PEERJS.PEER_CONNECT_FAILED") {
|
||||
failed.add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mgr.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new PeerConnectionManager(factory, peerOptions, callbacks)` — callbacks
|
||||
are `{onIncomingConnection, onMessage, onPeerLost}`.
|
||||
- `initialize(configuredPeerId?, signal?): Promise<string>` — returns the
|
||||
assigned local peer id.
|
||||
- `destroy(): void` — close + destroy. Idempotent.
|
||||
- `getMyPeerId(): string | null`.
|
||||
- `getConnections(): ReadonlyMap<string, DataConnectionLike>`.
|
||||
- `hasConnection(peerId): boolean`.
|
||||
- `removeConnection(peerId): void` — does not call `close()`.
|
||||
- `connectToPeer(peerId): void` — throws `DISPOSED` or `PEER_CONNECT_FAILED`.
|
||||
- `broadcast(message: PeerJSMessage): void`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `disposed` flag is the only mutable lifecycle state — every guard
|
||||
inspects it before touching the broker. `destroy()` clears the connections
|
||||
map before destroying the peer so that any synchronous `close` event PeerJS
|
||||
fires during teardown does not race against a registry mutation. The class
|
||||
deliberately exposes `getConnections()` as a `ReadonlyMap` rather than a
|
||||
cloned snapshot to keep the hot-path heartbeat broadcast allocation-free.
|
||||
@@ -0,0 +1,88 @@
|
||||
# peerjs-connection
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Thin helpers around the `PeerLike` / `DataConnectionLike` structural surface
|
||||
that own the WebRTC data-channel lifecycle: opening the peer against the
|
||||
signalling server, wiring up the inbound `connection` / `data` / `error` /
|
||||
`close` events, opening outbound `peer.connect(...)` calls, and broadcasting
|
||||
to every open connection. Keeping the helpers as pure functions over the
|
||||
injectable interfaces means the orchestrator stays small and every helper is
|
||||
trivially unit-testable without a real PeerJS broker.
|
||||
|
||||
## Features
|
||||
|
||||
- `initializePeer` resolves on `peer.on("open")`, rejects with
|
||||
`SIGNALING_FAILED` on `peer.on("error")` or constructor-throw.
|
||||
- `AbortSignal` support — pre-aborted signals reject immediately; mid-flight
|
||||
aborts call `peer.destroy()` and reject with the signal's `reason`.
|
||||
- `handleConnection` wires open/data/error/close idempotently and adds the
|
||||
connection to a shared registry on `open`.
|
||||
- `connectToPeer` is idempotent on the registry and translates
|
||||
synchronous `peer.connect` failures to `PEER_CONNECT_FAILED`.
|
||||
- `broadcastMessage` skips closed connections and swallows per-connection
|
||||
`send` errors so a single misbehaving peer cannot break the broadcast.
|
||||
- Defensive guard around the user-supplied `onMessage` callback — a thrown
|
||||
handler does not kill the channel.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { initializePeer, broadcastMessage } from "@suaveplan/network-discovery-peerjs/peerjs-connection";
|
||||
|
||||
const { peer, myPeerId } = await initializePeer(factory, "", {
|
||||
host: "signaling.example.com",
|
||||
port: 443,
|
||||
path: "/",
|
||||
secure: true,
|
||||
debug: 0,
|
||||
config: { iceServers: [] },
|
||||
});
|
||||
broadcastMessage({ type: "heartbeat", from: myPeerId, payload: {}, timestamp: Date.now() }, connections);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { connectToPeer, handleConnection } from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
const controller = new AbortController();
|
||||
const { peer, myPeerId } = await initializePeer(factory, "", opts, controller.signal);
|
||||
|
||||
peer.on("connection", (conn) => handleConnection(conn, connections, onRaw, onLost));
|
||||
|
||||
for (const id of remotePeerIds) {
|
||||
try {
|
||||
connectToPeer(peer, id, connections, onRaw, onLost);
|
||||
} catch (err) {
|
||||
if ((err as { code?: string }).code === "NET_DISC_PEERJS.PEER_CONNECT_FAILED") {
|
||||
failures.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `initializePeer(factory, peerId, options, signal?) → Promise<{peer, myPeerId}>`.
|
||||
Rejects with `NetDiscPeerJSError` code `SIGNALING_FAILED` on broker error.
|
||||
- `handleConnection(connection, connections, onMessage, onPeerLost)` — wires
|
||||
the four data-channel events, mutates the `connections` map on `open`.
|
||||
- `connectToPeer(peer, peerId, connections, onMessage, onPeerLost) →
|
||||
DataConnectionLike | undefined` — `undefined` when already connected;
|
||||
throws `NetDiscPeerJSError` code `PEER_CONNECT_FAILED` on connect failure.
|
||||
- `broadcastMessage(message, connections) → void` — fan-out send.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `settle` helper in `initializePeer` guarantees the promise resolves
|
||||
exactly once even if `peer.on("open")` and `peer.on("error")` both fire (or
|
||||
the signal aborts in between). The `signal.removeEventListener` cleanup
|
||||
keeps the listener set bounded so this helper is safe to call repeatedly
|
||||
across reconnects. Defensive `try/catch` around the user `onMessage` is the
|
||||
only `catch {}` in the package — message handlers must never throw because
|
||||
PeerJS surfaces unhandled exceptions to the data-channel error path and would
|
||||
disconnect the peer.
|
||||
@@ -0,0 +1,101 @@
|
||||
# peerjs-discovery
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Top-level orchestrator for the PeerJS discovery strategy. Implements
|
||||
`DiscoveryStrategy` from `@suaveplan/network-bootstrap` so the strategy can
|
||||
slot directly into a `DiscoveryChain`. Wires the `PeerConnectionManager`,
|
||||
`RoomManager`, `PeerJSIntervals`, message-handler dispatch table, and OTel
|
||||
surface together behind a class that takes a single Zod-validated
|
||||
`PeerJSDiscoveryConfig`. Refuses construction without an explicit `host`.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `DiscoveryStrategy` (`name`, `discover(timeoutMs, signal?)`,
|
||||
`isAvailable(signal?)`).
|
||||
- `start(signal?)` / `stop(signal?)` / `dispose()` lifecycle, all idempotent.
|
||||
- Mandatory `host` + `peerFactory` — constructor throws `INVALID_OPTIONS`.
|
||||
No public-`peerjs.com` fallback is reachable from this code.
|
||||
- `AbortSignal` threaded through `start`, `stop`, `discover`,
|
||||
`isAvailable`, `connect`.
|
||||
- Optional `EventBusLike<PeerJSEventMap>` integration for typed event fan-out.
|
||||
- Optional `ServiceRegistry` integration — discovered peers register under
|
||||
the `peerjs:<room>` service name.
|
||||
- OTel: `disc_peerjs.start` span, `peers_discovered_total{outcome}` counter,
|
||||
`signaling_duration_ms` histogram.
|
||||
- `DISPOSED` semantics — every public method after `dispose()` throws.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PeerJSDiscovery } from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
const discovery = new PeerJSDiscovery({
|
||||
host: "signaling.example.com",
|
||||
room: "demo",
|
||||
peerFactory: realPeerJsFactory,
|
||||
});
|
||||
|
||||
await discovery.start();
|
||||
const peers = await discovery.discover(5_000);
|
||||
await discovery.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PeerJSDiscovery } from "@suaveplan/network-discovery-peerjs";
|
||||
import { DiscoveryChain } from "@suaveplan/network-bootstrap";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { ServiceRegistry } from "@suaveplan/service-discovery";
|
||||
|
||||
const bus = new EventBus<import("@suaveplan/network-discovery-peerjs/types").PeerJSEventMap>();
|
||||
const registry = new ServiceRegistry({ maxServices: 1000, ttlMs: 120_000 });
|
||||
|
||||
const strategy = new PeerJSDiscovery({
|
||||
host: "signaling.example.com",
|
||||
port: 8443,
|
||||
secure: true,
|
||||
room: "alpha",
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
peerFactory: realPeerJsFactory,
|
||||
bus,
|
||||
registry,
|
||||
});
|
||||
|
||||
bus.on("peerjs:peer-discovered", (e) => console.log("found", e.peer.id));
|
||||
|
||||
const chain = new DiscoveryChain({ strategies: [strategy] });
|
||||
const controller = new AbortController();
|
||||
const found = await chain.discover(2_000, controller.signal);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new PeerJSDiscovery(config, clock?)` — `clock` defaults to `Date.now`.
|
||||
- `name: "peerjs"`.
|
||||
- `config: ResolvedPeerJSDiscoveryConfig`.
|
||||
- `start(signal?): Promise<void>`.
|
||||
- `stop(signal?): Promise<void>`.
|
||||
- `discover(timeoutMs, signal?): Promise<PeerInfo[]>` — returns bootstrap-
|
||||
shaped `PeerInfo` records (`{peerId, addresses, lastSeen, source}`).
|
||||
- `isAvailable(signal?): Promise<boolean>`.
|
||||
- `connect(peerId, signal?): Promise<void>`.
|
||||
- `getMyPeerId(): string | null`.
|
||||
- `getPeers(): readonly PeerJSPeerRecord[]`.
|
||||
- `getPeerCount(): number`.
|
||||
- `dispose(): void`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The orchestrator deliberately keeps no state of its own beyond `running`,
|
||||
`disposed`, and `myPeerId`: every other piece of state belongs to a
|
||||
sub-module (room, connection manager, intervals). `start()` records signal
|
||||
duration before flipping `running` so a slow broker handshake counts cleanly
|
||||
even if it ends up failing. `stop()` short-circuits when called after abort,
|
||||
skipping the `leave` broadcast because the transport is already gone. The
|
||||
config is parsed via the strict Zod schema before any side effects so a
|
||||
malformed call cannot wire up partial state.
|
||||
@@ -0,0 +1,87 @@
|
||||
# peerjs-intervals
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Replaces upstream's three raw `setInterval` calls with a single `Scheduler`
|
||||
from `@suaveplan/timing` running three self-rearming loops (announce,
|
||||
heartbeat, cleanup). The injectable `Clock` makes every cadence deterministic
|
||||
under test and the `scheduler.dispose()` atomicity guarantees `stop()` cannot
|
||||
leak pending timers — the upstream package had no equivalent and required
|
||||
manual `clearInterval` on every handle.
|
||||
|
||||
## Features
|
||||
|
||||
- Three self-rearming loops at independent cadences.
|
||||
- Single underlying `Scheduler` — `stop()` disposes every pending task at
|
||||
once.
|
||||
- Injectable `Clock` everywhere (`clock()` is the canonical timestamp source).
|
||||
- Idempotent `start()` / `stop()` — safe to call from any lifecycle hook.
|
||||
- Optional `schedulerFactory` override for unit tests that need a fake
|
||||
scheduler.
|
||||
- Heartbeat and cleanup loops are pure; announce is async to mirror the
|
||||
broadcast `await` semantics callers may expect.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PeerJSIntervals } from "@suaveplan/network-discovery-peerjs/peerjs-intervals";
|
||||
|
||||
const intervals = new PeerJSIntervals({
|
||||
clock: () => Date.now(),
|
||||
announceIntervalMs: 30_000,
|
||||
heartbeatIntervalMs: 15_000,
|
||||
cleanupIntervalMs: 30_000,
|
||||
timeoutMs: 60_000,
|
||||
connections,
|
||||
discoveredPeers,
|
||||
getMyPeerId: () => myPeerId,
|
||||
isRunning: () => running,
|
||||
selfMetadata: () => ({ id: myPeerId, addresses: [], metadata: {}, lastSeen: Date.now() }),
|
||||
handlePeerLost: (id) => removePeer(id),
|
||||
});
|
||||
intervals.start();
|
||||
// later
|
||||
intervals.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PeerJSIntervals } from "@suaveplan/network-discovery-peerjs";
|
||||
import { createScheduler } from "@suaveplan/timing";
|
||||
|
||||
let fakeNow = 0;
|
||||
const fakeClock = () => fakeNow;
|
||||
const intervals = new PeerJSIntervals(
|
||||
{ ...deps, clock: fakeClock },
|
||||
(clock) => createScheduler({ clock })
|
||||
);
|
||||
intervals.start();
|
||||
fakeNow += 30_000;
|
||||
// trigger scheduler ticks via the test scheduler in your harness...
|
||||
intervals.stop();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PeerJSIntervalDeps` — `{clock, announceIntervalMs, heartbeatIntervalMs,
|
||||
cleanupIntervalMs, timeoutMs, connections, discoveredPeers, getMyPeerId,
|
||||
isRunning, selfMetadata, handlePeerLost}`.
|
||||
- `new PeerJSIntervals(deps, schedulerFactory?)` — second arg overrides the
|
||||
scheduler construction (defaults to `createScheduler({ clock })`).
|
||||
- `start(): void` — kick off all three loops. Idempotent.
|
||||
- `stop(): void` — dispose every pending timer. Idempotent.
|
||||
- `isStopped: boolean` — true after `stop()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Each `schedule(fn, delay)` callback re-arms itself at the tail to keep the
|
||||
underlying scheduler one-shot. Re-arm is guarded by `stopped` and
|
||||
`scheduler.disposed` so a callback that fires the same tick as `stop()` does
|
||||
not enqueue a new task against a disposed scheduler. The announce loop awaits
|
||||
the broadcast before re-arming so a slow signalling path stretches the
|
||||
effective interval rather than stacking sends, which matches the upstream
|
||||
behaviour but without the `setInterval` pile-up.
|
||||
@@ -0,0 +1,90 @@
|
||||
# peerjs-message-handlers
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Pure dispatch table for inbound PeerJS discovery messages. Validates every
|
||||
incoming wire payload through `PeerJSMessageSchema` (Zod) at the boundary and
|
||||
routes well-formed messages to the matching case: `announce`, `discover`,
|
||||
`peer_list`, `leave`, `heartbeat`. The handlers are intentionally stateless —
|
||||
they never mutate the peer set directly. State lives in the orchestrator's
|
||||
`RoomManager`, which the handlers call via the `admitPeer` / `touchPeer` /
|
||||
`onPeerLost` callbacks. This split avoids double-bookkeeping bugs where two
|
||||
places race to set the same entry.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated boundary — malformed messages drop silently instead of
|
||||
crashing the strategy (discovery must tolerate hostile peers).
|
||||
- Exhaustive switch over the wire-format enum with safe-fallthrough.
|
||||
- Stateless: every state mutation is delegated to caller-supplied callbacks.
|
||||
- `admitPeer` returns `"added" | "refreshed" | "rejected"` so the room can
|
||||
enforce `maxPeers` admission centrally; the handler only reports the
|
||||
outcome (`false` when `"rejected"`).
|
||||
- Discover-request replies are skipped when the connection is closed,
|
||||
guarding against a race between `discover` arrival and channel teardown.
|
||||
- `knownPeerIds()` is a callback so the handler does not need to hold a Map
|
||||
reference, keeping memory ownership inside the room manager.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { handleMessage } from "@suaveplan/network-discovery-peerjs/peerjs-message-handlers";
|
||||
|
||||
handleMessage(raw, conn, {
|
||||
myPeerId,
|
||||
clock: () => Date.now(),
|
||||
knownPeerIds: () => room.listPeers().map((p) => p.id),
|
||||
admitPeer: (r) => room.admit(r),
|
||||
touchPeer: (id) => room.touch(id),
|
||||
onPeerLost: (id) => room.drop(id),
|
||||
connectToPeer: (id) => discovery.connect(id),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { handleMessage } from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
const deps = {
|
||||
myPeerId: "alpha",
|
||||
clock,
|
||||
knownPeerIds: () => Array.from(state.keys()),
|
||||
admitPeer(r) {
|
||||
const action = state.has(r.id) ? "refreshed" : "added";
|
||||
state.set(r.id, r);
|
||||
if (action === "added") metrics.peerFound.inc();
|
||||
return action;
|
||||
},
|
||||
touchPeer: (id) => (state.get(id) ? (state.get(id)!.lastSeen = clock(), true) : false),
|
||||
onPeerLost: (id) => state.delete(id),
|
||||
connectToPeer: (id) => discovery.connect(id),
|
||||
};
|
||||
|
||||
socket.on("data", (raw) => {
|
||||
const accepted = handleMessage(raw, connection, deps);
|
||||
if (!accepted) logger.debug("dropped malformed peerjs message");
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PeerJSMessageHandlerDeps` — `{myPeerId, clock, knownPeerIds, admitPeer,
|
||||
touchPeer, onPeerLost, connectToPeer}`.
|
||||
- `handleMessage(raw, connection, deps): boolean` — returns `true` when the
|
||||
message was accepted and routed, `false` when it was dropped (Zod failure,
|
||||
admission rejected, heartbeat for unknown peer, discover with no local
|
||||
peer id, send-to-closed-connection).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The module never throws — drop-on-error is intentional because PeerJS data
|
||||
channels can carry payloads from arbitrary remote peers and a strategy that
|
||||
crashes on bad input would create a trivial DoS vector. The `default` arm of
|
||||
the switch is unreachable in practice because Zod's enum constraint already
|
||||
rejects unknown types; an `/* c8 ignore */` annotation acknowledges that for
|
||||
the coverage gate. Fire-and-forget on `connectToPeer` is the same model
|
||||
upstream used — the orchestrator owns retry policy.
|
||||
@@ -0,0 +1,80 @@
|
||||
# room-manager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Owns the logical-room view of discovered peers, layered on top of the raw
|
||||
PeerJS connection registry. Provides admission (`maxPeers` cap), TTL-driven
|
||||
pruning, deduped `lastSeen` refresh, and optional fan-out to both an
|
||||
`EventBusLike` and a `ServiceRegistry` so the strategy can mirror state into
|
||||
shared discovery infrastructure without baking either dependency in.
|
||||
|
||||
## Features
|
||||
|
||||
- Deduped peer set with the `maxPeers` admission cap enforced inside
|
||||
`admit()` — `ROOM_FULL` outcomes increment the OTel counter and return a
|
||||
discriminated `"rejected"`.
|
||||
- `touch(peerId)` updates `lastSeen` in place — used by heartbeats.
|
||||
- `pruneStale(timeoutMs)` iterates the map once per cleanup tick and drops
|
||||
every peer past TTL, emitting `peerjs:peer-lost` for each.
|
||||
- Optional bus emissions on every state transition (`peerjs:peer-discovered`,
|
||||
`peerjs:peer-lost`).
|
||||
- Optional `ServiceRegistry` integration — discovered peers are registered as
|
||||
`peerjs:<room>` services keyed by peer id, deregistered on drop.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RoomManager } from "@suaveplan/network-discovery-peerjs/room-manager";
|
||||
|
||||
const room = new RoomManager(() => Date.now(), "demo", 100, undefined, undefined);
|
||||
const action = room.admit({ id: "alpha", addresses: [], metadata: {}, lastSeen: Date.now() });
|
||||
// action === "added"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RoomManager } from "@suaveplan/network-discovery-peerjs";
|
||||
import { ServiceRegistry } from "@suaveplan/service-discovery";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const bus = new EventBus<import("@suaveplan/network-discovery-peerjs/types").PeerJSEventMap>();
|
||||
const registry = new ServiceRegistry({ maxServices: 1000, ttlMs: 60_000 });
|
||||
const room = new RoomManager(() => Date.now(), "alpha", 50, bus, registry);
|
||||
|
||||
bus.on("peerjs:peer-lost", (e) => console.log("lost", e.peerId));
|
||||
|
||||
// every announce arriving from the wire:
|
||||
room.admit(record);
|
||||
// every heartbeat:
|
||||
room.touch(record.id);
|
||||
// once per cleanup tick:
|
||||
const dropped = room.pruneStale(60_000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new RoomManager(clock, room, maxPeers, bus?, registry?)`.
|
||||
- `getPeers(): ReadonlyMap<string, PeerJSPeerRecord>`.
|
||||
- `listPeers(): readonly PeerJSPeerRecord[]`.
|
||||
- `getPeer(peerId): PeerJSPeerRecord | undefined`.
|
||||
- `size(): number`.
|
||||
- `admit(record): "added" | "refreshed" | "rejected"`.
|
||||
- `touch(peerId): boolean` — `false` if peer unknown.
|
||||
- `drop(peerId): boolean` — `false` if peer unknown.
|
||||
- `pruneStale(timeoutMs): number` — count dropped.
|
||||
- `clear(): void` — drop every peer.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`admit` writes a fresh record (spread-copy) on first admission to seal the
|
||||
`metadata` and `capabilities` references against caller mutation, then
|
||||
mutates `lastSeen` in place on subsequent refreshes to keep the hot heartbeat
|
||||
path allocation-free. Pruning is done in a single pass over `entries()` and
|
||||
uses `drop()` so emissions and registry deregistration stay consistent with
|
||||
manual drops. The registry's `address` defaults to the first multiaddr or
|
||||
falls back to the peer id so `ServiceRegistry.register` never rejects on an
|
||||
empty `address` field.
|
||||
@@ -0,0 +1,78 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Structured-error registry for `@suaveplan/network-discovery-peerjs`. Defines the
|
||||
five frozen `NET_DISC_PEERJS_*` codes every public boundary uses and the
|
||||
`NetDiscPeerJSError` class that wraps them with `SuaveplanError`'s standard
|
||||
chained-cause + i18n-key plumbing. Centralising the registry here keeps every
|
||||
throw site consistent and makes branching on `error.code` reliable across the
|
||||
package and any downstream `network-bootstrap` chain consuming the strategy.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `defineCodes("NET_DISC_PEERJS", [...])` registry — code names are
|
||||
string-literal types, not arbitrary strings.
|
||||
- `NetDiscPeerJSError` extends `SuaveplanError` with the discriminated code type.
|
||||
- Stable code surface for downstream callers that switch on `error.code`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_DISC_PEERJS_CODES,
|
||||
NetDiscPeerJSError,
|
||||
} from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
throw new NetDiscPeerJSError(
|
||||
NET_DISC_PEERJS_CODES.INVALID_OPTIONS,
|
||||
"errors.peerjs.invalid_options"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_DISC_PEERJS_CODES,
|
||||
NetDiscPeerJSError,
|
||||
type NetDiscPeerJSErrorCode,
|
||||
} from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
function classify(err: unknown): NetDiscPeerJSErrorCode | "unknown" {
|
||||
if (err instanceof NetDiscPeerJSError) return err.code;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
try {
|
||||
await discovery.start();
|
||||
} catch (err) {
|
||||
const code = classify(err);
|
||||
if (code === NET_DISC_PEERJS_CODES.SIGNALING_FAILED) {
|
||||
// back off + retry against an alternate broker URL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NET_DISC_PEERJS_CODES` — frozen record with five members:
|
||||
`SIGNALING_FAILED`, `PEER_CONNECT_FAILED`, `ROOM_FULL`, `INVALID_OPTIONS`,
|
||||
`DISPOSED`.
|
||||
- `NetDiscPeerJSErrorCode` — string-literal union derived from the registry.
|
||||
- `NetDiscPeerJSError(code, messageKey, options?)` — `SuaveplanError` subclass.
|
||||
Accepts the standard `SuaveplanErrorOptions` (cause, params, redaction).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`SIGNALING_FAILED` is the wrapper for any error PeerJS surfaces during the
|
||||
initial `peer.on("error")` handshake. `PEER_CONNECT_FAILED` is reserved for
|
||||
post-handshake `peer.connect(...)` failures so call-sites can tell apart
|
||||
"broker unreachable" from "specific peer unreachable". `ROOM_FULL` is emitted
|
||||
proactively when the `maxPeers` cap would be exceeded — the strategy refuses
|
||||
the new peer rather than evicting an existing one to keep room semantics
|
||||
predictable. `DISPOSED` is thrown by every public method after `stop()` so
|
||||
that lifecycle bugs surface loudly instead of silently no-op-ing.
|
||||
@@ -0,0 +1,101 @@
|
||||
# otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry surface for `@suaveplan/network-discovery-peerjs`. Owns the
|
||||
tracer, meter, and instrument handles used by the strategy and its supporting
|
||||
modules so every signal in the package is namespaced consistently
|
||||
(`disc_peerjs.*`). Centralising instrument creation here prevents accidental
|
||||
duplicate-instrument warnings from the OTel SDK when multiple modules want to
|
||||
record the same metric.
|
||||
|
||||
## Features
|
||||
|
||||
- `withStartSpan(fn)` wraps `PeerJSDiscovery.start()` in an active span called
|
||||
`disc_peerjs.start`, exposing the live span to the caller for attribute
|
||||
enrichment.
|
||||
- `recordPeersDiscovered(outcome, count?)` counter — labelled by `outcome` to
|
||||
distinguish fresh peers (`found`), repeats (`duplicate`), and `ROOM_FULL`
|
||||
rejections (`rejected`).
|
||||
- `recordSignalingDuration(ms)` histogram of broker-handshake wall time.
|
||||
- `withTransportSpan(name, fn)` wraps transport-tier async operations
|
||||
(`peerjs_transport.connect`, `peerjs_transport.disconnect`,
|
||||
`peerjs_transport.send`) in active spans.
|
||||
- `recordTransportMessage(direction, outcome)` counter labelled by
|
||||
`direction` (`inbound` / `outbound`) and `outcome` (`ok` / `error`).
|
||||
- `adjustTransportActivePeers(delta)` up-down counter exposed as a gauge of
|
||||
currently-connected transport peers.
|
||||
- Single shared `tracer` + `meter` instance with stable names.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordPeersDiscovered,
|
||||
recordSignalingDuration,
|
||||
withStartSpan,
|
||||
} from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
await withStartSpan(async (span) => {
|
||||
span.setAttribute("room", "demo");
|
||||
const t = performance.now();
|
||||
// ...open peer...
|
||||
recordSignalingDuration(performance.now() - t);
|
||||
recordPeersDiscovered("found", 1);
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { withStartSpan, recordPeersDiscovered } from "@suaveplan/network-discovery-peerjs";
|
||||
|
||||
async function discoverWithBudget(timeoutMs: number) {
|
||||
return withStartSpan(async (span) => {
|
||||
span.setAttribute("budget_ms", timeoutMs);
|
||||
const start = performance.now();
|
||||
try {
|
||||
const peers = await openSignalling(timeoutMs);
|
||||
recordPeersDiscovered("found", peers.length);
|
||||
return peers;
|
||||
} catch (err) {
|
||||
span.recordException(err as Error);
|
||||
recordPeersDiscovered("rejected");
|
||||
throw err;
|
||||
} finally {
|
||||
span.setAttribute("elapsed_ms", performance.now() - start);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `withStartSpan<T>(fn: (span: Span) => Promise<T>): Promise<T>` — creates an
|
||||
active span named `disc_peerjs.start`, ends it in `finally`.
|
||||
- `recordPeersDiscovered(outcome: "found"|"duplicate"|"rejected", count?)` —
|
||||
increments the `disc_peerjs.peers_discovered_total` counter.
|
||||
- `recordSignalingDuration(ms: number)` — records into the
|
||||
`disc_peerjs.signaling_duration_ms` histogram, clamping negative values to
|
||||
zero.
|
||||
- `PeerDiscoveryOutcome` — exported string-literal union used by the counter.
|
||||
- `withTransportSpan<T>(name: string, fn: (span: Span) => Promise<T>): Promise<T>` —
|
||||
active-span wrapper for transport-tier operations.
|
||||
- `recordTransportMessage(direction, outcome)` — increments
|
||||
`peerjs_transport.messages_total`.
|
||||
- `adjustTransportActivePeers(delta)` — adjusts
|
||||
`peerjs_transport.active_peers`. No-op when `delta === 0`.
|
||||
- `TransportMessageDirection` / `TransportMessageOutcome` — exported
|
||||
string-literal unions.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`outcome` keeps cardinality bounded to three values so downstream Prometheus
|
||||
dashboards remain efficient. The histogram clamps negative inputs because the
|
||||
strategy's signalling-duration measurement uses `Date.now()` differences that
|
||||
can occasionally drift on systems where NTP corrects the wall clock during a
|
||||
handshake. Tracer and meter names match the package name verbatim so the OTel
|
||||
resource attributes line up with the npm package id without further mapping.
|
||||
@@ -0,0 +1,107 @@
|
||||
# plugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`PeerJSPlugin` is the thin `NetworkPlugin` lifecycle wrapper that mounts
|
||||
`PeerJSTransport` onto a `@suaveplan/network` `NetworkNode`. It mirrors the
|
||||
canonical `WebRTCPlugin` / `WebSocketPlugin` shape: construct → install →
|
||||
start (no-op) → stop (closes transport + unregisters) → dispose (drops the
|
||||
node reference).
|
||||
|
||||
The plugin pairs with the rest of this package — the same `PeerLike` the
|
||||
transport opens can serve a `PeerJSDiscovery` instance side-by-side because
|
||||
the transport's wire frame uses a discriminator (`"suaveplan.transport.v1"`)
|
||||
that the discovery Zod schema explicitly rejects. Discovery + transport on a
|
||||
single PeerJS unit was the upstream model and is preserved here by folding
|
||||
both into one package.
|
||||
|
||||
## Features
|
||||
|
||||
- `name: "peerjs"` lookup key.
|
||||
- Constructs and owns one `PeerJSTransport` from a `PeerJSTransportConfig`.
|
||||
- `install(node)` calls `node.registerTransport(transport)`.
|
||||
- `start()` is a no-op (transport is lazy).
|
||||
- `stop()` closes the transport and unregisters it from the node.
|
||||
- `dispose()` drops the cached `NetworkNode` reference; safe to call
|
||||
before `install` or twice in a row.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { PeerJSPlugin } from "@suaveplan/network-discovery-peerjs/plugin";
|
||||
|
||||
const node = new NetworkNode();
|
||||
node.use(
|
||||
new PeerJSPlugin({
|
||||
host: "signalling.example.com",
|
||||
peerFactory: makePeerFactory(),
|
||||
})
|
||||
);
|
||||
await node.start();
|
||||
// ... use node.send / node.broadcast ...
|
||||
await node.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { PeerJSPlugin } from "@suaveplan/network-discovery-peerjs/plugin";
|
||||
|
||||
const plugin = new PeerJSPlugin({
|
||||
host: "signalling.example.com",
|
||||
port: 443,
|
||||
path: "/",
|
||||
secure: true,
|
||||
iceServers: [
|
||||
{ urls: "stun:stun.example.com:3478" },
|
||||
{
|
||||
urls: "turn:turn.example.com:3478",
|
||||
username: "u",
|
||||
credential: "p",
|
||||
},
|
||||
],
|
||||
peerId: "alice",
|
||||
debug: 0,
|
||||
peerFactory: makePeerFactory(),
|
||||
});
|
||||
|
||||
const node = new NetworkNode();
|
||||
plugin.install(node);
|
||||
plugin.start();
|
||||
|
||||
// Direct access to the transport for connect / disconnect.
|
||||
await plugin.transport.connect("bob", "ignored");
|
||||
plugin.transport.onPeerEvent((peerId, event) => {
|
||||
console.log(`peer ${peerId} ${event}`);
|
||||
});
|
||||
|
||||
await plugin.stop();
|
||||
plugin.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PeerJSPlugin implements NetworkPlugin`
|
||||
- `readonly name = "peerjs"`.
|
||||
- `readonly transport: PeerJSTransport`.
|
||||
- `constructor(config: PeerJSTransportConfig)`.
|
||||
- `install(node: NetworkNode): void`.
|
||||
- `start(): void` — no-op.
|
||||
- `stop(): Promise<void>` — idempotent close + unregister.
|
||||
- `dispose(): void` — drops the node reference; idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`install` caches the `NetworkNode` reference so `stop` can unregister the
|
||||
transport on tear-down even when the caller forgets to invoke `stop` via
|
||||
the node lifecycle. `dispose` clears that reference so the plugin can be
|
||||
garbage-collected after the node is released. The plugin deliberately does
|
||||
no eager broker connect — opening the signalling handshake on `start` would
|
||||
make `NetworkNode.start()` block on a remote service, which violates the
|
||||
upstream contract where `start` is synchronous-or-fast and connections are
|
||||
opened by the surrounding mesh / discovery plugins.
|
||||
@@ -0,0 +1,147 @@
|
||||
# transport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`PeerJSTransport` adapts the PeerJS WebRTC stack to the
|
||||
`@suaveplan/network` mesh `Transport` contract. It lazy-initializes a single
|
||||
local `PeerLike` against a configured signalling broker, then opens one
|
||||
`DataConnectionLike` per remote peer and routes `MessageEnvelope` traffic
|
||||
through a small wire frame (`{kind: "suaveplan.transport.v1", envelope}`) so
|
||||
the same `PeerLike` can host concurrent discovery traffic from the rest of
|
||||
this package without confusion.
|
||||
|
||||
The transport composes the existing primitives in this package
|
||||
(`initializePeer`, `connectToPeer`, `handleConnection`) rather than reaching
|
||||
into raw `peerjs` APIs, which keeps the migrated unit byte-identical to the
|
||||
upstream integrated discovery + transport bundle.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `Transport` implementation with `id: "peerjs"`.
|
||||
- Lazy broker connect — the first `connect()` opens the signalling
|
||||
handshake; subsequent connects reuse the same `PeerLike`.
|
||||
- Per-peer `DataConnectionLike` registry with `connect` / `disconnect` /
|
||||
`send` / `onMessage` / `onPeerEvent` / `getConnectedPeers` / `close`.
|
||||
- Pre-aborted `AbortSignal` rejected with `ABORTED`; mid-handshake aborts
|
||||
surface the same code.
|
||||
- Frame discriminator ensures discovery-only peers ignore transport bytes.
|
||||
- OTel: `peerjs_transport.connect` / `disconnect` / `send` spans,
|
||||
`peerjs_transport.messages_total{direction,outcome}` counter,
|
||||
`peerjs_transport.active_peers` gauge.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PeerJSTransport } from "@suaveplan/network-discovery-peerjs/transport";
|
||||
|
||||
const transport = new PeerJSTransport({
|
||||
host: "signalling.example.com",
|
||||
peerFactory: makePeerFactory(),
|
||||
});
|
||||
|
||||
await transport.connect("remote-peer-id", "ignored");
|
||||
transport.onMessage((peerId, envelope) => {
|
||||
console.log("from", peerId, envelope);
|
||||
});
|
||||
await transport.send("remote-peer-id", {
|
||||
id: "m1",
|
||||
type: "ping",
|
||||
senderId: "self",
|
||||
payload: { hello: "world" },
|
||||
timestamp: Date.now(),
|
||||
headers: {},
|
||||
ttl: 64,
|
||||
visitedNodes: [],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { PeerJSPlugin } from "@suaveplan/network-discovery-peerjs/plugin";
|
||||
|
||||
const node = new NetworkNode();
|
||||
const plugin = new PeerJSPlugin({
|
||||
host: "signalling.example.com",
|
||||
iceServers: [{ urls: "stun:stun.example.com:3478" }],
|
||||
peerFactory: makePeerFactory(),
|
||||
});
|
||||
|
||||
node.use(plugin);
|
||||
await node.start();
|
||||
|
||||
const transport = plugin.transport;
|
||||
transport.onPeerEvent((peerId, event) => {
|
||||
if (event === "connected") {
|
||||
console.log("peer joined", peerId);
|
||||
}
|
||||
});
|
||||
|
||||
// Open a connection with a cancellable signal — useful for UI flows that
|
||||
// abort the dial when the user navigates away.
|
||||
const ac = new AbortController();
|
||||
setTimeout(() => ac.abort(), 5_000);
|
||||
await transport.connect("alice", "ignored", ac.signal);
|
||||
|
||||
await node.stop();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PeerJSTransport implements Transport`
|
||||
- `readonly id = "peerjs"`.
|
||||
- `constructor(config: PeerJSTransportConfig)` — `host` is required; all
|
||||
other fields default (`port=443`, `path="/"`, `secure=true`, `debug=0`).
|
||||
- `connect(peerId: string, address: string, signal?: AbortSignal): Promise<void>` —
|
||||
lazy-opens the broker handshake on first call, then opens a `DataConnection`
|
||||
to `peerId`. `address` is accepted for `Transport` compatibility but
|
||||
unused (PeerJS centralises signalling at `host`). Throws
|
||||
`PEER_ALREADY_CONNECTED` / `TRANSPORT_CLOSED` / `ABORTED` /
|
||||
`PEER_CONNECT_FAILED`.
|
||||
- `disconnect(peerId: string): Promise<void>` — closes the per-peer
|
||||
connection. Unknown peer is a no-op.
|
||||
- `send(peerId: string, envelope: MessageEnvelope): Promise<void>` —
|
||||
wraps `envelope` in the transport frame and sends. Throws
|
||||
`PEER_UNKNOWN` / `TRANSPORT_CLOSED` / `SEND_FAILED`.
|
||||
- `onMessage(handler)` / `onPeerEvent(handler)` — return unsubscribe.
|
||||
- `getConnectedPeers(): readonly string[]`.
|
||||
- `close(): Promise<void>` — idempotent; closes every connection,
|
||||
destroys the `PeerLike`, drops handler sets.
|
||||
- `getMyPeerId(): string | null` — broker-assigned local id, or `null`
|
||||
before the first `connect()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The transport reuses `initializePeer` + `connectToPeer` + `handleConnection`
|
||||
from `core/peerjs-connection/` so the integrated discovery + transport unit
|
||||
upstream sees as a single PeerJS package is preserved without splitting it.
|
||||
The discovery layer's Zod `PeerJSMessageSchema` deliberately does not list
|
||||
the transport `kind` discriminator, so when a discovery-only peer receives
|
||||
a transport frame the schema rejects it and the handler silently drops it —
|
||||
mixing both protocols on the same `PeerLike` is safe.
|
||||
|
||||
Per-peer connection state is held in `#peers: Map<PeerId, PeerState>`. The
|
||||
fresh-`Map` invariant in `connect()` (each call hands `openConnection` a
|
||||
private map) guarantees the only path that returns `undefined` from
|
||||
`openConnection` is unreachable in production; an `/* istanbul ignore next */`
|
||||
guard documents the defensive branch.
|
||||
|
||||
`#waitForOpen` resolves on the first `open` event from the new
|
||||
`DataConnectionLike`; if the channel emits `error` before `open` it
|
||||
rejects with `PEER_CONNECT_FAILED`. Pre-aborted and mid-handshake
|
||||
abort signals both reject with `ABORTED`.
|
||||
|
||||
Frame validation uses a structural type-guard (`kind === "suaveplan.transport.v1"`
|
||||
|
||||
- object `envelope`), which means malformed or non-transport traffic is
|
||||
dropped without touching the registered handlers, preventing a hostile
|
||||
peer from injecting through `onMessage`.
|
||||
|
||||
The transport never emits `connected` for a peer twice — `#acceptIncoming`
|
||||
short-circuits when the registry already has the id, and `#handlePeerLost`
|
||||
gates the `disconnected` emission on registry presence, so consumers can
|
||||
treat the event stream as exactly-once per session.
|
||||
@@ -0,0 +1,87 @@
|
||||
# types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Type-only foundation for `@suaveplan/network-discovery-peerjs`. Declares the
|
||||
wire-format `PeerJSMessage` envelope, the structural `PeerLike` and
|
||||
`DataConnectionLike` surfaces used to inject real or stub PeerJS implementations,
|
||||
the strict `PeerJSDiscoveryConfig` (which deliberately makes `host` mandatory —
|
||||
there is no public-server fallback), the resolved-config shape produced by Zod
|
||||
defaulting, and the typed event map fanned out on the optional bus.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `PeerJSMessageType` literal-record covering every wire-format verb.
|
||||
- `PeerJSPeerMetadata` + `PeerJSPeerRecord` separation: wire payload vs.
|
||||
in-memory room state.
|
||||
- `DataConnectionLike` / `PeerLike` structural surfaces — production binds to
|
||||
real `peerjs` instances, tests bind to a deterministic stub with no mocks.
|
||||
- `PeerFactory` contract for injecting the constructor.
|
||||
- `PeerJSDiscoveryConfig` with required `host` and Zod-defaulted optional
|
||||
fields; `ResolvedPeerJSDiscoveryConfig` for the post-parse internal shape.
|
||||
- `PeerJSEventMap` typed bus emissions: `peerjs:started`,
|
||||
`peerjs:peer-discovered`, `peerjs:peer-lost`, `peerjs:signaling-failed`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
PeerJSDiscoveryConfig,
|
||||
PeerJSEventMap,
|
||||
} from "@suaveplan/network-discovery-peerjs/types";
|
||||
|
||||
const config: PeerJSDiscoveryConfig = {
|
||||
host: "signaling.example.com",
|
||||
room: "demo",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
PeerJSDiscoveryConfig,
|
||||
PeerFactory,
|
||||
PeerLike,
|
||||
} from "@suaveplan/network-discovery-peerjs/types";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const bus = new EventBus<import("@suaveplan/network-discovery-peerjs/types").PeerJSEventMap>();
|
||||
const factory: PeerFactory = (id, opts): PeerLike => fakePeer(id, opts);
|
||||
|
||||
const config: PeerJSDiscoveryConfig = {
|
||||
host: "signaling.example.com",
|
||||
port: 8443,
|
||||
secure: true,
|
||||
room: "alpha",
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
bus,
|
||||
peerFactory: factory,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PeerJSMessageType` — frozen `{ANNOUNCE,DISCOVER,PEER_LIST,LEAVE,HEARTBEAT}`.
|
||||
- `PeerJSPeerMetadata` — announce payload shape.
|
||||
- `PeerJSMessage` — full envelope `{type, from, payload, timestamp}`.
|
||||
- `PeerJSPeerRecord` — internal room-manager record (mutable `lastSeen`).
|
||||
- `DataConnectionLike` — `{peer, open, send, on(event,...), close}`.
|
||||
- `PeerLike` — `{connect, on(event,...), destroy}`.
|
||||
- `PeerFactory` — `(peerId, opts) => PeerLike`.
|
||||
- `PeerJSPeerOptions` — host/port/path/secure/key/debug/config bundle.
|
||||
- `PeerJSDiscoveryConfig` — caller-facing options, `host`+`room` required.
|
||||
- `ResolvedPeerJSDiscoveryConfig` — internal post-default shape.
|
||||
- `PeerJSEventMap` — typed bus event keys + payloads.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`host` has no default. Constructing without one throws `INVALID_OPTIONS` at
|
||||
the Zod boundary inside `PeerJSDiscovery` — this is the explicit decision the
|
||||
parity checklist records as an enterprise uplift over the upstream package,
|
||||
which silently fell back to the public `peerjs.com` cloud. Wire-format types
|
||||
keep `readonly` modifiers so downstream code cannot mutate buffers shared
|
||||
across the message-handler dispatch table.
|
||||
Reference in New Issue
Block a user