Files
skills/docs/networking/network/network-discovery-crdt
2026-05-29 08:20:56 +01:00
..
2026-05-29 08:20:56 +01:00
2026-05-29 08:20:56 +01:00

@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)*- [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

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_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.<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.