@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 — (no submodules)
- automerge-provider — (no submodules)
- crdt-discovery — (no submodules)
- crdt-discovery — (no submodules)
- crdt-intervals — (no submodules)
- crdt-intervals — (no submodules)
- crdt-peer-manager — (no submodules)
- crdt-peer-manager — (no submodules)
- yjs-provider — (no submodules)
- yjs-provider — (no submodules)
- errors — (no submodules)
- errors — (no submodules)
- otel — (no submodules)
- otel — (no submodules)
- types — (no submodules)
- types — (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
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
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
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_FAILEDNET_DISC_CRDT.STATE_DESYNCNET_DISC_CRDT.PEER_REJECTEDNET_DISC_CRDT.INVALID_OPTIONSNET_DISC_CRDT.DISPOSEDNET_DISC_CRDT.YJS_AWARENESS_FAILEDNET_DISC_CRDT.AUTOMERGE_SYNC_FAILED
Observability
- Spans:
disc_crdt.start,disc_crdt.provider.<yjs|automerge>.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.