first commit
This commit is contained in:
114
docs/networking/network/network-discovery-webtorrent/README.md
Normal file
114
docs/networking/network/network-discovery-webtorrent/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# @suaveplan/network-discovery-webtorrent - Browser-tier WebTorrent DiscoveryStrategy: WebTorrent-tracker-driven WebRTC peer discovery via p2pt. Tracker list REQUIRED — no public-tracker fallback. AbortSignal + EventBusLike + ServiceRegistry + OTel + Zod boundaries.
|
||||
|
||||
## Summary
|
||||
|
||||
Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` chains.
|
||||
Wraps a [`p2pt`](https://github.com/subins2000/p2pt) instance —
|
||||
WebTorrent-tracker-driven WebRTC peer discovery — and exposes a deduped,
|
||||
capped peer set to the bootstrap pipeline.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [webtorrent-discovery](./src/core/webtorrent-discovery/webtorrent-discovery.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [webtorrent-discovery](./src/core/webtorrent-discovery/webtorrent-discovery.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [webtorrent-discovery](./src/core/webtorrent-discovery/webtorrent-discovery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [webtorrent-discovery](./src/core/webtorrent-discovery/webtorrent-discovery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [webtorrent-discovery](./src/core/webtorrent-discovery/webtorrent-discovery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [webtorrent-discovery](./src/core/webtorrent-discovery/webtorrent-discovery.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_ - [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Why this package
|
||||
|
||||
- Plug-and-play `DiscoveryStrategy` slot for network bootstrap
|
||||
chains. Works alongside `@suaveplan/network-discovery-peerjs`,
|
||||
`@suaveplan/network-discovery-trystero`, and
|
||||
`@suaveplan/network-discovery-crdt`.
|
||||
- **Tracker list REQUIRED.** Upstream's hard-coded public-tracker fallback
|
||||
was removed. Callers must supply their own tracker infrastructure.
|
||||
- Injectable `p2ptFactory` — production wires the real `p2pt`; tests bind
|
||||
a deterministic `P2PTLike` fake (no `vi.mock` ever used).
|
||||
- Typed `EventBusLike<WebTorrentEventMap>` integration.
|
||||
- Optional `@suaveplan/service-discovery` `ServiceRegistry` mirroring.
|
||||
- OTel surface: `disc_webtorrent.start` span;
|
||||
`peers_discovered_total{outcome}`, `tracker_errors_total{tracker}`,
|
||||
`tracker_announce_duration_ms` instruments.
|
||||
- AbortSignal honoured on every async public method.
|
||||
- 100% test + branch + function + line coverage.
|
||||
|
||||
## Basic example
|
||||
|
||||
```ts
|
||||
import { WebTorrentDiscovery } from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
const discovery = new WebTorrentDiscovery({
|
||||
trackers: ["wss://my-tracker.example.com"],
|
||||
p2ptFactory: async (trackers, announceId) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(trackers as string[], announceId);
|
||||
},
|
||||
});
|
||||
|
||||
await discovery.start();
|
||||
const peers = await discovery.discover(5_000);
|
||||
console.log(`found ${peers.length} peers`);
|
||||
await discovery.stop();
|
||||
```
|
||||
|
||||
## Advanced example
|
||||
|
||||
```ts
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { ServiceRegistry } from "@suaveplan/service-discovery";
|
||||
import { DiscoveryChain } from "@suaveplan/network-bootstrap";
|
||||
import {
|
||||
WebTorrentDiscovery,
|
||||
type WebTorrentEventMap,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
const bus = new EventBus<WebTorrentEventMap>();
|
||||
const registry = new ServiceRegistry();
|
||||
|
||||
bus.on("webtorrent:peer-discovered", ({ peer }) => {
|
||||
console.log("peer:", peer.id, peer.addresses);
|
||||
});
|
||||
bus.on("webtorrent:tracker-error", ({ reason }) => {
|
||||
console.warn("tracker:", reason);
|
||||
});
|
||||
|
||||
const discovery = new WebTorrentDiscovery({
|
||||
trackers: [
|
||||
"wss://my-tracker-a.example.com",
|
||||
"wss://my-tracker-b.example.com",
|
||||
],
|
||||
announceId: "my-app/v1",
|
||||
maxPeers: 256,
|
||||
p2ptFactory: async (trackers, id) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(trackers as string[], id);
|
||||
},
|
||||
bus,
|
||||
registry,
|
||||
});
|
||||
|
||||
const chain = new DiscoveryChain([discovery]);
|
||||
const peers = await chain.discover(5_000);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
See sibling co-located markdown:
|
||||
|
||||
- [`src/types/types.md`](./src/types/types.md)
|
||||
- [`src/errors/errors.md`](./src/errors/errors.md)
|
||||
- [`src/otel/otel.md`](./src/otel/otel.md)
|
||||
- [`src/core/webtorrent-discovery/webtorrent-discovery.md`](./src/core/webtorrent-discovery/webtorrent-discovery.md)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,170 @@
|
||||
# webtorrent-discovery
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`WebTorrentDiscovery` is the browser-tier `DiscoveryStrategy` for
|
||||
`@suaveplan/network-bootstrap` chains. It wraps a single
|
||||
[`p2pt`](https://github.com/subins2000/p2pt) instance — WebTorrent-tracker-
|
||||
driven WebRTC peer discovery — and exposes a deduped, capped peer set to
|
||||
the bootstrap pipeline. The strategy is purely an observer: peers join, the
|
||||
strategy emits typed events and registers them with the optional
|
||||
`ServiceRegistry`; peers drop, the strategy removes them.
|
||||
|
||||
The `trackers` list is REQUIRED and non-empty. Upstream's hard-coded
|
||||
public-tracker fallback (`tracker.openwebtorrent.com`, `tracker.btorrent.xyz`,
|
||||
`tracker.files.fm:7073`, `spacetradersapi-chatbox.herokuapp.com:443`) was
|
||||
deliberately removed — callers must supply their own tracker infrastructure
|
||||
or supply an explicit list. Missing or empty `trackers` raises
|
||||
`NET_DISC_WEBTORRENT.INVALID_OPTIONS` at construction time, not at
|
||||
`start()` time.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the `DiscoveryStrategy` contract (`name`, `discover`,
|
||||
`isAvailable`) so it slots directly into `@suaveplan/network-bootstrap`
|
||||
`DiscoveryChain` instances.
|
||||
- Lifecycle: idempotent `start()` / `stop()` / `dispose()`.
|
||||
- Injectable `p2ptFactory` — production wires the real `p2pt`; tests pass a
|
||||
deterministic in-memory `P2PTLike` fake (no `vi.mock` ever used).
|
||||
- Zod-validated configuration. Tracker list is non-optional and
|
||||
`.min(1)`-enforced. The factory is enforced at runtime in addition to
|
||||
schema-level acceptance.
|
||||
- Typed `EventBusLike<WebTorrentEventMap>` integration: `started`,
|
||||
`peer-discovered`, `peer-lost`, `tracker-error`, `stopped`.
|
||||
- Optional `@suaveplan/service-discovery` `ServiceRegistry` integration —
|
||||
peers auto-register on join, auto-deregister on drop and on
|
||||
`stop()`/`dispose()`.
|
||||
- OTel instrumentation: `disc_webtorrent.start` span;
|
||||
`peers_discovered_total{outcome}`, `tracker_errors_total{tracker}`,
|
||||
`tracker_announce_duration_ms` instruments.
|
||||
- `AbortSignal` honoured on every async public method.
|
||||
- Peer cap (`maxPeers`, default `100`) enforced at admission.
|
||||
- Local-peer admission counters: `found`, `duplicate`, `rejected`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WebTorrentDiscovery } from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
const discovery = new WebTorrentDiscovery({
|
||||
trackers: ["wss://my-tracker.example.com"],
|
||||
p2ptFactory: async (trackers, announceId) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(trackers as string[], announceId);
|
||||
},
|
||||
});
|
||||
|
||||
await discovery.start();
|
||||
const peers = await discovery.discover(5_000);
|
||||
console.log(`found ${peers.length} peers`);
|
||||
await discovery.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createEventBus } from "@suaveplan/events";
|
||||
import { ServiceRegistry } from "@suaveplan/service-discovery";
|
||||
import { DiscoveryChain } from "@suaveplan/network-bootstrap";
|
||||
import {
|
||||
WebTorrentDiscovery,
|
||||
type WebTorrentEventMap,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
const bus = createEventBus<WebTorrentEventMap>();
|
||||
const registry = new ServiceRegistry();
|
||||
|
||||
bus.on("webtorrent:peer-discovered", ({ peer }) => {
|
||||
console.log("peer joined:", peer.id, peer.addresses);
|
||||
});
|
||||
bus.on("webtorrent:tracker-error", ({ reason }) => {
|
||||
console.warn("tracker error:", reason);
|
||||
});
|
||||
|
||||
const discovery = new WebTorrentDiscovery({
|
||||
trackers: [
|
||||
"wss://my-tracker-a.example.com",
|
||||
"wss://my-tracker-b.example.com",
|
||||
],
|
||||
announceId: "my-app/v1",
|
||||
maxPeers: 256,
|
||||
timeout: 30_000,
|
||||
p2ptFactory: async (trackers, id) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(trackers as string[], id);
|
||||
},
|
||||
bus,
|
||||
registry,
|
||||
});
|
||||
|
||||
const ac = new AbortController();
|
||||
const chain = new DiscoveryChain([discovery]);
|
||||
const peers = await chain.discover(5_000, ac.signal);
|
||||
|
||||
for (const peer of peers) {
|
||||
// service-discovery has already mirrored these registrations.
|
||||
console.log(peer.peerId, peer.addresses);
|
||||
}
|
||||
|
||||
await discovery.stop();
|
||||
discovery.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new WebTorrentDiscovery(config, clock?)` — Zod-validates the config,
|
||||
enforces a callable `p2ptFactory`, stores the resolved configuration.
|
||||
Throws `NET_DISC_WEBTORRENT.INVALID_OPTIONS` on validation / missing
|
||||
factory.
|
||||
- `start(signal?)` — instantiates the underlying p2pt via the factory,
|
||||
wires every event handler, calls `p2pt.start()`, emits `started`. Throws
|
||||
`TRACKER_FAILED` (with the underlying error as `cause`) on factory or
|
||||
start failure; throws `DISPOSED` if disposed; rethrows abort-error if
|
||||
signalled.
|
||||
- `stop(signal?)` — idempotent. Tears the p2pt instance down, deregisters
|
||||
every peer from the registry, clears local state. Emits `stopped` when
|
||||
the call wasn't aborted.
|
||||
- `discover(timeoutMs, signal?)` — returns the current dedup'd peer
|
||||
snapshot mapped into `network-bootstrap` `PeerInfo[]`. Honours signal
|
||||
up-front.
|
||||
- `isAvailable(signal?)` — `true` only while running, not disposed, and
|
||||
the underlying transport has reported `trackerconnect`.
|
||||
- `announce(peerInfo, signal?)` — registers the local descriptor with the
|
||||
optional service registry. Throws `DISPOSED` / `NOT_RUNNING` when
|
||||
appropriate.
|
||||
- `getPeers()` / `getPeerCount()` / `isRunning()` /
|
||||
`isTrackerConnected()` — synchronous introspection.
|
||||
- `dispose()` — permanent teardown; subsequent lifecycle calls throw
|
||||
`DISPOSED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `peers` map keys on the p2pt-assigned id (preferring `peer.id`, then
|
||||
`peer._id`). Anything else surfaces as `recordPeersDiscovered("rejected")`
|
||||
but never bubbles an exception — the underlying transport runs its own
|
||||
retries.
|
||||
- The `maxPeers` cap is enforced at admission: once the map is full,
|
||||
further `peerconnect` events emit `recordPeersDiscovered("rejected")`
|
||||
and are dropped. The cap is intentionally simple — no LRU — because
|
||||
WebTorrent rooms tend to grow monotonically and operators are expected
|
||||
to set the cap to a sane upper bound for their tracker.
|
||||
- `trackerconnect` flips `trackerConnected` to `true` exactly once, even
|
||||
if the underlying p2pt instance re-fires the event during reconnects.
|
||||
The first transition records the announce-duration histogram.
|
||||
- The factory is `async`-tolerant so production code can dynamically
|
||||
`import("p2pt")` (a Node-flavoured package) without forcing this
|
||||
package itself to bundle it. Tests pass a synchronous in-memory fake.
|
||||
- `AbortSignal` is honoured up-front on every public method; in
|
||||
`start()`, after the factory resolves we re-check the signal so a
|
||||
cancellation during the dynamic import doesn't leak the p2pt instance.
|
||||
- The `ServiceRegistry` integration uses the canonical id form
|
||||
`webtorrent:<announceId>:<peerId>` mirroring the `trystero` and `peerjs`
|
||||
packages, so a single `ServiceRegistry` watcher can see peers from
|
||||
every discovery family without colliding.
|
||||
- Every catch around `instance.destroy()` swallows because real p2pt
|
||||
instances can throw during teardown if a tracker is mid-flight — the
|
||||
swallowed error is already covered by the structured `TRACKER_FAILED`
|
||||
emission, so re-throwing would double-report.
|
||||
@@ -0,0 +1,107 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the structured-error registry for
|
||||
`@suaveplan/network-discovery-webtorrent`. Every public boundary in the
|
||||
package surfaces failures as a `NetDiscWebTorrentError` whose `code` field
|
||||
is drawn from the frozen `NET_DISC_WEBTORRENT_CODES` registry built via
|
||||
`@suaveplan/error`'s `defineCodes` helper. Consumers branch on `error.code`
|
||||
without parsing message strings.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `NET_DISC_WEBTORRENT_CODES` registry — five stable codes covering
|
||||
every failure mode the strategy can hit.
|
||||
- `NetDiscWebTorrentError` class extending `SuaveplanError`, carrying a
|
||||
typed `code`, a stable message-key, and an optional `cause` chain for
|
||||
upstream provenance.
|
||||
- Round-trippable with the rest of the error ecosystem — wire
|
||||
serialization, redaction, and the OTel span-event adapter all work
|
||||
out-of-the-box.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_DISC_WEBTORRENT_CODES,
|
||||
NetDiscWebTorrentError,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
throw new NetDiscWebTorrentError(
|
||||
NET_DISC_WEBTORRENT_CODES.INVALID_OPTIONS,
|
||||
"errors.webtorrent.invalid_options"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_DISC_WEBTORRENT_CODES,
|
||||
NetDiscWebTorrentError,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
try {
|
||||
await discovery.start();
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NetDiscWebTorrentError &&
|
||||
err.code === NET_DISC_WEBTORRENT_CODES.TRACKER_FAILED
|
||||
) {
|
||||
// Fall back to another DiscoveryStrategy in the chain.
|
||||
console.warn("webtorrent trackers unreachable:", err.cause);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NET_DISC_WEBTORRENT_CODES` — frozen object whose keys are:
|
||||
- `INVALID_OPTIONS` — caller-supplied config failed Zod validation, the
|
||||
`trackers` list was empty, or the required `p2ptFactory` was missing.
|
||||
- `DISPOSED` — operation attempted after `dispose()` cleared state.
|
||||
- `NOT_RUNNING` — operation attempted before `start()` completed or after
|
||||
`stop()` cleared state.
|
||||
- `TRACKER_FAILED` — the underlying transport's tracker stack failed and
|
||||
no peers can be discovered.
|
||||
- `PEER_CONNECT_FAILED` — a peer-connect event was received but the
|
||||
record could not be admitted.
|
||||
- `TRANSPORT_CLOSED` — transport-level: a `send`/`connect` was attempted
|
||||
after `close()`.
|
||||
- `PEER_ALREADY_CONNECTED` — transport-level: `connect(peerId)` invoked
|
||||
twice for the same id.
|
||||
- `PEER_UNKNOWN` — transport-level: `send` invoked for an unknown peer.
|
||||
- `MAX_PEERS_EXCEEDED` — transport-level: per-peer cap reached on
|
||||
`connect`.
|
||||
- `SEND_FAILED` — transport-level: the underlying `p2pt.send` rejected.
|
||||
- `ABORTED` — transport-level: pre-aborted `AbortSignal` observed on
|
||||
`connect`.
|
||||
- `CONNECT_TIMEOUT` — transport-level: peer never showed up before the
|
||||
configured connect timeout.
|
||||
- `SEND_UNSUPPORTED` — transport-level: the supplied `p2pt` instance
|
||||
omits the `send` capability.
|
||||
- `NetDiscWebTorrentError extends SuaveplanError<NetDiscWebTorrentErrorCode>`
|
||||
— constructor `(code, messageKey, options?)` accepting an optional
|
||||
`{ cause }` for chained errors.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Codes are registered with `@suaveplan/error`'s `defineCodes` so they
|
||||
participate in the global stable-code namespace; duplicate registrations
|
||||
fail at import time rather than silently overlapping.
|
||||
- `messageKey` is a translation key, not human-facing copy — i18n bundles
|
||||
resolve these via `@suaveplan/i18n` when running in apps that wire the
|
||||
resolver.
|
||||
- The `cause` chain is preserved across `JSON.stringify` via
|
||||
`SuaveplanError.toJSON`; consumers can serialise discovery failures to
|
||||
structured logs without losing the original exception.
|
||||
- The registry is intentionally small. `TRACKER_FAILED` covers every
|
||||
transport-level failure (we have only one transport here); `INVALID_OPTIONS`
|
||||
is the umbrella for every static configuration error including the
|
||||
missing-tracker-list case.
|
||||
@@ -0,0 +1,105 @@
|
||||
# otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry instrumentation surface for the WebTorrent discovery
|
||||
strategy. Provides one span helper for the `start()` lifecycle, one counter
|
||||
for discovered peers (labelled by outcome), one counter for tracker errors
|
||||
(labelled by tracker URL), and one histogram measuring tracker-announce
|
||||
wall-clock latency.
|
||||
|
||||
All instruments resolve against `@opentelemetry/api` only — no SDK is
|
||||
required for the package to load or run. When the host application wires an
|
||||
SDK, every instrument records into the same pipelines as the rest of the
|
||||
suaveplan observability stack.
|
||||
|
||||
## Features
|
||||
|
||||
- `withStartSpan(fn)` — wraps the `start()` body in a
|
||||
`disc_webtorrent.start` span; surfaces errors through the span's standard
|
||||
`recordException` path.
|
||||
- `recordPeersDiscovered(outcome, count?)` — increments
|
||||
`disc_webtorrent.peers_discovered_total{outcome}`. Outcomes are
|
||||
`"found" | "duplicate" | "rejected"`. Negative or zero counts are
|
||||
silently ignored.
|
||||
- `recordTrackerError(tracker)` — increments
|
||||
`disc_webtorrent.tracker_errors_total{tracker}`. Missing tracker labels
|
||||
fall back to `"unknown"` to keep cardinality bounded.
|
||||
- `recordTrackerAnnounceDuration(ms)` — records into
|
||||
`disc_webtorrent.tracker_announce_duration_ms`. Negative values are
|
||||
clamped to zero.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordPeersDiscovered,
|
||||
withStartSpan,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
await withStartSpan(async (span) => {
|
||||
span.setAttribute("app_id", "my-app/v1");
|
||||
recordPeersDiscovered("found", 1);
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordPeersDiscovered,
|
||||
recordTrackerAnnounceDuration,
|
||||
recordTrackerError,
|
||||
withStartSpan,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
await withStartSpan(async (span) => {
|
||||
span.setAttribute("tracker_count", 3);
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
await connectToTrackers();
|
||||
recordTrackerAnnounceDuration(Date.now() - t0);
|
||||
} catch (err) {
|
||||
recordTrackerError("wss://tracker.example.com");
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Inside event loop:
|
||||
recordPeersDiscovered("found", 1);
|
||||
recordPeersDiscovered("duplicate", 1);
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `withStartSpan<T>(fn: (span: Span) => Promise<T>): Promise<T>` — runs
|
||||
`fn` inside an active `disc_webtorrent.start` span, ending the span on
|
||||
resolution and on failure.
|
||||
- `PeerDiscoveryOutcome = "found" | "duplicate" | "rejected"` — outcome
|
||||
label union for the peers-discovered counter.
|
||||
- `recordPeersDiscovered(outcome: PeerDiscoveryOutcome, count?: number)` —
|
||||
increments the discovered-peers counter (default count `1`; non-positive
|
||||
counts are no-ops).
|
||||
- `recordTrackerError(tracker: string | undefined)` — increments the
|
||||
tracker-error counter; an `undefined` label is normalised to `"unknown"`.
|
||||
- `recordTrackerAnnounceDuration(ms: number)` — records milliseconds into
|
||||
the announce-duration histogram, clamping negative values to zero.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Instrument names are namespaced under `disc_webtorrent.*` so dashboards
|
||||
can filter the entire WebTorrent discovery family without parsing
|
||||
attribute values.
|
||||
- Label cardinality is intentionally bounded: outcomes are a closed
|
||||
three-way union, and unknown tracker labels collapse to `"unknown"` so
|
||||
the counter cannot explode under adversarial input.
|
||||
- The tracker-announce histogram measures from `start()` entry to the
|
||||
underlying transport's first `trackerconnect` event, not to the first
|
||||
peer. The two latencies are deliberately separated so dashboards can
|
||||
distinguish slow trackers from slow peers.
|
||||
- The span helper is a thin pass-through over `tracer.startActiveSpan`
|
||||
with mandatory `span.end()` in a `finally` block. Errors propagate
|
||||
unchanged so the caller's catch-block sees the original exception.
|
||||
@@ -0,0 +1,105 @@
|
||||
# plugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`WebTorrentPlugin` is the `NetworkPlugin` adapter that exposes a
|
||||
`WebTorrentTransport` to a host `NetworkNode` from `@suaveplan/network`.
|
||||
It is a thin lifecycle wrapper — the heavy lifting (tracker
|
||||
rendezvous, peer registry, message framing, error mapping, OTel) lives
|
||||
inside the underlying `WebTorrentTransport` — so the plugin keeps the
|
||||
shape callers already learned from the `WebSocketPlugin` and
|
||||
`WebRTCPlugin` siblings.
|
||||
|
||||
This package is the "fold" Wave-3.4 addition to the existing
|
||||
`@suaveplan/network-discovery-webtorrent` package: both the
|
||||
`DiscoveryStrategy` and the `Transport` family of plugins live under the
|
||||
same package umbrella, mirroring the upstream layout while preserving a
|
||||
clean subpath surface (`./transport`, `./plugin`).
|
||||
|
||||
## Features
|
||||
|
||||
- Stable `name: "webtorrent"` plugin identifier — discovery and routing
|
||||
plumbing key off this value.
|
||||
- `install(node)` registers the underlying transport with the node's
|
||||
`MessageRouter` so routed envelopes can find it.
|
||||
- `start()` is intentionally a no-op — the p2pt tracker socket is opened
|
||||
lazily inside the transport on the first `connect()`.
|
||||
- `stop()` closes the transport (rejecting pending connects, draining
|
||||
peer state, destroying the p2pt instance) and unregisters it from the
|
||||
node.
|
||||
- `dispose()` is idempotent.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { WebTorrentPlugin } from "@suaveplan/network-discovery-webtorrent/plugin";
|
||||
import P2PT from "p2pt";
|
||||
|
||||
const node = new NetworkNode();
|
||||
const plugin = new WebTorrentPlugin({
|
||||
trackers: ["wss://tracker.openwebtorrent.com"],
|
||||
p2ptFactory: (trackers, id) => new P2PT(trackers as string[], id),
|
||||
});
|
||||
node.use(plugin);
|
||||
await node.start();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { WebTorrentPlugin } from "@suaveplan/network-discovery-webtorrent/plugin";
|
||||
|
||||
async function makeNode(trackers: string[]) {
|
||||
const node = new NetworkNode();
|
||||
const plugin = new WebTorrentPlugin({
|
||||
trackers,
|
||||
announceId: "demo-room",
|
||||
maxPeers: 16,
|
||||
connectTimeoutMs: 8_000,
|
||||
p2ptFactory: async (t, id) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(t as string[], id);
|
||||
},
|
||||
});
|
||||
node.use(plugin);
|
||||
await node.start();
|
||||
// Routing through the node now selects the webtorrent transport
|
||||
// whenever an envelope's target peer is reachable through it.
|
||||
return { node, plugin };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class WebTorrentPlugin implements NetworkPlugin`
|
||||
- `readonly name: "webtorrent"`
|
||||
- `readonly transport: WebTorrentTransport` — exposed for tests and
|
||||
advanced wiring; consumers should drive sends through the host
|
||||
`NetworkNode` rather than this field.
|
||||
- `constructor(config: WebTorrentTransportConfig)` — throws
|
||||
`INVALID_OPTIONS` on bad config.
|
||||
- `install(node: NetworkNode): void` — registers the transport.
|
||||
- `start(): void` — no-op.
|
||||
- `stop(): Promise<void>` — closes the transport and unregisters it
|
||||
from the node. Safe to call when no node was installed.
|
||||
- `dispose(): void` — idempotent; releases the node reference.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `start()` is intentionally non-async — there is no remote rendezvous
|
||||
to perform up-front. p2pt is created on the first `connect()` so a
|
||||
headless node never opens a tracker socket.
|
||||
- `stop()` always closes the transport first, then unregisters; this
|
||||
ordering guarantees that any in-flight `send` rejects with
|
||||
`TRANSPORT_CLOSED` rather than `ROUTER_NO_TRANSPORT`.
|
||||
- Sequential `stop()`s are tolerated through the underlying transport's
|
||||
idempotent `close()` and the node's idempotent
|
||||
`unregisterTransport` (the second call is a no-op when the transport
|
||||
is already gone).
|
||||
- `dispose()` is separate from `stop()` so consumers can release the
|
||||
node reference without re-closing the transport.
|
||||
@@ -0,0 +1,147 @@
|
||||
# transport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`WebTorrentTransport` implements the `@suaveplan/network` `Transport`
|
||||
contract over a WebTorrent-tracker-signalled WebRTC mesh via `p2pt`. It
|
||||
composes the same `P2PTLike` surface that `WebTorrentDiscovery` consumes,
|
||||
so production code wires the real `(trackers, id) => new P2PT(trackers,
|
||||
id)` and tests wire `FakeP2PT` — no `vi.mock` required. The adapter owns
|
||||
its own p2pt instance (lazily constructed on the first `connect` call)
|
||||
and surfaces typed peer events, JSON-framed envelope delivery, AbortSignal
|
||||
cancellation, structured errors, and OTel instrumentation.
|
||||
|
||||
This package is "Option B (fold)" from the `network-transport-webtorrent`
|
||||
proposal — the Wave 2 discovery package gains a sibling `Transport`
|
||||
implementation rather than spawning a separate package, matching upstream
|
||||
layout while keeping a clean public surface via the `./transport`
|
||||
subpath export.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements every member of the `Transport` contract (`id`, `connect`,
|
||||
`disconnect`, `send`, `onMessage`, `onPeerEvent`, `getConnectedPeers`,
|
||||
`close`).
|
||||
- Lazy p2pt instantiation — headless nodes that never `connect()` never
|
||||
open a tracker socket.
|
||||
- AbortSignal threaded through `connect` (both pre-aborted and mid-flight).
|
||||
- Per-`connect` timeout (`connectTimeoutMs`, default 30s) surfacing as
|
||||
`CONNECT_TIMEOUT`.
|
||||
- Strict per-peer registry — `connect` rejects duplicates with
|
||||
`PEER_ALREADY_CONNECTED` and respects an optional `maxPeers` cap.
|
||||
- Outbound `send` JSON-stringifies envelopes and delegates to
|
||||
`p2pt.send(peer, msg)`; inbound `msg` events JSON-parse and fan out to
|
||||
every registered handler.
|
||||
- 9 transport-level error codes share the
|
||||
`NET_DISC_WEBTORRENT_CODES` registry alongside the discovery codes.
|
||||
- OTel: `webtorrent_transport.{connect,disconnect,send}` spans, a
|
||||
`webtorrent_transport.messages_total{direction,outcome}` counter, and
|
||||
a `webtorrent_transport.active_peers` up-down gauge.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WebTorrentTransport } from "@suaveplan/network-discovery-webtorrent/transport";
|
||||
import P2PT from "p2pt";
|
||||
|
||||
const transport = new WebTorrentTransport({
|
||||
trackers: ["wss://tracker.openwebtorrent.com"],
|
||||
p2ptFactory: (trackers, id) => new P2PT(trackers as string[], id),
|
||||
});
|
||||
|
||||
transport.onMessage((peerId, envelope) => {
|
||||
console.log("from", peerId, envelope);
|
||||
});
|
||||
|
||||
await transport.connect("peer-abc", "/webrtc/p2p/peer-abc");
|
||||
await transport.send("peer-abc", {
|
||||
id: "m1",
|
||||
type: "ping",
|
||||
senderId: "self",
|
||||
payload: {},
|
||||
timestamp: Date.now(),
|
||||
headers: {},
|
||||
ttl: 64,
|
||||
visitedNodes: [],
|
||||
});
|
||||
await transport.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { WebTorrentTransport } from "@suaveplan/network-discovery-webtorrent/transport";
|
||||
import {
|
||||
NET_DISC_WEBTORRENT_CODES,
|
||||
NetDiscWebTorrentError,
|
||||
} from "@suaveplan/network-discovery-webtorrent";
|
||||
|
||||
const ac = new AbortController();
|
||||
const transport = new WebTorrentTransport({
|
||||
trackers: ["wss://tracker.openwebtorrent.com"],
|
||||
announceId: "my-app-room",
|
||||
maxPeers: 32,
|
||||
connectTimeoutMs: 10_000,
|
||||
p2ptFactory: async (trackers, id) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(trackers as string[], id);
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => ac.abort(), 5_000);
|
||||
|
||||
try {
|
||||
await transport.connect("peer-xyz", "/p2p/peer-xyz", ac.signal);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NetDiscWebTorrentError &&
|
||||
err.code === NET_DISC_WEBTORRENT_CODES.CONNECT_TIMEOUT
|
||||
) {
|
||||
// fall back to a different transport
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class WebTorrentTransport implements Transport`
|
||||
- `readonly id: "webtorrent"`
|
||||
- `constructor(config: WebTorrentTransportConfig)` — throws
|
||||
`INVALID_OPTIONS` on bad config.
|
||||
- `connect(peerId, address, signal?): Promise<void>` — resolves once
|
||||
p2pt reports `peerconnect` for the id. Rejects with
|
||||
`TRANSPORT_CLOSED`, `ABORTED`, `PEER_ALREADY_CONNECTED`,
|
||||
`MAX_PEERS_EXCEEDED`, `CONNECT_TIMEOUT`, or `TRACKER_FAILED`.
|
||||
- `disconnect(peerId): Promise<void>` — no-op for unknown peers.
|
||||
- `send(peerId, envelope): Promise<void>` — rejects with
|
||||
`TRANSPORT_CLOSED`, `PEER_UNKNOWN`, `SEND_UNSUPPORTED`, or
|
||||
`SEND_FAILED`.
|
||||
- `onMessage(handler): () => void` — returns unsubscribe handle.
|
||||
- `onPeerEvent(handler): () => void` — returns unsubscribe handle.
|
||||
- `getConnectedPeers(): readonly PeerId[]`
|
||||
- `close(): Promise<void>` — idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The Transport contract's `connect(peerId, address)` is a poor fit for
|
||||
p2pt's room-based discovery model. The adapter therefore treats
|
||||
`address` as informational and resolves the promise once p2pt emits
|
||||
`peerconnect` for the matching id (within `connectTimeoutMs`).
|
||||
- p2pt is created lazily and exactly once per transport instance. A
|
||||
factory rejection bubbles up wrapped in `TRACKER_FAILED` and clears the
|
||||
init promise so the next `connect()` can retry.
|
||||
- The active-peers gauge is `UpDownCounter`-backed and only adjusted on
|
||||
observed `peerconnect`/`peerclose` (or `disconnect`/`close`); `connect`
|
||||
rollback paths never adjust the gauge so the metric stays consistent
|
||||
with `getConnectedPeers()`.
|
||||
- All AbortSignal listeners are registered with `{ once: true }` and
|
||||
proactively removed on resolve/reject/timeout to avoid leaks.
|
||||
- Inbound messages are tolerated as either pre-parsed objects or JSON
|
||||
strings — real p2pt delivers strings; in-memory fakes can deliver
|
||||
objects directly. Parse failures count as `inbound,error` on the
|
||||
messages counter.
|
||||
@@ -0,0 +1,122 @@
|
||||
# types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Public type contracts for `@suaveplan/network-discovery-webtorrent`. Defines
|
||||
the wire-format-neutral peer-metadata record, the structural `P2PTLike`
|
||||
surface that production code and tests both bind to, the discovery-strategy
|
||||
configuration, the fully-resolved configuration shape after defaults are
|
||||
applied, and the typed event-map for the optional `EventBusLike` integration.
|
||||
|
||||
These types are zero-runtime; every public boundary is also validated by a
|
||||
Zod schema in `types.schemas.ts`.
|
||||
|
||||
## Features
|
||||
|
||||
- `WebTorrentPeerMetadata` — observed-peer record with `addresses`,
|
||||
`connectedAt`, mutable `lastSeen`, and free-form metadata.
|
||||
- `P2PTLike` — structural surface of a `p2pt` instance (`peerconnect`,
|
||||
`peerclose`, `msg`, `trackerconnect`, `trackerwarning` events, `start` /
|
||||
`destroy` lifecycle). Tests bind a deterministic in-memory implementation
|
||||
to this interface — no `vi.mock` ever needed.
|
||||
- `P2PTFactory` — `(trackers, announceId) => P2PTLike | Promise<P2PTLike>`
|
||||
signature injected via constructor options.
|
||||
- `WebTorrentDiscoveryConfig` — strict caller-supplied configuration. The
|
||||
`trackers` array is REQUIRED and may not be empty; there is no implicit
|
||||
public-tracker fallback in this package.
|
||||
- `ResolvedWebTorrentDiscoveryConfig` — fully-defaulted configuration the
|
||||
orchestrator stores after Zod parsing.
|
||||
- `WebTorrentEventMap` — typed payload contract for every event emitted on
|
||||
the optional `EventBusLike`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
WebTorrentDiscoveryConfig,
|
||||
P2PTFactory,
|
||||
} from "@suaveplan/network-discovery-webtorrent/types";
|
||||
|
||||
const factory: P2PTFactory = (trackers, id) => {
|
||||
// Tests pass a deterministic stub here.
|
||||
throw new Error("supply a real factory in production");
|
||||
};
|
||||
|
||||
const config: WebTorrentDiscoveryConfig = {
|
||||
trackers: ["wss://my-tracker.example.com"],
|
||||
p2ptFactory: factory,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ServiceRegistry } from "@suaveplan/service-discovery";
|
||||
import { createEventBus } from "@suaveplan/events";
|
||||
import type {
|
||||
WebTorrentDiscoveryConfig,
|
||||
WebTorrentEventMap,
|
||||
} from "@suaveplan/network-discovery-webtorrent/types";
|
||||
|
||||
const bus = createEventBus<WebTorrentEventMap>();
|
||||
const registry = new ServiceRegistry();
|
||||
|
||||
bus.on("webtorrent:peer-discovered", ({ peer }) => {
|
||||
console.log("peer:", peer.id, peer.addresses);
|
||||
});
|
||||
|
||||
const config: WebTorrentDiscoveryConfig = {
|
||||
trackers: [
|
||||
"wss://my-tracker-a.example.com",
|
||||
"wss://my-tracker-b.example.com",
|
||||
],
|
||||
announceId: "my-app/v1",
|
||||
maxPeers: 256,
|
||||
timeout: 30_000,
|
||||
p2ptFactory: async (trackers, id) => {
|
||||
const { default: P2PT } = await import("p2pt");
|
||||
return new P2PT(trackers as string[], id);
|
||||
},
|
||||
bus,
|
||||
registry,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WebTorrentPeerMetadata` — `{ id, addresses, protocols, metadata,
|
||||
connectedAt, lastSeen }`. `lastSeen` is intentionally mutable.
|
||||
- `P2PTLike` — `on(event, handler)`, `start()`, `destroy()`. The structural
|
||||
surface every p2pt-like backend (real + fake) must conform to.
|
||||
- `P2PTFactory` — `(trackers, announceId) => P2PTLike | Promise<P2PTLike>`.
|
||||
Production binds to the real `p2pt` package; tests pass an in-memory
|
||||
implementation.
|
||||
- `WebTorrentDiscoveryConfig` — `trackers` (REQUIRED, non-empty),
|
||||
`announceId` (default `"suaveplan-discovery"`), `maxPeers` (default `100`),
|
||||
`timeout` (default `60_000`), `p2ptFactory` (REQUIRED), `bus`, `registry`.
|
||||
- `ResolvedWebTorrentDiscoveryConfig` — defaulted form stored on the
|
||||
orchestrator instance.
|
||||
- `WebTorrentEventMap` — `webtorrent:started`,
|
||||
`webtorrent:peer-discovered`, `webtorrent:peer-lost`,
|
||||
`webtorrent:tracker-error`, `webtorrent:stopped` payload types.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `trackers` array is never defaulted by this package. Upstream's public
|
||||
tracker fallback (`wss://tracker.openwebtorrent.com` / `tracker.btorrent.xyz`
|
||||
/ `tracker.files.fm:7073/announce` / `spacetradersapi-chatbox.herokuapp.com:443/announce`)
|
||||
was deliberately removed — callers must own their tracker infrastructure
|
||||
or supply an explicit list. The Zod schema enforces `.min(1)` and the
|
||||
constructor surfaces violations as `INVALID_OPTIONS`.
|
||||
- `P2PTLike` is intentionally narrower than the real `p2pt` event-emitter
|
||||
surface — only the events this package consumes are typed. The real
|
||||
`p2pt` instance conforms; the test fake conforms; nothing else is required.
|
||||
- `P2PTFactory` returns `P2PTLike | Promise<P2PTLike>` so production code
|
||||
can dynamically import `p2pt` (a Node-flavoured package) without forcing
|
||||
the discovery package itself to bundle it.
|
||||
- `WebTorrentPeerMetadata.lastSeen` is `let`-mutable to allow zero-allocation
|
||||
in-place touch updates — this is the only field that mutates after the
|
||||
record is built.
|
||||
Reference in New Issue
Block a user