# @suaveplan/network-discovery-crdt - Browser-tier CRDT DiscoveryStrategy: dual Yjs / Automerge awareness backends behind a single CRDTDiscovery class. Produces deduped PeerInfo[] for @suaveplan/network-bootstrap chains with @suaveplan/events, @suaveplan/timing, AbortSignal, OTel and Zod boundaries. ## Summary Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Wraps an awareness-document CRDT (Yjs *or* Automerge — chosen at construction) and produces a deduped, eventually-consistent set of `PeerInfo[]` for the bootstrap chain to consume. ## Documentation tree - [automerge-provider](./src/core/automerge-provider/automerge-provider.md) — _(no submodules)_ - [automerge-provider](./src/core/automerge-provider/automerge-provider.md) — _(no submodules)_ - [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — _(no submodules)_ - [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — _(no submodules)_ - [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — _(no submodules)_ - [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — _(no submodules)_ - [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — _(no submodules)_ - [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — _(no submodules)_ - [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — _(no submodules)_ - [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — _(no submodules)_ - [errors](./src/errors/errors.md) — _(no submodules)_ - [errors](./src/errors/errors.md) — _(no submodules)_ - [otel](./src/otel/otel.md) — _(no submodules)_ - [otel](./src/otel/otel.md) — _(no submodules)_ - [types](./src/types/types.md) — _(no submodules)_ - [types](./src/types/types.md) — _(no submodules)_ - [automerge-provider](./src/core/automerge-provider/automerge-provider.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)*- [automerge-provider](./src/core/automerge-provider/automerge-provider.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)*- [automerge-provider](./src/core/automerge-provider/automerge-provider.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)*- [automerge-provider](./src/core/automerge-provider/automerge-provider.md) — *(no submodules)*- [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)* - [crdt-discovery](./src/core/crdt-discovery/crdt-discovery.md) — *(no submodules)*- [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)* - [crdt-intervals](./src/core/crdt-intervals/crdt-intervals.md) — *(no submodules)*- [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)* - [crdt-peer-manager](./src/core/crdt-peer-manager/crdt-peer-manager.md) — *(no submodules)*- [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)* - [yjs-provider](./src/core/yjs-provider/yjs-provider.md) — *(no submodules)*- [errors](./src/errors/errors.md) — *(no submodules)* - [errors](./src/errors/errors.md) — *(no submodules)*- [otel](./src/otel/otel.md) — *(no submodules)* - [otel](./src/otel/otel.md) — *(no submodules)*- [types](./src/types/types.md) — *(no submodules)* - [types](./src/types/types.md) — *(no submodules)* ## Why WebRTC mesh discovery without a centralised registry. Every peer maintains a shared CRDT room; presence + metadata are written into the room's `peers` map; the CRDT's conflict-resolution handles network partitions and reconnections without bespoke retry logic. ## Install ```bash bun add @suaveplan/network-discovery-crdt yjs y-webrtc # for Yjs # or bun add @suaveplan/network-discovery-crdt @automerge/automerge-repo @automerge/automerge-repo-network-broadcastchannel @automerge/automerge-repo-network-websocket @automerge/automerge-repo-storage-indexeddb ``` The CRDT libraries are declared as **optional peer dependencies** — install the set for the provider you actually use; the other set can be skipped entirely. ## Usage — Yjs ```typescript import { CRDTDiscovery } from "@suaveplan/network-discovery-crdt"; import { Doc } from "yjs"; import { WebrtcProvider } from "y-webrtc"; const discovery = new CRDTDiscovery({ provider: "yjs", room: "team-A", yjs: { signaling: ["wss://signal.internal.example"], peerExchange: true, }, yjsDocFactory: (room, opts) => { const doc = new Doc(); const provider = new WebrtcProvider(room, doc, { signaling: [...opts.signaling], maxConns: opts.maxConns, peerExchange: opts.peerExchange ?? true, }); return { doc: doc as never, provider: provider as never }; }, }); await discovery.start(); const peers = await discovery.discover(1000); ``` ## Usage — Automerge ```typescript import { CRDTDiscovery } from "@suaveplan/network-discovery-crdt"; import { Repo } from "@automerge/automerge-repo"; import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel"; import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket"; import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb"; const discovery = new CRDTDiscovery({ provider: "automerge", room: "team-A", automerge: { websocketUrl: "wss://sync.internal.example" }, automergeRepoFactory: (room, opts) => { const repo = new Repo({ network: [ new BroadcastChannelNetworkAdapter(), ...(opts.websocketUrl ? [new BrowserWebSocketClientAdapter(opts.websocketUrl)] : []), ], storage: opts.enableIndexedDB !== false ? new IndexedDBStorageAdapter( opts.storageKey ?? "automerge-discovery" ) : undefined, }); // Wrap as RepoLike — see automerge-provider.md. return wrapAsRepoLike(repo, room); }, }); await discovery.start(); ``` ## Architecture ``` CRDTDiscovery — orchestrator implementing DiscoveryStrategy ├── CRDTPeerManager — dedup, EventBusLike + ServiceRegistry side-effects ├── CRDTIntervals — Scheduler-driven announce/heartbeat/cleanup loops └── CRDTProviderAdapter ├── YjsProviderAdapter — Y.Doc + y-webrtc bridge └── AutomergeProviderAdapter — Automerge Repo bridge ``` The orchestrator only ever sees the structural `CRDTProviderAdapter` interface; switching backends means instantiating a new `CRDTDiscovery` with a different `provider`. ## Subpath exports - `@suaveplan/network-discovery-crdt` — root barrel. - `@suaveplan/network-discovery-crdt/types` — types-only entry. - `@suaveplan/network-discovery-crdt/crdt-discovery` — orchestrator class. - `@suaveplan/network-discovery-crdt/crdt-peer-manager` — peer-tracking layer. - `@suaveplan/network-discovery-crdt/crdt-intervals` — periodic-loop scheduler. - `@suaveplan/network-discovery-crdt/yjs-provider` — Yjs adapter. - `@suaveplan/network-discovery-crdt/automerge-provider` — Automerge adapter. ## Errors Every public boundary throws `NetDiscCRDTError` (extends `SuaveplanError`). Branch on `error.code`: - `NET_DISC_CRDT.PROVIDER_FAILED` - `NET_DISC_CRDT.STATE_DESYNC` - `NET_DISC_CRDT.PEER_REJECTED` - `NET_DISC_CRDT.INVALID_OPTIONS` - `NET_DISC_CRDT.DISPOSED` - `NET_DISC_CRDT.YJS_AWARENESS_FAILED` - `NET_DISC_CRDT.AUTOMERGE_SYNC_FAILED` ## Observability - Spans: `disc_crdt.start`, `disc_crdt.provider..sync`. - Counter: `disc_crdt.peers_discovered_total{provider,outcome}` (outcome ∈ `found` | `duplicate` | `rejected`). - Histogram: `disc_crdt.reconcile_duration_ms{provider}`. ## License MIT — see repo root.