@noy-db/by-peer 0.2.0-pre.9 → 0.3.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  WebRTC peer-to-peer transport for [noy-db](https://github.com/vLannaAi/noy-db) — no server in the middle.
4
4
 
5
- > First member of the `by-*` family of session-share transports. See [`docs/packages/by-transports.md`](../../docs/packages/by-transports.md) for the family contract and roster.
5
+ > First member of the `by-*` family of session-share transports. See [`docs/packages/by-transports.md`](https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/packages/by-transports.md) for the family contract and roster.
6
6
  >
7
7
  > Previously published as `@noy-db/p2p` — same code, renamed to fit the prefix family.
8
8
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { NoydbStore } from '@noy-db/hub';
2
+ import { CoordinationProvider } from '@noy-db/hub/kernel';
2
3
 
3
4
  /**
4
5
  * `PeerChannel` — the minimal duplex message primitive used by the p2p
@@ -285,4 +286,65 @@ declare function createOffer(opts?: WebRTCOptions): Promise<Initiator>;
285
286
  */
286
287
  declare function acceptOffer(offer: SessionDescription, opts?: WebRTCOptions): Promise<Responder>;
287
288
 
288
- export { type Initiator, type MinimalLockManager, type PeerChannel, type PeerStoreOptions, type Responder, type RpcClientOptions, type RpcHandler, type RpcMessage, type RpcRequest, type RpcResponse, type ServePeerStoreOptions, type WebRTCOptions, acceptOffer, createOffer, createRpcClient, fromDataChannel, pairInMemory, peerStore, servePeerStore, serveRpc };
289
+ /**
290
+ * **@noy-db/by-peer** real-time {@link CoordinationProvider} — the shared
291
+ * drain-barrier core for every `by-*` transport (#469).
292
+ *
293
+ * The kernel defines the {@link CoordinationProvider} port; the store default
294
+ * polls `_meta/schema-fence`, but a real-time app can inject this push-based
295
+ * provider so a schema cutover fences **instantly** instead of via store
296
+ * polling. The migration cutover and `@klum-db/lobby` both drive the same port
297
+ * through `coordinationStrategy`, so neither names a `by-*` package.
298
+ *
299
+ * This module owns the transport-agnostic logic for ANY {@link PeerChannel}:
300
+ * `@noy-db/by-tabs` (BroadcastChannel) delegates to {@link channelCoordination}
301
+ * here rather than duplicating the protocol, and the WebRTC peer path uses
302
+ * {@link peerCoordination} (the same factory, by its by-peer public name).
303
+ *
304
+ * ## Wire protocol
305
+ *
306
+ * Payloads are JSON strings (the `PeerChannel` is string-only). Every envelope
307
+ * carries a `k` discriminator, namespaced `co-*` so it never collides with a
308
+ * write-relay sharing the same channel:
309
+ *
310
+ * ```jsonc
311
+ * { "k": "co-fence", "vault": "acme", "fence": { ... } }
312
+ * { "k": "co-presence", "vault": "acme", "presence": { ... } }
313
+ * ```
314
+ *
315
+ * ## Local-echo
316
+ *
317
+ * A `PeerChannel` does **not** echo a post back to the sender (neither
318
+ * BroadcastChannel nor a WebRTC DataChannel does), so every local op
319
+ * (`setFence`, `reportPresence`) updates this provider's own in-memory state
320
+ * and notifies its own subscribers *before* broadcasting. Remote ends converge
321
+ * via the `message` listener.
322
+ *
323
+ * @module
324
+ */
325
+
326
+ /**
327
+ * Build a real-time {@link CoordinationProvider} backed by any
328
+ * {@link PeerChannel} — the shared core for every `by-*` transport.
329
+ *
330
+ * The provider owns its in-memory fence + presence maps per vault and keeps
331
+ * them in sync with every other end on the channel. Pass it as
332
+ * `createNoydb({ coordinationStrategy: channelCoordination(ch) })`, or drive it
333
+ * directly via `runDrainBarrier`.
334
+ *
335
+ * `@noy-db/by-tabs` re-exports this under the name `tabsCoordination`, and
336
+ * by-peer exposes the same factory as {@link peerCoordination}.
337
+ */
338
+ declare function channelCoordination(channel: PeerChannel): CoordinationProvider;
339
+ /**
340
+ * by-peer's public name for the shared {@link channelCoordination} core — a
341
+ * real-time {@link CoordinationProvider} over a WebRTC `PeerChannel` (or any
342
+ * other `PeerChannel`). Inject via
343
+ * `createNoydb({ coordinationStrategy: peerCoordination(ch) })`.
344
+ *
345
+ * It is the *same* factory as {@link channelCoordination}; both names are
346
+ * exported so callers can pick the one that reads best at the call site.
347
+ */
348
+ declare const peerCoordination: (channel: PeerChannel) => CoordinationProvider;
349
+
350
+ export { type Initiator, type MinimalLockManager, type PeerChannel, type PeerStoreOptions, type Responder, type RpcClientOptions, type RpcHandler, type RpcMessage, type RpcRequest, type RpcResponse, type ServePeerStoreOptions, type WebRTCOptions, acceptOffer, channelCoordination, createOffer, createRpcClient, fromDataChannel, pairInMemory, peerCoordination, peerStore, servePeerStore, serveRpc };
package/dist/index.js CHANGED
@@ -444,12 +444,111 @@ function waitForIceComplete(pc) {
444
444
  pc.addEventListener("icegatheringstatechange", check);
445
445
  });
446
446
  }
447
+
448
+ // src/coordination.ts
449
+ var DEFAULT_FENCE = { currentSchemaVersion: 0, fenceState: "normal" };
450
+ var DEFAULT_STALE_MS = 3e4;
451
+ function channelCoordination(channel) {
452
+ const vaults = /* @__PURE__ */ new Map();
453
+ function stateFor(vault) {
454
+ let s = vaults.get(vault);
455
+ if (!s) {
456
+ s = {
457
+ fence: DEFAULT_FENCE,
458
+ presence: /* @__PURE__ */ new Map(),
459
+ fenceObservers: /* @__PURE__ */ new Set(),
460
+ presenceObservers: /* @__PURE__ */ new Set()
461
+ };
462
+ vaults.set(vault, s);
463
+ }
464
+ return s;
465
+ }
466
+ function notifyFence(s) {
467
+ for (const cb of [...s.fenceObservers]) cb(s.fence);
468
+ }
469
+ function prune(s, now, staleMs) {
470
+ const live = [];
471
+ for (const [id, p] of [...s.presence]) {
472
+ if (now - p.lastSeen <= staleMs) live.push(p);
473
+ else s.presence.delete(id);
474
+ }
475
+ return live;
476
+ }
477
+ function notifyPresence(s) {
478
+ if (s.presenceObservers.size === 0) return;
479
+ const newest = Math.max(...[...s.presence.values()].map((p) => p.lastSeen));
480
+ const horizon = newest - DEFAULT_STALE_MS;
481
+ const live = [...s.presence.values()].filter((p) => p.lastSeen >= horizon);
482
+ for (const cb of [...s.presenceObservers]) cb(live);
483
+ }
484
+ function send(env) {
485
+ if (!channel.isOpen) return;
486
+ try {
487
+ channel.send(JSON.stringify(env));
488
+ } catch {
489
+ }
490
+ }
491
+ channel.on("message", (raw) => {
492
+ let env;
493
+ try {
494
+ const parsed = JSON.parse(raw);
495
+ if (parsed.k !== "co-fence" && parsed.k !== "co-presence") return;
496
+ env = parsed;
497
+ } catch {
498
+ return;
499
+ }
500
+ if (env.k === "co-fence") {
501
+ const s = stateFor(env.vault);
502
+ s.fence = env.fence;
503
+ notifyFence(s);
504
+ } else {
505
+ const s = stateFor(env.vault);
506
+ s.presence.set(env.presence.writerId, env.presence);
507
+ notifyPresence(s);
508
+ }
509
+ });
510
+ return {
511
+ async setFence(vault, fence) {
512
+ const s = stateFor(vault);
513
+ s.fence = fence;
514
+ notifyFence(s);
515
+ send({ k: "co-fence", vault, fence });
516
+ },
517
+ async readFence(vault) {
518
+ return vaults.get(vault)?.fence ?? DEFAULT_FENCE;
519
+ },
520
+ observeFence(vault, onChange) {
521
+ const s = stateFor(vault);
522
+ s.fenceObservers.add(onChange);
523
+ return () => s.fenceObservers.delete(onChange);
524
+ },
525
+ async reportPresence(vault, p) {
526
+ const s = stateFor(vault);
527
+ s.presence.set(p.writerId, p);
528
+ notifyPresence(s);
529
+ send({ k: "co-presence", vault, presence: p });
530
+ },
531
+ observePresence(vault, onChange) {
532
+ const s = stateFor(vault);
533
+ s.presenceObservers.add(onChange);
534
+ return () => s.presenceObservers.delete(onChange);
535
+ },
536
+ async reachableWriters(vault, o) {
537
+ const s = vaults.get(vault);
538
+ if (!s) return [];
539
+ return prune(s, o.now, o.staleMs);
540
+ }
541
+ };
542
+ }
543
+ var peerCoordination = channelCoordination;
447
544
  export {
448
545
  acceptOffer,
546
+ channelCoordination,
449
547
  createOffer,
450
548
  createRpcClient,
451
549
  fromDataChannel,
452
550
  pairInMemory,
551
+ peerCoordination,
453
552
  peerStore,
454
553
  servePeerStore,
455
554
  serveRpc
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/channel.ts","../src/rpc.ts","../src/peer-store.ts","../src/serve.ts","../src/webrtc.ts"],"sourcesContent":["/**\n * `PeerChannel` — the minimal duplex message primitive used by the p2p\n * NoydbStore wrapper. Any transport that can deliver UTF-8 strings\n * reliably and in-order qualifies: WebRTC DataChannel, BroadcastChannel,\n * MessagePort, WebSocket, even postMessage pairs.\n *\n * Keeping the transport abstract has three payoffs:\n *\n * 1. **Tests run without a WebRTC polyfill.** `pairInMemory()` returns\n * two wired channels for conformance tests against `to-memory`.\n * 2. **Consumers pick their signaling story.** Matrix rooms, QR codes,\n * pastebin, Firebase Realtime DB — the handshake is out of scope.\n * 3. **Future transports slot in cheaply.** WebTransport (HTTP/3),\n * libp2p, Iroh, or a plain relay WebSocket become additional\n * bindings without touching the RPC layer.\n *\n * @module\n */\n\n/**\n * Minimal duplex message primitive.\n *\n * Implementations MUST deliver every `send` payload in order exactly\n * once to every live `on('message')` subscriber. `close()` is best-effort\n * — once called, further `send()` calls MAY throw and `on('close')` MUST\n * fire once.\n */\nexport interface PeerChannel {\n /** Enqueue a payload for delivery to the remote end. */\n send(payload: string): void\n /** Subscribe to incoming payloads or lifecycle events. Returns unsubscribe. */\n on(event: 'message', listener: (payload: string) => void): () => void\n on(event: 'close', listener: () => void): () => void\n /** Close the channel. Idempotent. */\n close(): void\n /** True once the channel is ready for `send`. */\n readonly isOpen: boolean\n}\n\n/**\n * Create a pair of in-memory `PeerChannel`s wired to each other.\n * Intended for tests and multi-tab simulations inside a single process.\n */\nexport function pairInMemory(): [PeerChannel, PeerChannel] {\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n\n function make(): { ch: PeerChannel; listeners: Listeners; closed: { v: boolean } } {\n const listeners: Listeners = { message: new Set(), close: new Set() }\n const closed = { v: false }\n const ch: PeerChannel = {\n get isOpen() {\n return !closed.v\n },\n send() {\n // Placeholder — wired below.\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed.v) return\n closed.v = true\n for (const fn of listeners.close) fn()\n },\n } as PeerChannel\n return { ch, listeners, closed }\n }\n\n const a = make()\n const b = make()\n\n function closeBoth(): void {\n if (!a.closed.v) {\n a.closed.v = true\n for (const fn of a.listeners.close) fn()\n }\n if (!b.closed.v) {\n b.closed.v = true\n for (const fn of b.listeners.close) fn()\n }\n }\n\n a.ch.send = (payload) => {\n if (b.closed.v) throw new Error('PeerChannel closed')\n queueMicrotask(() => {\n for (const fn of b.listeners.message) fn(payload)\n })\n }\n b.ch.send = (payload) => {\n if (a.closed.v) throw new Error('PeerChannel closed')\n queueMicrotask(() => {\n for (const fn of a.listeners.message) fn(payload)\n })\n }\n a.ch.close = closeBoth\n b.ch.close = closeBoth\n\n return [a.ch, b.ch]\n}\n\n/**\n * Wrap a WebRTC `RTCDataChannel` as a `PeerChannel`.\n *\n * Browser-only — the caller is responsible for establishing the\n * `RTCPeerConnection`, exchanging SDP offers/answers out of band, and\n * passing the opened DataChannel here. When the remote peer is only\n * reachable via TURN, the relay sees DTLS-wrapped ciphertext (noy-db\n * already encrypts at rest, so even a TURN compromise leaks nothing).\n */\nexport function fromDataChannel(dc: RTCDataChannel): PeerChannel {\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n dc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n dc.addEventListener('close', () => {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n })\n\n return {\n get isOpen() {\n return !closed && dc.readyState === 'open'\n },\n send(payload) {\n if (closed || dc.readyState !== 'open') {\n throw new Error(`PeerChannel not open (readyState: ${dc.readyState})`)\n }\n dc.send(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n closed = true\n try {\n dc.close()\n } catch {\n // ignore — channel already torn down\n }\n for (const fn of listeners.close) fn()\n },\n } as PeerChannel\n}\n","/**\n * JSON-RPC protocol over a `PeerChannel`.\n *\n * Request shape:\n * `{ t: 'req', id, method, args }`\n * Response shape (success):\n * `{ t: 'res', id, ok: true, result }`\n * Response shape (error):\n * `{ t: 'res', id, ok: false, error: { name, message, version? } }`\n *\n * Why not reuse msgpack/protobuf? The payloads are already base64-encoded\n * ciphertext — further binary packing saves ~8-12% at a large dependency\n * cost. JSON over UTF-8 is inspectable, fits the zero-dependency ethos,\n * and WebRTC DataChannel string mode already frames for us.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\n\n/** Wire format discriminator for RPC messages. */\nexport type RpcMessage = RpcRequest | RpcResponse\n\nexport interface RpcRequest {\n readonly t: 'req'\n readonly id: string\n readonly method: string\n readonly args: readonly unknown[]\n}\n\nexport interface RpcResponse {\n readonly t: 'res'\n readonly id: string\n readonly ok: boolean\n readonly result?: unknown\n readonly error?: { name: string; message: string; version?: number }\n}\n\n/** Handler invoked when an RPC request arrives. Return value is serialized as `result`. */\nexport type RpcHandler = (method: string, args: readonly unknown[]) => Promise<unknown>\n\n/** Options for a client-side RPC caller. */\nexport interface RpcClientOptions {\n /** Max milliseconds to wait for a response before rejecting. */\n timeoutMs?: number\n}\n\n/** Client: wrap a `PeerChannel` in a `call(method, args)` helper. */\nexport function createRpcClient(channel: PeerChannel, opts: RpcClientOptions = {}) {\n const timeoutMs = opts.timeoutMs ?? 30_000\n type Pending = {\n resolve: (v: unknown) => void\n reject: (err: Error) => void\n timer: ReturnType<typeof setTimeout>\n }\n const pending = new Map<string, Pending>()\n let counter = 0\n\n const offMessage = channel.on('message', (payload) => {\n let msg: RpcMessage\n try {\n msg = JSON.parse(payload) as RpcMessage\n } catch {\n return\n }\n if (msg.t !== 'res') return\n const entry = pending.get(msg.id)\n if (!entry) return\n clearTimeout(entry.timer)\n pending.delete(msg.id)\n if (msg.ok) {\n entry.resolve(msg.result)\n } else {\n const e = msg.error ?? { name: 'Error', message: 'unknown remote error' }\n const err = new Error(e.message)\n err.name = e.name\n if (typeof e.version === 'number') {\n ;(err as Error & { version?: number }).version = e.version\n }\n entry.reject(err)\n }\n })\n\n const offClose = channel.on('close', () => {\n for (const [, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new Error('PeerChannel closed before response'))\n }\n pending.clear()\n })\n\n return {\n async call<T = unknown>(method: string, args: readonly unknown[]): Promise<T> {\n const id = `${Date.now().toString(36)}-${(++counter).toString(36)}`\n const req: RpcRequest = { t: 'req', id, method, args }\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id)\n reject(new Error(`RPC ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n })\n try {\n channel.send(JSON.stringify(req))\n } catch (err) {\n clearTimeout(timer)\n pending.delete(id)\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n })\n },\n dispose() {\n offMessage()\n offClose()\n for (const [, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new Error('RPC client disposed'))\n }\n pending.clear()\n },\n }\n}\n\n/** Server: dispatch incoming RPC requests through a handler. Returns a dispose fn. */\nexport function serveRpc(channel: PeerChannel, handler: RpcHandler): () => void {\n async function handle(payload: string): Promise<void> {\n let msg: RpcMessage\n try {\n msg = JSON.parse(payload) as RpcMessage\n } catch {\n return\n }\n if (msg.t !== 'req') return\n\n let response: RpcResponse\n try {\n const result = await handler(msg.method, msg.args)\n response = { t: 'res', id: msg.id, ok: true, result }\n } catch (err) {\n const e = err as Error & { version?: number }\n response = {\n t: 'res',\n id: msg.id,\n ok: false,\n error: {\n name: e.name ?? 'Error',\n message: e.message ?? String(err),\n ...(typeof e.version === 'number' && { version: e.version }),\n },\n }\n }\n\n if (!channel.isOpen) return\n try {\n channel.send(JSON.stringify(response))\n } catch {\n // Channel closed mid-response — nothing to do.\n }\n }\n\n const offMessage = channel.on('message', (payload) => {\n void handle(payload)\n })\n\n return () => offMessage()\n}\n","/**\n * `peerStore()` — a `NoydbStore` backed by RPC calls over a `PeerChannel`.\n *\n * The local peer calls `get`/`put`/`delete`/… against this store as if\n * it were any other backend; every call is serialized as an RPC request\n * to the remote peer, which runs `servePeerStore()` to funnel the RPCs\n * into its own local `NoydbStore`.\n *\n * Error re-hydration: the remote handler re-throws `ConflictError` with\n * a `.version` field when a CAS check fails. The RPC layer carries\n * `version` in the error envelope so the local caller can catch\n * `ConflictError` with the same semantics as a direct store call.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport type { PeerChannel } from './channel.js'\nimport { createRpcClient } from './rpc.js'\n\nexport interface PeerStoreOptions {\n /** The duplex channel to the remote peer. */\n readonly channel: PeerChannel\n /** Max ms to wait for any single RPC response. Default 30s. */\n readonly timeoutMs?: number\n /** Optional display name used in diagnostics. Default `'by-peer'`. */\n readonly name?: string\n}\n\n/**\n * Create a `NoydbStore` that forwards every operation to a remote peer\n * over the supplied `PeerChannel`. The remote peer must be running\n * `servePeerStore()` against its own local store.\n */\nexport function peerStore(opts: PeerStoreOptions): NoydbStore & { dispose: () => void } {\n const rpc = createRpcClient(opts.channel, { timeoutMs: opts.timeoutMs ?? 30_000 })\n\n async function call<T>(method: string, args: readonly unknown[]): Promise<T> {\n try {\n return await rpc.call<T>(method, args)\n } catch (err) {\n // Re-hydrate ConflictError so CAS semantics survive the wire hop.\n const e = err as Error & { version?: number }\n if (e.name === 'ConflictError' && typeof e.version === 'number') {\n throw new ConflictError(e.version, e.message)\n }\n throw err\n }\n }\n\n return {\n name: opts.name ?? 'by-peer',\n\n async get(vault, collection, id) {\n return call<EncryptedEnvelope | null>('get', [vault, collection, id])\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n await call<void>('put', [vault, collection, id, envelope, expectedVersion])\n },\n\n async delete(vault, collection, id) {\n await call<void>('delete', [vault, collection, id])\n },\n\n async list(vault, collection) {\n return call<string[]>('list', [vault, collection])\n },\n\n async loadAll(vault) {\n return call<VaultSnapshot>('loadAll', [vault])\n },\n\n async saveAll(vault, data) {\n await call<void>('saveAll', [vault, data])\n },\n\n async ping() {\n try {\n return await call<boolean>('ping', [])\n } catch {\n return false\n }\n },\n\n dispose() {\n rpc.dispose()\n },\n }\n}\n","/**\n * `servePeerStore()` — runs on the peer that owns the data. Listens on\n * a `PeerChannel` for RPC requests from a remote `peerStore()` client\n * and executes each one against the local `NoydbStore`.\n *\n * The 6 core methods plus the optional `ping` / `listSince` / `listPage`\n * extensions are exposed. Unknown methods surface as a remote Error.\n *\n * **Leader-election (issue #3).** When a `BroadcastChannel`-backed\n * `PeerChannel` is shared by 3+ tabs each running `servePeerStore`,\n * every non-sending tab responds to every RPC — producing duplicate\n * responses for the same request id and O(N²) channel traffic at scale.\n * Pass `leaderElection: { lockName, locks? }` to gate serving on the\n * Web Locks API: only the lock-holding tab registers an RPC handler;\n * others queue and take over when the holder's tab closes (lock auto-\n * releases). Default behaviour (no `leaderElection`) is unchanged for\n * the 2-tab case.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport type { PeerChannel } from './channel.js'\nimport { serveRpc } from './rpc.js'\n\n/**\n * Minimal subset of the Web Locks API used by leader-election. The\n * browser's `navigator.locks` satisfies this; tests inject a stub.\n *\n * @public\n */\nexport interface MinimalLockManager {\n request<T>(\n name: string,\n options: { mode?: 'exclusive' | 'shared'; signal?: AbortSignal },\n callback: (lock: unknown) => Promise<T>,\n ): Promise<T>\n}\n\nexport interface ServePeerStoreOptions {\n /** The duplex channel from the remote peer. */\n readonly channel: PeerChannel\n /** The local store to serve. */\n readonly store: NoydbStore\n /**\n * Optional method whitelist. When provided, any method not in the set\n * is rejected with \"method not allowed\". Useful for read-only peers.\n */\n readonly allow?: ReadonlySet<string>\n /**\n * Opt in to leader-election semantics for cross-tab coordination via\n * the Web Locks API. Required when 3+ tabs share a `BroadcastChannel`-\n * backed `PeerChannel` — without leader election, every non-sending tab\n * responds to every RPC, producing duplicate responses (issue #3).\n *\n * Browser support: Chrome 69+, Firefox 96+, Safari 15.4+.\n *\n * - `lockName` — globally unique lock name. Convention:\n * `noy-db:peer-server:<channel-name>`. The same `lockName` MUST be\n * used by every tab that wants to share the leader role.\n * - `locks` — defaults to `navigator.locks` in browsers. Pass a stub\n * for tests or for non-browser hosts that polyfill the Web Locks API.\n *\n * Behaviour: at most one tab holds the lock at a time. Only that tab\n * registers an RPC handler on the channel; other tabs queue (their\n * `servePeerStore` call returns a working dispose function but does\n * not respond to incoming RPCs). When the holder's tab closes (or its\n * `servePeerStore` is disposed), the next queued tab takes over.\n */\n readonly leaderElection?: {\n readonly lockName: string\n readonly locks?: MinimalLockManager\n }\n}\n\nconst CORE_METHODS = new Set<string>([\n 'get',\n 'put',\n 'delete',\n 'list',\n 'loadAll',\n 'saveAll',\n 'ping',\n 'listSince',\n 'listPage',\n 'listVaults',\n])\n\n/**\n * Start serving the local store on the channel. Returns a dispose\n * function that stops the RPC listener. The underlying channel is NOT\n * closed by dispose — ownership stays with the caller.\n *\n * When `leaderElection` is set, the RPC handler is only registered while\n * this tab holds the named Web Lock. The returned dispose function\n * always works: if called before the lock is acquired, it cancels the\n * wait; if called while holding the lock, it releases the lock and\n * stops serving.\n */\nexport function servePeerStore(opts: ServePeerStoreOptions): () => void {\n if (!opts.leaderElection) {\n return startServing(opts)\n }\n return startServingWithLeaderElection(opts)\n}\n\n/**\n * Register the RPC handler immediately. Used when leader-election is off,\n * and inside the lock callback when on.\n */\nfunction startServing(opts: ServePeerStoreOptions): () => void {\n const { store, channel, allow } = opts\n\n return serveRpc(channel, async (method, args) => {\n if (!CORE_METHODS.has(method)) {\n throw new Error(`Unknown RPC method: ${method}`)\n }\n if (allow && !allow.has(method)) {\n throw new Error(`Method not allowed: ${method}`)\n }\n\n switch (method) {\n case 'get': {\n const [vault, collection, id] = args as [string, string, string]\n return store.get(vault, collection, id)\n }\n case 'put': {\n const [vault, collection, id, envelope, expectedVersion] = args as [\n string,\n string,\n string,\n EncryptedEnvelope,\n number | undefined,\n ]\n await store.put(vault, collection, id, envelope, expectedVersion)\n return null\n }\n case 'delete': {\n const [vault, collection, id] = args as [string, string, string]\n await store.delete(vault, collection, id)\n return null\n }\n case 'list': {\n const [vault, collection] = args as [string, string]\n return store.list(vault, collection)\n }\n case 'loadAll': {\n const [vault] = args as [string]\n return store.loadAll(vault)\n }\n case 'saveAll': {\n const [vault, data] = args as [string, VaultSnapshot]\n await store.saveAll(vault, data)\n return null\n }\n case 'ping': {\n if (!store.ping) return true\n return store.ping()\n }\n case 'listSince': {\n if (!store.listSince) throw new Error('listSince not supported by remote store')\n const [vault, collection, since] = args as [string, string, string]\n return store.listSince(vault, collection, since)\n }\n case 'listPage': {\n if (!store.listPage) throw new Error('listPage not supported by remote store')\n const [vault, collection, cursor, limit] = args as [\n string,\n string,\n string | undefined,\n number | undefined,\n ]\n return store.listPage(vault, collection, cursor, limit)\n }\n case 'listVaults': {\n if (!store.listVaults) throw new Error('listVaults not supported by remote store')\n return store.listVaults()\n }\n }\n /* istanbul ignore next — CORE_METHODS gate makes this unreachable */\n throw new Error(`Unhandled method: ${method}`)\n })\n}\n\n/**\n * Acquire a Web Lock first; only register the RPC listener while\n * holding it. Returns a dispose that cancels the wait (if not yet\n * acquired) or releases the lock (if held).\n */\nfunction startServingWithLeaderElection(opts: ServePeerStoreOptions): () => void {\n // Asserted by the caller branch in `servePeerStore`.\n const leader = opts.leaderElection as { lockName: string; locks?: MinimalLockManager }\n const locks = leader.locks ?? getDefaultLocks()\n const ac = new AbortController()\n let stopServing: (() => void) | null = null\n let releaseLock: (() => void) | null = null\n let disposed = false\n\n void locks\n .request(leader.lockName, { mode: 'exclusive', signal: ac.signal }, () => {\n if (disposed) return Promise.resolve()\n stopServing = startServing(opts)\n // Hold the lock for the lifetime of this tab's leadership. The\n // returned promise stays pending until `releaseLock` is called by\n // the dispose function.\n return new Promise<void>((resolve) => {\n releaseLock = resolve\n })\n })\n .catch((err: unknown) => {\n // AbortError is the expected cancellation when dispose runs before\n // the lock was acquired. Anything else is unexpected — surface it\n // at the next tick so the host can observe it.\n if ((err as { name?: string } | null)?.name === 'AbortError') return\n queueMicrotask(() => {\n throw err\n })\n })\n\n return () => {\n if (disposed) return\n disposed = true\n if (stopServing) {\n stopServing()\n stopServing = null\n }\n if (releaseLock) {\n releaseLock()\n releaseLock = null\n } else {\n ac.abort()\n }\n }\n}\n\nfunction getDefaultLocks(): MinimalLockManager {\n const nav = (globalThis as { navigator?: { locks?: MinimalLockManager } }).navigator\n if (nav && nav.locks) return nav.locks\n throw new Error(\n 'leaderElection requires the Web Locks API (navigator.locks). ' +\n 'Use Chrome 69+, Firefox 96+, Safari 15.4+, or pass `leaderElection.locks: <stub>` ' +\n 'for tests / non-browser hosts.',\n )\n}\n","/**\n * WebRTC handshake helper — opinionated wrapper around\n * `RTCPeerConnection` that produces a ready-to-use `PeerChannel`.\n *\n * The handshake is split into two halves so the caller can ferry the\n * SDP blobs over whatever signaling channel they prefer (QR code,\n * Matrix room, pastebin, Firebase, signed URL…). Signaling is\n * intentionally out of scope — noy-db has no opinion on how peers\n * discover each other, only on what flows once they do.\n *\n * ```ts\n * // Peer A (initiator)\n * const a = await createOffer({ iceServers })\n * send(a.offer) // → signaling channel\n * const answer = await receive() // ← signaling channel\n * await a.accept(answer)\n * const channel = await a.channel // ready PeerChannel\n *\n * // Peer B (responder)\n * const offer = await receive() // ← signaling channel\n * const b = await acceptOffer(offer, { iceServers })\n * send(b.answer) // → signaling channel\n * const channel = await b.channel // ready PeerChannel\n * ```\n *\n * Browser-only. Node consumers who want to interconnect with browsers\n * can plug `@roamhq/wrtc` into the global `RTCPeerConnection` slot; this\n * module does not pull it in so the package has zero runtime deps.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\nimport { fromDataChannel } from './channel.js'\n\ntype PeerConnection = RTCPeerConnection\ntype SessionDescription = RTCSessionDescriptionInit\n\nexport interface WebRTCOptions {\n /** Optional ICE servers (STUN / TURN). */\n readonly iceServers?: RTCIceServer[]\n /** Label for the `RTCDataChannel`. Default `'noydb'`. */\n readonly label?: string\n}\n\nexport interface Initiator {\n readonly offer: SessionDescription\n /** Feed in the remote peer's SDP answer to complete the handshake. */\n accept(answer: SessionDescription): Promise<void>\n /** Resolves with the opened `PeerChannel` once the DataChannel is live. */\n readonly channel: Promise<PeerChannel>\n readonly connection: PeerConnection\n}\n\nexport interface Responder {\n readonly answer: SessionDescription\n /** Resolves with the opened `PeerChannel` once the DataChannel is live. */\n readonly channel: Promise<PeerChannel>\n readonly connection: PeerConnection\n}\n\nfunction requireRTC(): typeof RTCPeerConnection {\n const g = globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }\n if (!g.RTCPeerConnection) {\n throw new Error(\n '[@noy-db/by-peer] globalThis.RTCPeerConnection is undefined — use this module in a browser, or polyfill with @roamhq/wrtc in Node',\n )\n }\n return g.RTCPeerConnection\n}\n\n/**\n * Build an offer as the initiating peer. Returns the SDP offer to send\n * to the remote peer and a promise for the opened `PeerChannel`.\n */\nexport async function createOffer(opts: WebRTCOptions = {}): Promise<Initiator> {\n const RTC = requireRTC()\n const pc = new RTC({\n ...(opts.iceServers && { iceServers: opts.iceServers }),\n })\n\n const dc = pc.createDataChannel(opts.label ?? 'noydb', {\n ordered: true,\n })\n\n const channelPromise = new Promise<PeerChannel>((resolve, reject) => {\n dc.addEventListener('open', () => resolve(fromDataChannel(dc)))\n dc.addEventListener('error', () => reject(new Error('DataChannel error')))\n })\n\n const offer = await pc.createOffer()\n await pc.setLocalDescription(offer)\n await waitForIceComplete(pc)\n\n return {\n offer: pc.localDescription ?? offer,\n connection: pc,\n channel: channelPromise,\n async accept(answer) {\n await pc.setRemoteDescription(answer)\n },\n }\n}\n\n/**\n * Accept an incoming offer as the responding peer. Returns the SDP\n * answer to send back to the initiator and a promise for the opened\n * `PeerChannel`.\n */\nexport async function acceptOffer(\n offer: SessionDescription,\n opts: WebRTCOptions = {},\n): Promise<Responder> {\n const RTC = requireRTC()\n const pc = new RTC({\n ...(opts.iceServers && { iceServers: opts.iceServers }),\n })\n\n const channelPromise = new Promise<PeerChannel>((resolve, reject) => {\n pc.addEventListener('datachannel', (ev) => {\n const dc = ev.channel\n if (dc.readyState === 'open') {\n resolve(fromDataChannel(dc))\n } else {\n dc.addEventListener('open', () => resolve(fromDataChannel(dc)))\n dc.addEventListener('error', () => reject(new Error('DataChannel error')))\n }\n })\n })\n\n await pc.setRemoteDescription(offer)\n const answer = await pc.createAnswer()\n await pc.setLocalDescription(answer)\n await waitForIceComplete(pc)\n\n return {\n answer: pc.localDescription ?? answer,\n connection: pc,\n channel: channelPromise,\n }\n}\n\n/**\n * Resolve once ICE gathering reaches `'complete'`. Using non-trickle ICE\n * keeps the signaling exchange to a single round-trip — at the cost of\n * a slightly longer initial handshake. Consumers that want trickle ICE\n * can bypass this helper and drive `RTCPeerConnection` directly.\n */\nfunction waitForIceComplete(pc: PeerConnection): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve()\n return new Promise((resolve) => {\n const check = () => {\n if (pc.iceGatheringState === 'complete') {\n pc.removeEventListener('icegatheringstatechange', check)\n resolve()\n }\n }\n pc.addEventListener('icegatheringstatechange', check)\n })\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/channel.ts","../src/rpc.ts","../src/peer-store.ts","../src/serve.ts","../src/webrtc.ts","../src/coordination.ts"],"sourcesContent":["/**\n * `PeerChannel` — the minimal duplex message primitive used by the p2p\n * NoydbStore wrapper. Any transport that can deliver UTF-8 strings\n * reliably and in-order qualifies: WebRTC DataChannel, BroadcastChannel,\n * MessagePort, WebSocket, even postMessage pairs.\n *\n * Keeping the transport abstract has three payoffs:\n *\n * 1. **Tests run without a WebRTC polyfill.** `pairInMemory()` returns\n * two wired channels for conformance tests against `to-memory`.\n * 2. **Consumers pick their signaling story.** Matrix rooms, QR codes,\n * pastebin, Firebase Realtime DB — the handshake is out of scope.\n * 3. **Future transports slot in cheaply.** WebTransport (HTTP/3),\n * libp2p, Iroh, or a plain relay WebSocket become additional\n * bindings without touching the RPC layer.\n *\n * @module\n */\n\n/**\n * Minimal duplex message primitive.\n *\n * Implementations MUST deliver every `send` payload in order exactly\n * once to every live `on('message')` subscriber. `close()` is best-effort\n * — once called, further `send()` calls MAY throw and `on('close')` MUST\n * fire once.\n */\nexport interface PeerChannel {\n /** Enqueue a payload for delivery to the remote end. */\n send(payload: string): void\n /** Subscribe to incoming payloads or lifecycle events. Returns unsubscribe. */\n on(event: 'message', listener: (payload: string) => void): () => void\n on(event: 'close', listener: () => void): () => void\n /** Close the channel. Idempotent. */\n close(): void\n /** True once the channel is ready for `send`. */\n readonly isOpen: boolean\n}\n\n/**\n * Create a pair of in-memory `PeerChannel`s wired to each other.\n * Intended for tests and multi-tab simulations inside a single process.\n */\nexport function pairInMemory(): [PeerChannel, PeerChannel] {\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n\n function make(): { ch: PeerChannel; listeners: Listeners; closed: { v: boolean } } {\n const listeners: Listeners = { message: new Set(), close: new Set() }\n const closed = { v: false }\n const ch: PeerChannel = {\n get isOpen() {\n return !closed.v\n },\n send() {\n // Placeholder — wired below.\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed.v) return\n closed.v = true\n for (const fn of listeners.close) fn()\n },\n } as PeerChannel\n return { ch, listeners, closed }\n }\n\n const a = make()\n const b = make()\n\n function closeBoth(): void {\n if (!a.closed.v) {\n a.closed.v = true\n for (const fn of a.listeners.close) fn()\n }\n if (!b.closed.v) {\n b.closed.v = true\n for (const fn of b.listeners.close) fn()\n }\n }\n\n a.ch.send = (payload) => {\n if (b.closed.v) throw new Error('PeerChannel closed')\n queueMicrotask(() => {\n for (const fn of b.listeners.message) fn(payload)\n })\n }\n b.ch.send = (payload) => {\n if (a.closed.v) throw new Error('PeerChannel closed')\n queueMicrotask(() => {\n for (const fn of a.listeners.message) fn(payload)\n })\n }\n a.ch.close = closeBoth\n b.ch.close = closeBoth\n\n return [a.ch, b.ch]\n}\n\n/**\n * Wrap a WebRTC `RTCDataChannel` as a `PeerChannel`.\n *\n * Browser-only — the caller is responsible for establishing the\n * `RTCPeerConnection`, exchanging SDP offers/answers out of band, and\n * passing the opened DataChannel here. When the remote peer is only\n * reachable via TURN, the relay sees DTLS-wrapped ciphertext (noy-db\n * already encrypts at rest, so even a TURN compromise leaks nothing).\n */\nexport function fromDataChannel(dc: RTCDataChannel): PeerChannel {\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n dc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n dc.addEventListener('close', () => {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n })\n\n return {\n get isOpen() {\n return !closed && dc.readyState === 'open'\n },\n send(payload) {\n if (closed || dc.readyState !== 'open') {\n throw new Error(`PeerChannel not open (readyState: ${dc.readyState})`)\n }\n dc.send(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n closed = true\n try {\n dc.close()\n } catch {\n // ignore — channel already torn down\n }\n for (const fn of listeners.close) fn()\n },\n } as PeerChannel\n}\n","/**\n * JSON-RPC protocol over a `PeerChannel`.\n *\n * Request shape:\n * `{ t: 'req', id, method, args }`\n * Response shape (success):\n * `{ t: 'res', id, ok: true, result }`\n * Response shape (error):\n * `{ t: 'res', id, ok: false, error: { name, message, version? } }`\n *\n * Why not reuse msgpack/protobuf? The payloads are already base64-encoded\n * ciphertext — further binary packing saves ~8-12% at a large dependency\n * cost. JSON over UTF-8 is inspectable, fits the zero-dependency ethos,\n * and WebRTC DataChannel string mode already frames for us.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\n\n/** Wire format discriminator for RPC messages. */\nexport type RpcMessage = RpcRequest | RpcResponse\n\nexport interface RpcRequest {\n readonly t: 'req'\n readonly id: string\n readonly method: string\n readonly args: readonly unknown[]\n}\n\nexport interface RpcResponse {\n readonly t: 'res'\n readonly id: string\n readonly ok: boolean\n readonly result?: unknown\n readonly error?: { name: string; message: string; version?: number }\n}\n\n/** Handler invoked when an RPC request arrives. Return value is serialized as `result`. */\nexport type RpcHandler = (method: string, args: readonly unknown[]) => Promise<unknown>\n\n/** Options for a client-side RPC caller. */\nexport interface RpcClientOptions {\n /** Max milliseconds to wait for a response before rejecting. */\n timeoutMs?: number\n}\n\n/** Client: wrap a `PeerChannel` in a `call(method, args)` helper. */\nexport function createRpcClient(channel: PeerChannel, opts: RpcClientOptions = {}) {\n const timeoutMs = opts.timeoutMs ?? 30_000\n type Pending = {\n resolve: (v: unknown) => void\n reject: (err: Error) => void\n timer: ReturnType<typeof setTimeout>\n }\n const pending = new Map<string, Pending>()\n let counter = 0\n\n const offMessage = channel.on('message', (payload) => {\n let msg: RpcMessage\n try {\n msg = JSON.parse(payload) as RpcMessage\n } catch {\n return\n }\n if (msg.t !== 'res') return\n const entry = pending.get(msg.id)\n if (!entry) return\n clearTimeout(entry.timer)\n pending.delete(msg.id)\n if (msg.ok) {\n entry.resolve(msg.result)\n } else {\n const e = msg.error ?? { name: 'Error', message: 'unknown remote error' }\n const err = new Error(e.message)\n err.name = e.name\n if (typeof e.version === 'number') {\n ;(err as Error & { version?: number }).version = e.version\n }\n entry.reject(err)\n }\n })\n\n const offClose = channel.on('close', () => {\n for (const [, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new Error('PeerChannel closed before response'))\n }\n pending.clear()\n })\n\n return {\n async call<T = unknown>(method: string, args: readonly unknown[]): Promise<T> {\n const id = `${Date.now().toString(36)}-${(++counter).toString(36)}`\n const req: RpcRequest = { t: 'req', id, method, args }\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id)\n reject(new Error(`RPC ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n })\n try {\n channel.send(JSON.stringify(req))\n } catch (err) {\n clearTimeout(timer)\n pending.delete(id)\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n })\n },\n dispose() {\n offMessage()\n offClose()\n for (const [, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new Error('RPC client disposed'))\n }\n pending.clear()\n },\n }\n}\n\n/** Server: dispatch incoming RPC requests through a handler. Returns a dispose fn. */\nexport function serveRpc(channel: PeerChannel, handler: RpcHandler): () => void {\n async function handle(payload: string): Promise<void> {\n let msg: RpcMessage\n try {\n msg = JSON.parse(payload) as RpcMessage\n } catch {\n return\n }\n if (msg.t !== 'req') return\n\n let response: RpcResponse\n try {\n const result = await handler(msg.method, msg.args)\n response = { t: 'res', id: msg.id, ok: true, result }\n } catch (err) {\n const e = err as Error & { version?: number }\n response = {\n t: 'res',\n id: msg.id,\n ok: false,\n error: {\n name: e.name ?? 'Error',\n message: e.message ?? String(err),\n ...(typeof e.version === 'number' && { version: e.version }),\n },\n }\n }\n\n if (!channel.isOpen) return\n try {\n channel.send(JSON.stringify(response))\n } catch {\n // Channel closed mid-response — nothing to do.\n }\n }\n\n const offMessage = channel.on('message', (payload) => {\n void handle(payload)\n })\n\n return () => offMessage()\n}\n","/**\n * `peerStore()` — a `NoydbStore` backed by RPC calls over a `PeerChannel`.\n *\n * The local peer calls `get`/`put`/`delete`/… against this store as if\n * it were any other backend; every call is serialized as an RPC request\n * to the remote peer, which runs `servePeerStore()` to funnel the RPCs\n * into its own local `NoydbStore`.\n *\n * Error re-hydration: the remote handler re-throws `ConflictError` with\n * a `.version` field when a CAS check fails. The RPC layer carries\n * `version` in the error envelope so the local caller can catch\n * `ConflictError` with the same semantics as a direct store call.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport type { PeerChannel } from './channel.js'\nimport { createRpcClient } from './rpc.js'\n\nexport interface PeerStoreOptions {\n /** The duplex channel to the remote peer. */\n readonly channel: PeerChannel\n /** Max ms to wait for any single RPC response. Default 30s. */\n readonly timeoutMs?: number\n /** Optional display name used in diagnostics. Default `'by-peer'`. */\n readonly name?: string\n}\n\n/**\n * Create a `NoydbStore` that forwards every operation to a remote peer\n * over the supplied `PeerChannel`. The remote peer must be running\n * `servePeerStore()` against its own local store.\n */\nexport function peerStore(opts: PeerStoreOptions): NoydbStore & { dispose: () => void } {\n const rpc = createRpcClient(opts.channel, { timeoutMs: opts.timeoutMs ?? 30_000 })\n\n async function call<T>(method: string, args: readonly unknown[]): Promise<T> {\n try {\n return await rpc.call<T>(method, args)\n } catch (err) {\n // Re-hydrate ConflictError so CAS semantics survive the wire hop.\n const e = err as Error & { version?: number }\n if (e.name === 'ConflictError' && typeof e.version === 'number') {\n throw new ConflictError(e.version, e.message)\n }\n throw err\n }\n }\n\n return {\n name: opts.name ?? 'by-peer',\n\n async get(vault, collection, id) {\n return call<EncryptedEnvelope | null>('get', [vault, collection, id])\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n await call<void>('put', [vault, collection, id, envelope, expectedVersion])\n },\n\n async delete(vault, collection, id) {\n await call<void>('delete', [vault, collection, id])\n },\n\n async list(vault, collection) {\n return call<string[]>('list', [vault, collection])\n },\n\n async loadAll(vault) {\n return call<VaultSnapshot>('loadAll', [vault])\n },\n\n async saveAll(vault, data) {\n await call<void>('saveAll', [vault, data])\n },\n\n async ping() {\n try {\n return await call<boolean>('ping', [])\n } catch {\n return false\n }\n },\n\n dispose() {\n rpc.dispose()\n },\n }\n}\n","/**\n * `servePeerStore()` — runs on the peer that owns the data. Listens on\n * a `PeerChannel` for RPC requests from a remote `peerStore()` client\n * and executes each one against the local `NoydbStore`.\n *\n * The 6 core methods plus the optional `ping` / `listSince` / `listPage`\n * extensions are exposed. Unknown methods surface as a remote Error.\n *\n * **Leader-election (issue #3).** When a `BroadcastChannel`-backed\n * `PeerChannel` is shared by 3+ tabs each running `servePeerStore`,\n * every non-sending tab responds to every RPC — producing duplicate\n * responses for the same request id and O(N²) channel traffic at scale.\n * Pass `leaderElection: { lockName, locks? }` to gate serving on the\n * Web Locks API: only the lock-holding tab registers an RPC handler;\n * others queue and take over when the holder's tab closes (lock auto-\n * releases). Default behaviour (no `leaderElection`) is unchanged for\n * the 2-tab case.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport type { PeerChannel } from './channel.js'\nimport { serveRpc } from './rpc.js'\n\n/**\n * Minimal subset of the Web Locks API used by leader-election. The\n * browser's `navigator.locks` satisfies this; tests inject a stub.\n *\n * @public\n */\nexport interface MinimalLockManager {\n request<T>(\n name: string,\n options: { mode?: 'exclusive' | 'shared'; signal?: AbortSignal },\n callback: (lock: unknown) => Promise<T>,\n ): Promise<T>\n}\n\nexport interface ServePeerStoreOptions {\n /** The duplex channel from the remote peer. */\n readonly channel: PeerChannel\n /** The local store to serve. */\n readonly store: NoydbStore\n /**\n * Optional method whitelist. When provided, any method not in the set\n * is rejected with \"method not allowed\". Useful for read-only peers.\n */\n readonly allow?: ReadonlySet<string>\n /**\n * Opt in to leader-election semantics for cross-tab coordination via\n * the Web Locks API. Required when 3+ tabs share a `BroadcastChannel`-\n * backed `PeerChannel` — without leader election, every non-sending tab\n * responds to every RPC, producing duplicate responses (issue #3).\n *\n * Browser support: Chrome 69+, Firefox 96+, Safari 15.4+.\n *\n * - `lockName` — globally unique lock name. Convention:\n * `noy-db:peer-server:<channel-name>`. The same `lockName` MUST be\n * used by every tab that wants to share the leader role.\n * - `locks` — defaults to `navigator.locks` in browsers. Pass a stub\n * for tests or for non-browser hosts that polyfill the Web Locks API.\n *\n * Behaviour: at most one tab holds the lock at a time. Only that tab\n * registers an RPC handler on the channel; other tabs queue (their\n * `servePeerStore` call returns a working dispose function but does\n * not respond to incoming RPCs). When the holder's tab closes (or its\n * `servePeerStore` is disposed), the next queued tab takes over.\n */\n readonly leaderElection?: {\n readonly lockName: string\n readonly locks?: MinimalLockManager\n }\n}\n\nconst CORE_METHODS = new Set<string>([\n 'get',\n 'put',\n 'delete',\n 'list',\n 'loadAll',\n 'saveAll',\n 'ping',\n 'listSince',\n 'listPage',\n 'listVaults',\n])\n\n/**\n * Start serving the local store on the channel. Returns a dispose\n * function that stops the RPC listener. The underlying channel is NOT\n * closed by dispose — ownership stays with the caller.\n *\n * When `leaderElection` is set, the RPC handler is only registered while\n * this tab holds the named Web Lock. The returned dispose function\n * always works: if called before the lock is acquired, it cancels the\n * wait; if called while holding the lock, it releases the lock and\n * stops serving.\n */\nexport function servePeerStore(opts: ServePeerStoreOptions): () => void {\n if (!opts.leaderElection) {\n return startServing(opts)\n }\n return startServingWithLeaderElection(opts)\n}\n\n/**\n * Register the RPC handler immediately. Used when leader-election is off,\n * and inside the lock callback when on.\n */\nfunction startServing(opts: ServePeerStoreOptions): () => void {\n const { store, channel, allow } = opts\n\n return serveRpc(channel, async (method, args) => {\n if (!CORE_METHODS.has(method)) {\n throw new Error(`Unknown RPC method: ${method}`)\n }\n if (allow && !allow.has(method)) {\n throw new Error(`Method not allowed: ${method}`)\n }\n\n switch (method) {\n case 'get': {\n const [vault, collection, id] = args as [string, string, string]\n return store.get(vault, collection, id)\n }\n case 'put': {\n const [vault, collection, id, envelope, expectedVersion] = args as [\n string,\n string,\n string,\n EncryptedEnvelope,\n number | undefined,\n ]\n await store.put(vault, collection, id, envelope, expectedVersion)\n return null\n }\n case 'delete': {\n const [vault, collection, id] = args as [string, string, string]\n await store.delete(vault, collection, id)\n return null\n }\n case 'list': {\n const [vault, collection] = args as [string, string]\n return store.list(vault, collection)\n }\n case 'loadAll': {\n const [vault] = args as [string]\n return store.loadAll(vault)\n }\n case 'saveAll': {\n const [vault, data] = args as [string, VaultSnapshot]\n await store.saveAll(vault, data)\n return null\n }\n case 'ping': {\n if (!store.ping) return true\n return store.ping()\n }\n case 'listSince': {\n if (!store.listSince) throw new Error('listSince not supported by remote store')\n const [vault, collection, since] = args as [string, string, string]\n return store.listSince(vault, collection, since)\n }\n case 'listPage': {\n if (!store.listPage) throw new Error('listPage not supported by remote store')\n const [vault, collection, cursor, limit] = args as [\n string,\n string,\n string | undefined,\n number | undefined,\n ]\n return store.listPage(vault, collection, cursor, limit)\n }\n case 'listVaults': {\n if (!store.listVaults) throw new Error('listVaults not supported by remote store')\n return store.listVaults()\n }\n }\n /* istanbul ignore next — CORE_METHODS gate makes this unreachable */\n throw new Error(`Unhandled method: ${method}`)\n })\n}\n\n/**\n * Acquire a Web Lock first; only register the RPC listener while\n * holding it. Returns a dispose that cancels the wait (if not yet\n * acquired) or releases the lock (if held).\n */\nfunction startServingWithLeaderElection(opts: ServePeerStoreOptions): () => void {\n // Asserted by the caller branch in `servePeerStore`.\n const leader = opts.leaderElection as { lockName: string; locks?: MinimalLockManager }\n const locks = leader.locks ?? getDefaultLocks()\n const ac = new AbortController()\n let stopServing: (() => void) | null = null\n let releaseLock: (() => void) | null = null\n let disposed = false\n\n void locks\n .request(leader.lockName, { mode: 'exclusive', signal: ac.signal }, () => {\n if (disposed) return Promise.resolve()\n stopServing = startServing(opts)\n // Hold the lock for the lifetime of this tab's leadership. The\n // returned promise stays pending until `releaseLock` is called by\n // the dispose function.\n return new Promise<void>((resolve) => {\n releaseLock = resolve\n })\n })\n .catch((err: unknown) => {\n // AbortError is the expected cancellation when dispose runs before\n // the lock was acquired. Anything else is unexpected — surface it\n // at the next tick so the host can observe it.\n if ((err as { name?: string } | null)?.name === 'AbortError') return\n queueMicrotask(() => {\n throw err\n })\n })\n\n return () => {\n if (disposed) return\n disposed = true\n if (stopServing) {\n stopServing()\n stopServing = null\n }\n if (releaseLock) {\n releaseLock()\n releaseLock = null\n } else {\n ac.abort()\n }\n }\n}\n\nfunction getDefaultLocks(): MinimalLockManager {\n const nav = (globalThis as { navigator?: { locks?: MinimalLockManager } }).navigator\n if (nav && nav.locks) return nav.locks\n throw new Error(\n 'leaderElection requires the Web Locks API (navigator.locks). ' +\n 'Use Chrome 69+, Firefox 96+, Safari 15.4+, or pass `leaderElection.locks: <stub>` ' +\n 'for tests / non-browser hosts.',\n )\n}\n","/**\n * WebRTC handshake helper — opinionated wrapper around\n * `RTCPeerConnection` that produces a ready-to-use `PeerChannel`.\n *\n * The handshake is split into two halves so the caller can ferry the\n * SDP blobs over whatever signaling channel they prefer (QR code,\n * Matrix room, pastebin, Firebase, signed URL…). Signaling is\n * intentionally out of scope — noy-db has no opinion on how peers\n * discover each other, only on what flows once they do.\n *\n * ```ts\n * // Peer A (initiator)\n * const a = await createOffer({ iceServers })\n * send(a.offer) // → signaling channel\n * const answer = await receive() // ← signaling channel\n * await a.accept(answer)\n * const channel = await a.channel // ready PeerChannel\n *\n * // Peer B (responder)\n * const offer = await receive() // ← signaling channel\n * const b = await acceptOffer(offer, { iceServers })\n * send(b.answer) // → signaling channel\n * const channel = await b.channel // ready PeerChannel\n * ```\n *\n * Browser-only. Node consumers who want to interconnect with browsers\n * can plug `@roamhq/wrtc` into the global `RTCPeerConnection` slot; this\n * module does not pull it in so the package has zero runtime deps.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\nimport { fromDataChannel } from './channel.js'\n\ntype PeerConnection = RTCPeerConnection\ntype SessionDescription = RTCSessionDescriptionInit\n\nexport interface WebRTCOptions {\n /** Optional ICE servers (STUN / TURN). */\n readonly iceServers?: RTCIceServer[]\n /** Label for the `RTCDataChannel`. Default `'noydb'`. */\n readonly label?: string\n}\n\nexport interface Initiator {\n readonly offer: SessionDescription\n /** Feed in the remote peer's SDP answer to complete the handshake. */\n accept(answer: SessionDescription): Promise<void>\n /** Resolves with the opened `PeerChannel` once the DataChannel is live. */\n readonly channel: Promise<PeerChannel>\n readonly connection: PeerConnection\n}\n\nexport interface Responder {\n readonly answer: SessionDescription\n /** Resolves with the opened `PeerChannel` once the DataChannel is live. */\n readonly channel: Promise<PeerChannel>\n readonly connection: PeerConnection\n}\n\nfunction requireRTC(): typeof RTCPeerConnection {\n const g = globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }\n if (!g.RTCPeerConnection) {\n throw new Error(\n '[@noy-db/by-peer] globalThis.RTCPeerConnection is undefined — use this module in a browser, or polyfill with @roamhq/wrtc in Node',\n )\n }\n return g.RTCPeerConnection\n}\n\n/**\n * Build an offer as the initiating peer. Returns the SDP offer to send\n * to the remote peer and a promise for the opened `PeerChannel`.\n */\nexport async function createOffer(opts: WebRTCOptions = {}): Promise<Initiator> {\n const RTC = requireRTC()\n const pc = new RTC({\n ...(opts.iceServers && { iceServers: opts.iceServers }),\n })\n\n const dc = pc.createDataChannel(opts.label ?? 'noydb', {\n ordered: true,\n })\n\n const channelPromise = new Promise<PeerChannel>((resolve, reject) => {\n dc.addEventListener('open', () => resolve(fromDataChannel(dc)))\n dc.addEventListener('error', () => reject(new Error('DataChannel error')))\n })\n\n const offer = await pc.createOffer()\n await pc.setLocalDescription(offer)\n await waitForIceComplete(pc)\n\n return {\n offer: pc.localDescription ?? offer,\n connection: pc,\n channel: channelPromise,\n async accept(answer) {\n await pc.setRemoteDescription(answer)\n },\n }\n}\n\n/**\n * Accept an incoming offer as the responding peer. Returns the SDP\n * answer to send back to the initiator and a promise for the opened\n * `PeerChannel`.\n */\nexport async function acceptOffer(\n offer: SessionDescription,\n opts: WebRTCOptions = {},\n): Promise<Responder> {\n const RTC = requireRTC()\n const pc = new RTC({\n ...(opts.iceServers && { iceServers: opts.iceServers }),\n })\n\n const channelPromise = new Promise<PeerChannel>((resolve, reject) => {\n pc.addEventListener('datachannel', (ev) => {\n const dc = ev.channel\n if (dc.readyState === 'open') {\n resolve(fromDataChannel(dc))\n } else {\n dc.addEventListener('open', () => resolve(fromDataChannel(dc)))\n dc.addEventListener('error', () => reject(new Error('DataChannel error')))\n }\n })\n })\n\n await pc.setRemoteDescription(offer)\n const answer = await pc.createAnswer()\n await pc.setLocalDescription(answer)\n await waitForIceComplete(pc)\n\n return {\n answer: pc.localDescription ?? answer,\n connection: pc,\n channel: channelPromise,\n }\n}\n\n/**\n * Resolve once ICE gathering reaches `'complete'`. Using non-trickle ICE\n * keeps the signaling exchange to a single round-trip — at the cost of\n * a slightly longer initial handshake. Consumers that want trickle ICE\n * can bypass this helper and drive `RTCPeerConnection` directly.\n */\nfunction waitForIceComplete(pc: PeerConnection): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve()\n return new Promise((resolve) => {\n const check = () => {\n if (pc.iceGatheringState === 'complete') {\n pc.removeEventListener('icegatheringstatechange', check)\n resolve()\n }\n }\n pc.addEventListener('icegatheringstatechange', check)\n })\n}\n","/**\n * **@noy-db/by-peer** real-time {@link CoordinationProvider} — the shared\n * drain-barrier core for every `by-*` transport (#469).\n *\n * The kernel defines the {@link CoordinationProvider} port; the store default\n * polls `_meta/schema-fence`, but a real-time app can inject this push-based\n * provider so a schema cutover fences **instantly** instead of via store\n * polling. The migration cutover and `@klum-db/lobby` both drive the same port\n * through `coordinationStrategy`, so neither names a `by-*` package.\n *\n * This module owns the transport-agnostic logic for ANY {@link PeerChannel}:\n * `@noy-db/by-tabs` (BroadcastChannel) delegates to {@link channelCoordination}\n * here rather than duplicating the protocol, and the WebRTC peer path uses\n * {@link peerCoordination} (the same factory, by its by-peer public name).\n *\n * ## Wire protocol\n *\n * Payloads are JSON strings (the `PeerChannel` is string-only). Every envelope\n * carries a `k` discriminator, namespaced `co-*` so it never collides with a\n * write-relay sharing the same channel:\n *\n * ```jsonc\n * { \"k\": \"co-fence\", \"vault\": \"acme\", \"fence\": { ... } }\n * { \"k\": \"co-presence\", \"vault\": \"acme\", \"presence\": { ... } }\n * ```\n *\n * ## Local-echo\n *\n * A `PeerChannel` does **not** echo a post back to the sender (neither\n * BroadcastChannel nor a WebRTC DataChannel does), so every local op\n * (`setFence`, `reportPresence`) updates this provider's own in-memory state\n * and notifies its own subscribers *before* broadcasting. Remote ends converge\n * via the `message` listener.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\nimport type { CoordinationProvider, FenceState, WriterPresence } from '@noy-db/hub/kernel'\n\n/** Default fence when a vault has never been fenced. */\nconst DEFAULT_FENCE: FenceState = { currentSchemaVersion: 0, fenceState: 'normal' }\n\n/**\n * Default presence horizon used when pruning before an `observePresence` emit\n * (`reachableWriters` is always given an explicit `staleMs` by the caller). An\n * end that stops heartbeating for longer than this drops out of pushed batches.\n */\nconst DEFAULT_STALE_MS = 30_000\n\ntype FenceEnvelope = { readonly k: 'co-fence'; readonly vault: string; readonly fence: FenceState }\ntype PresenceEnvelope = {\n readonly k: 'co-presence'\n readonly vault: string\n readonly presence: WriterPresence\n}\ntype Envelope = FenceEnvelope | PresenceEnvelope\n\ninterface VaultState {\n fence: FenceState\n readonly presence: Map<string, WriterPresence>\n readonly fenceObservers: Set<(f: FenceState) => void>\n readonly presenceObservers: Set<(w: readonly WriterPresence[]) => void>\n}\n\n/**\n * Build a real-time {@link CoordinationProvider} backed by any\n * {@link PeerChannel} — the shared core for every `by-*` transport.\n *\n * The provider owns its in-memory fence + presence maps per vault and keeps\n * them in sync with every other end on the channel. Pass it as\n * `createNoydb({ coordinationStrategy: channelCoordination(ch) })`, or drive it\n * directly via `runDrainBarrier`.\n *\n * `@noy-db/by-tabs` re-exports this under the name `tabsCoordination`, and\n * by-peer exposes the same factory as {@link peerCoordination}.\n */\nexport function channelCoordination(channel: PeerChannel): CoordinationProvider {\n const vaults = new Map<string, VaultState>()\n\n function stateFor(vault: string): VaultState {\n let s = vaults.get(vault)\n if (!s) {\n s = {\n fence: DEFAULT_FENCE,\n presence: new Map(),\n fenceObservers: new Set(),\n presenceObservers: new Set(),\n }\n vaults.set(vault, s)\n }\n return s\n }\n\n function notifyFence(s: VaultState): void {\n for (const cb of [...s.fenceObservers]) cb(s.fence)\n }\n\n /**\n * Prune a vault's presence map to writers seen within `staleMs` of `now`,\n * deleting stale entries from the canonical map. Callers pass their own\n * authoritative clock — never a wall clock — so a synthetic test clock and a\n * real `Date.now()` heartbeat stay consistent.\n */\n function prune(s: VaultState, now: number, staleMs: number): WriterPresence[] {\n const live: WriterPresence[] = []\n for (const [id, p] of [...s.presence]) {\n if (now - p.lastSeen <= staleMs) live.push(p)\n else s.presence.delete(id)\n }\n return live\n }\n\n function notifyPresence(s: VaultState): void {\n if (s.presenceObservers.size === 0) return\n // Best-effort prune for the *pushed batch* so an end that stopped\n // heartbeating drops out, but DON'T mutate the canonical map here: emits\n // happen on the provider's local clock, while `reportPresence` callers may\n // stamp `lastSeen` with a synthetic clock. Authoritative deletion belongs to\n // `reachableWriters`, which is handed the caller's clock + staleMs.\n const newest = Math.max(...[...s.presence.values()].map((p) => p.lastSeen))\n const horizon = newest - DEFAULT_STALE_MS\n const live = [...s.presence.values()].filter((p) => p.lastSeen >= horizon)\n for (const cb of [...s.presenceObservers]) cb(live)\n }\n\n function send(env: Envelope): void {\n if (!channel.isOpen) return\n try {\n channel.send(JSON.stringify(env))\n } catch {\n // Channel torn down mid-flight — local state already updated; drop.\n }\n }\n\n channel.on('message', (raw: string) => {\n let env: Envelope\n try {\n const parsed = JSON.parse(raw) as { k?: unknown }\n if (parsed.k !== 'co-fence' && parsed.k !== 'co-presence') return\n env = parsed as Envelope\n } catch {\n return // not our message / malformed JSON\n }\n\n if (env.k === 'co-fence') {\n const s = stateFor(env.vault)\n s.fence = env.fence\n notifyFence(s)\n } else {\n const s = stateFor(env.vault)\n s.presence.set(env.presence.writerId, env.presence)\n notifyPresence(s)\n }\n })\n\n return {\n async setFence(vault: string, fence: FenceState): Promise<void> {\n const s = stateFor(vault)\n s.fence = fence\n notifyFence(s)\n send({ k: 'co-fence', vault, fence })\n },\n\n async readFence(vault: string): Promise<FenceState> {\n return vaults.get(vault)?.fence ?? DEFAULT_FENCE\n },\n\n observeFence(vault: string, onChange: (f: FenceState) => void): () => void {\n const s = stateFor(vault)\n s.fenceObservers.add(onChange)\n return () => s.fenceObservers.delete(onChange)\n },\n\n async reportPresence(vault: string, p: WriterPresence): Promise<void> {\n const s = stateFor(vault)\n s.presence.set(p.writerId, p)\n notifyPresence(s)\n send({ k: 'co-presence', vault, presence: p })\n },\n\n observePresence(vault: string, onChange: (w: readonly WriterPresence[]) => void): () => void {\n const s = stateFor(vault)\n s.presenceObservers.add(onChange)\n return () => s.presenceObservers.delete(onChange)\n },\n\n async reachableWriters(\n vault: string,\n o: { staleMs: number; now: number },\n ): Promise<readonly WriterPresence[]> {\n const s = vaults.get(vault)\n if (!s) return []\n return prune(s, o.now, o.staleMs)\n },\n }\n}\n\n/**\n * by-peer's public name for the shared {@link channelCoordination} core — a\n * real-time {@link CoordinationProvider} over a WebRTC `PeerChannel` (or any\n * other `PeerChannel`). Inject via\n * `createNoydb({ coordinationStrategy: peerCoordination(ch) })`.\n *\n * It is the *same* factory as {@link channelCoordination}; both names are\n * exported so callers can pick the one that reads best at the call site.\n */\nexport const peerCoordination: (channel: PeerChannel) => CoordinationProvider = channelCoordination\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/by-peer",
3
- "version": "0.2.0-pre.9",
3
+ "version": "0.3.0-pre.1",
4
4
  "description": "WebRTC peer-to-peer transport for noy-db — direct browser-to-browser channel with signaling-agnostic handshake",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -17,17 +17,10 @@
17
17
  "sideEffects": false,
18
18
  "exports": {
19
19
  ".": {
20
- "import": {
21
- "types": "./dist/index.d.ts",
22
- "default": "./dist/index.js"
23
- },
24
- "require": {
25
- "types": "./dist/index.d.cts",
26
- "default": "./dist/index.cjs"
27
- }
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
28
22
  }
29
23
  },
30
- "main": "./dist/index.cjs",
31
24
  "module": "./dist/index.js",
32
25
  "types": "./dist/index.d.ts",
33
26
  "files": [
@@ -36,14 +29,14 @@
36
29
  "LICENSE"
37
30
  ],
38
31
  "engines": {
39
- "node": ">=18.0.0"
32
+ "node": ">=22.0.0"
40
33
  },
41
34
  "peerDependencies": {
42
- "@noy-db/hub": "0.2.0-pre.9"
35
+ "@noy-db/hub": "0.3.0-pre.1"
43
36
  },
44
37
  "devDependencies": {
45
- "@noy-db/hub": "0.2.0-pre.9",
46
- "@noy-db/to-memory": "0.2.0-pre.9"
38
+ "@noy-db/hub": "0.3.0-pre.1",
39
+ "@noy-db/to-memory": "0.3.0-pre.1"
47
40
  },
48
41
  "keywords": [
49
42
  "noy-db",
package/dist/index.cjs DELETED
@@ -1,491 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- acceptOffer: () => acceptOffer,
24
- createOffer: () => createOffer,
25
- createRpcClient: () => createRpcClient,
26
- fromDataChannel: () => fromDataChannel,
27
- pairInMemory: () => pairInMemory,
28
- peerStore: () => peerStore,
29
- servePeerStore: () => servePeerStore,
30
- serveRpc: () => serveRpc
31
- });
32
- module.exports = __toCommonJS(index_exports);
33
-
34
- // src/channel.ts
35
- function pairInMemory() {
36
- function make() {
37
- const listeners = { message: /* @__PURE__ */ new Set(), close: /* @__PURE__ */ new Set() };
38
- const closed = { v: false };
39
- const ch = {
40
- get isOpen() {
41
- return !closed.v;
42
- },
43
- send() {
44
- },
45
- on(event, listener) {
46
- if (event === "message") {
47
- listeners.message.add(listener);
48
- return () => listeners.message.delete(listener);
49
- }
50
- listeners.close.add(listener);
51
- return () => listeners.close.delete(listener);
52
- },
53
- close() {
54
- if (closed.v) return;
55
- closed.v = true;
56
- for (const fn of listeners.close) fn();
57
- }
58
- };
59
- return { ch, listeners, closed };
60
- }
61
- const a = make();
62
- const b = make();
63
- function closeBoth() {
64
- if (!a.closed.v) {
65
- a.closed.v = true;
66
- for (const fn of a.listeners.close) fn();
67
- }
68
- if (!b.closed.v) {
69
- b.closed.v = true;
70
- for (const fn of b.listeners.close) fn();
71
- }
72
- }
73
- a.ch.send = (payload) => {
74
- if (b.closed.v) throw new Error("PeerChannel closed");
75
- queueMicrotask(() => {
76
- for (const fn of b.listeners.message) fn(payload);
77
- });
78
- };
79
- b.ch.send = (payload) => {
80
- if (a.closed.v) throw new Error("PeerChannel closed");
81
- queueMicrotask(() => {
82
- for (const fn of a.listeners.message) fn(payload);
83
- });
84
- };
85
- a.ch.close = closeBoth;
86
- b.ch.close = closeBoth;
87
- return [a.ch, b.ch];
88
- }
89
- function fromDataChannel(dc) {
90
- const listeners = { message: /* @__PURE__ */ new Set(), close: /* @__PURE__ */ new Set() };
91
- let closed = false;
92
- dc.addEventListener("message", (ev) => {
93
- if (typeof ev.data !== "string") return;
94
- for (const fn of listeners.message) fn(ev.data);
95
- });
96
- dc.addEventListener("close", () => {
97
- if (closed) return;
98
- closed = true;
99
- for (const fn of listeners.close) fn();
100
- });
101
- return {
102
- get isOpen() {
103
- return !closed && dc.readyState === "open";
104
- },
105
- send(payload) {
106
- if (closed || dc.readyState !== "open") {
107
- throw new Error(`PeerChannel not open (readyState: ${dc.readyState})`);
108
- }
109
- dc.send(payload);
110
- },
111
- on(event, listener) {
112
- if (event === "message") {
113
- listeners.message.add(listener);
114
- return () => listeners.message.delete(listener);
115
- }
116
- listeners.close.add(listener);
117
- return () => listeners.close.delete(listener);
118
- },
119
- close() {
120
- if (closed) return;
121
- closed = true;
122
- try {
123
- dc.close();
124
- } catch {
125
- }
126
- for (const fn of listeners.close) fn();
127
- }
128
- };
129
- }
130
-
131
- // src/rpc.ts
132
- function createRpcClient(channel, opts = {}) {
133
- const timeoutMs = opts.timeoutMs ?? 3e4;
134
- const pending = /* @__PURE__ */ new Map();
135
- let counter = 0;
136
- const offMessage = channel.on("message", (payload) => {
137
- let msg;
138
- try {
139
- msg = JSON.parse(payload);
140
- } catch {
141
- return;
142
- }
143
- if (msg.t !== "res") return;
144
- const entry = pending.get(msg.id);
145
- if (!entry) return;
146
- clearTimeout(entry.timer);
147
- pending.delete(msg.id);
148
- if (msg.ok) {
149
- entry.resolve(msg.result);
150
- } else {
151
- const e = msg.error ?? { name: "Error", message: "unknown remote error" };
152
- const err = new Error(e.message);
153
- err.name = e.name;
154
- if (typeof e.version === "number") {
155
- ;
156
- err.version = e.version;
157
- }
158
- entry.reject(err);
159
- }
160
- });
161
- const offClose = channel.on("close", () => {
162
- for (const [, entry] of pending) {
163
- clearTimeout(entry.timer);
164
- entry.reject(new Error("PeerChannel closed before response"));
165
- }
166
- pending.clear();
167
- });
168
- return {
169
- async call(method, args) {
170
- const id = `${Date.now().toString(36)}-${(++counter).toString(36)}`;
171
- const req = { t: "req", id, method, args };
172
- return new Promise((resolve, reject) => {
173
- const timer = setTimeout(() => {
174
- pending.delete(id);
175
- reject(new Error(`RPC ${method} timed out after ${timeoutMs}ms`));
176
- }, timeoutMs);
177
- pending.set(id, {
178
- resolve,
179
- reject,
180
- timer
181
- });
182
- try {
183
- channel.send(JSON.stringify(req));
184
- } catch (err) {
185
- clearTimeout(timer);
186
- pending.delete(id);
187
- reject(err instanceof Error ? err : new Error(String(err)));
188
- }
189
- });
190
- },
191
- dispose() {
192
- offMessage();
193
- offClose();
194
- for (const [, entry] of pending) {
195
- clearTimeout(entry.timer);
196
- entry.reject(new Error("RPC client disposed"));
197
- }
198
- pending.clear();
199
- }
200
- };
201
- }
202
- function serveRpc(channel, handler) {
203
- async function handle(payload) {
204
- let msg;
205
- try {
206
- msg = JSON.parse(payload);
207
- } catch {
208
- return;
209
- }
210
- if (msg.t !== "req") return;
211
- let response;
212
- try {
213
- const result = await handler(msg.method, msg.args);
214
- response = { t: "res", id: msg.id, ok: true, result };
215
- } catch (err) {
216
- const e = err;
217
- response = {
218
- t: "res",
219
- id: msg.id,
220
- ok: false,
221
- error: {
222
- name: e.name ?? "Error",
223
- message: e.message ?? String(err),
224
- ...typeof e.version === "number" && { version: e.version }
225
- }
226
- };
227
- }
228
- if (!channel.isOpen) return;
229
- try {
230
- channel.send(JSON.stringify(response));
231
- } catch {
232
- }
233
- }
234
- const offMessage = channel.on("message", (payload) => {
235
- void handle(payload);
236
- });
237
- return () => offMessage();
238
- }
239
-
240
- // src/peer-store.ts
241
- var import_hub = require("@noy-db/hub");
242
- function peerStore(opts) {
243
- const rpc = createRpcClient(opts.channel, { timeoutMs: opts.timeoutMs ?? 3e4 });
244
- async function call(method, args) {
245
- try {
246
- return await rpc.call(method, args);
247
- } catch (err) {
248
- const e = err;
249
- if (e.name === "ConflictError" && typeof e.version === "number") {
250
- throw new import_hub.ConflictError(e.version, e.message);
251
- }
252
- throw err;
253
- }
254
- }
255
- return {
256
- name: opts.name ?? "by-peer",
257
- async get(vault, collection, id) {
258
- return call("get", [vault, collection, id]);
259
- },
260
- async put(vault, collection, id, envelope, expectedVersion) {
261
- await call("put", [vault, collection, id, envelope, expectedVersion]);
262
- },
263
- async delete(vault, collection, id) {
264
- await call("delete", [vault, collection, id]);
265
- },
266
- async list(vault, collection) {
267
- return call("list", [vault, collection]);
268
- },
269
- async loadAll(vault) {
270
- return call("loadAll", [vault]);
271
- },
272
- async saveAll(vault, data) {
273
- await call("saveAll", [vault, data]);
274
- },
275
- async ping() {
276
- try {
277
- return await call("ping", []);
278
- } catch {
279
- return false;
280
- }
281
- },
282
- dispose() {
283
- rpc.dispose();
284
- }
285
- };
286
- }
287
-
288
- // src/serve.ts
289
- var CORE_METHODS = /* @__PURE__ */ new Set([
290
- "get",
291
- "put",
292
- "delete",
293
- "list",
294
- "loadAll",
295
- "saveAll",
296
- "ping",
297
- "listSince",
298
- "listPage",
299
- "listVaults"
300
- ]);
301
- function servePeerStore(opts) {
302
- if (!opts.leaderElection) {
303
- return startServing(opts);
304
- }
305
- return startServingWithLeaderElection(opts);
306
- }
307
- function startServing(opts) {
308
- const { store, channel, allow } = opts;
309
- return serveRpc(channel, async (method, args) => {
310
- if (!CORE_METHODS.has(method)) {
311
- throw new Error(`Unknown RPC method: ${method}`);
312
- }
313
- if (allow && !allow.has(method)) {
314
- throw new Error(`Method not allowed: ${method}`);
315
- }
316
- switch (method) {
317
- case "get": {
318
- const [vault, collection, id] = args;
319
- return store.get(vault, collection, id);
320
- }
321
- case "put": {
322
- const [vault, collection, id, envelope, expectedVersion] = args;
323
- await store.put(vault, collection, id, envelope, expectedVersion);
324
- return null;
325
- }
326
- case "delete": {
327
- const [vault, collection, id] = args;
328
- await store.delete(vault, collection, id);
329
- return null;
330
- }
331
- case "list": {
332
- const [vault, collection] = args;
333
- return store.list(vault, collection);
334
- }
335
- case "loadAll": {
336
- const [vault] = args;
337
- return store.loadAll(vault);
338
- }
339
- case "saveAll": {
340
- const [vault, data] = args;
341
- await store.saveAll(vault, data);
342
- return null;
343
- }
344
- case "ping": {
345
- if (!store.ping) return true;
346
- return store.ping();
347
- }
348
- case "listSince": {
349
- if (!store.listSince) throw new Error("listSince not supported by remote store");
350
- const [vault, collection, since] = args;
351
- return store.listSince(vault, collection, since);
352
- }
353
- case "listPage": {
354
- if (!store.listPage) throw new Error("listPage not supported by remote store");
355
- const [vault, collection, cursor, limit] = args;
356
- return store.listPage(vault, collection, cursor, limit);
357
- }
358
- case "listVaults": {
359
- if (!store.listVaults) throw new Error("listVaults not supported by remote store");
360
- return store.listVaults();
361
- }
362
- }
363
- throw new Error(`Unhandled method: ${method}`);
364
- });
365
- }
366
- function startServingWithLeaderElection(opts) {
367
- const leader = opts.leaderElection;
368
- const locks = leader.locks ?? getDefaultLocks();
369
- const ac = new AbortController();
370
- let stopServing = null;
371
- let releaseLock = null;
372
- let disposed = false;
373
- void locks.request(leader.lockName, { mode: "exclusive", signal: ac.signal }, () => {
374
- if (disposed) return Promise.resolve();
375
- stopServing = startServing(opts);
376
- return new Promise((resolve) => {
377
- releaseLock = resolve;
378
- });
379
- }).catch((err) => {
380
- if (err?.name === "AbortError") return;
381
- queueMicrotask(() => {
382
- throw err;
383
- });
384
- });
385
- return () => {
386
- if (disposed) return;
387
- disposed = true;
388
- if (stopServing) {
389
- stopServing();
390
- stopServing = null;
391
- }
392
- if (releaseLock) {
393
- releaseLock();
394
- releaseLock = null;
395
- } else {
396
- ac.abort();
397
- }
398
- };
399
- }
400
- function getDefaultLocks() {
401
- const nav = globalThis.navigator;
402
- if (nav && nav.locks) return nav.locks;
403
- throw new Error(
404
- "leaderElection requires the Web Locks API (navigator.locks). Use Chrome 69+, Firefox 96+, Safari 15.4+, or pass `leaderElection.locks: <stub>` for tests / non-browser hosts."
405
- );
406
- }
407
-
408
- // src/webrtc.ts
409
- function requireRTC() {
410
- const g = globalThis;
411
- if (!g.RTCPeerConnection) {
412
- throw new Error(
413
- "[@noy-db/by-peer] globalThis.RTCPeerConnection is undefined \u2014 use this module in a browser, or polyfill with @roamhq/wrtc in Node"
414
- );
415
- }
416
- return g.RTCPeerConnection;
417
- }
418
- async function createOffer(opts = {}) {
419
- const RTC = requireRTC();
420
- const pc = new RTC({
421
- ...opts.iceServers && { iceServers: opts.iceServers }
422
- });
423
- const dc = pc.createDataChannel(opts.label ?? "noydb", {
424
- ordered: true
425
- });
426
- const channelPromise = new Promise((resolve, reject) => {
427
- dc.addEventListener("open", () => resolve(fromDataChannel(dc)));
428
- dc.addEventListener("error", () => reject(new Error("DataChannel error")));
429
- });
430
- const offer = await pc.createOffer();
431
- await pc.setLocalDescription(offer);
432
- await waitForIceComplete(pc);
433
- return {
434
- offer: pc.localDescription ?? offer,
435
- connection: pc,
436
- channel: channelPromise,
437
- async accept(answer) {
438
- await pc.setRemoteDescription(answer);
439
- }
440
- };
441
- }
442
- async function acceptOffer(offer, opts = {}) {
443
- const RTC = requireRTC();
444
- const pc = new RTC({
445
- ...opts.iceServers && { iceServers: opts.iceServers }
446
- });
447
- const channelPromise = new Promise((resolve, reject) => {
448
- pc.addEventListener("datachannel", (ev) => {
449
- const dc = ev.channel;
450
- if (dc.readyState === "open") {
451
- resolve(fromDataChannel(dc));
452
- } else {
453
- dc.addEventListener("open", () => resolve(fromDataChannel(dc)));
454
- dc.addEventListener("error", () => reject(new Error("DataChannel error")));
455
- }
456
- });
457
- });
458
- await pc.setRemoteDescription(offer);
459
- const answer = await pc.createAnswer();
460
- await pc.setLocalDescription(answer);
461
- await waitForIceComplete(pc);
462
- return {
463
- answer: pc.localDescription ?? answer,
464
- connection: pc,
465
- channel: channelPromise
466
- };
467
- }
468
- function waitForIceComplete(pc) {
469
- if (pc.iceGatheringState === "complete") return Promise.resolve();
470
- return new Promise((resolve) => {
471
- const check = () => {
472
- if (pc.iceGatheringState === "complete") {
473
- pc.removeEventListener("icegatheringstatechange", check);
474
- resolve();
475
- }
476
- };
477
- pc.addEventListener("icegatheringstatechange", check);
478
- });
479
- }
480
- // Annotate the CommonJS export names for ESM import in node:
481
- 0 && (module.exports = {
482
- acceptOffer,
483
- createOffer,
484
- createRpcClient,
485
- fromDataChannel,
486
- pairInMemory,
487
- peerStore,
488
- servePeerStore,
489
- serveRpc
490
- });
491
- //# sourceMappingURL=index.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/channel.ts","../src/rpc.ts","../src/peer-store.ts","../src/serve.ts","../src/webrtc.ts"],"sourcesContent":["/**\n * **@noy-db/by-peer** — WebRTC peer-to-peer transport for noy-db.\n *\n * First member of the `by-*` family of session-share transports.\n * Previously published as `@noy-db/p2p`.\n *\n * Ships three pieces:\n *\n * 1. **`peerStore()`** — a `NoydbStore` that RPCs every operation to a\n * remote peer. Slots into `NoydbOptions.sync` as a `SyncTarget`\n * with `role: 'sync-peer'` — the sync engine treats it exactly like\n * any other store.\n *\n * 2. **`servePeerStore()`** — the remote-side listener that funnels\n * incoming RPCs into the local store.\n *\n * 3. **`createOffer()` / `acceptOffer()`** — thin WebRTC handshake\n * helpers. Signaling is out of scope — the caller ferries SDP\n * blobs over QR codes, Matrix rooms, pastebins, or whatever.\n *\n * The transport is abstract: anything that implements `PeerChannel`\n * (reliable, in-order, string messages) works. `pairInMemory()` is the\n * test/dev helper that wires two channels together in one process.\n *\n * ## Threat model\n *\n * - Peer-to-peer DTLS protects the wire.\n * - noy-db already encrypts at rest — the remote peer, any TURN relay,\n * and any on-path observer see only E2E ciphertext envelopes.\n * - The opt-in `allow` whitelist on `servePeerStore` enables read-only\n * or append-only peers (e.g. `allow = ['get', 'list', 'loadAll']`).\n *\n * @packageDocumentation\n */\n\nexport type { PeerChannel } from './channel.js'\nexport { pairInMemory, fromDataChannel } from './channel.js'\nexport type { RpcMessage, RpcRequest, RpcResponse, RpcHandler, RpcClientOptions } from './rpc.js'\nexport { createRpcClient, serveRpc } from './rpc.js'\nexport type { PeerStoreOptions } from './peer-store.js'\nexport { peerStore } from './peer-store.js'\nexport type { ServePeerStoreOptions, MinimalLockManager } from './serve.js'\nexport { servePeerStore } from './serve.js'\nexport type { WebRTCOptions, Initiator, Responder } from './webrtc.js'\nexport { createOffer, acceptOffer } from './webrtc.js'\n","/**\n * `PeerChannel` — the minimal duplex message primitive used by the p2p\n * NoydbStore wrapper. Any transport that can deliver UTF-8 strings\n * reliably and in-order qualifies: WebRTC DataChannel, BroadcastChannel,\n * MessagePort, WebSocket, even postMessage pairs.\n *\n * Keeping the transport abstract has three payoffs:\n *\n * 1. **Tests run without a WebRTC polyfill.** `pairInMemory()` returns\n * two wired channels for conformance tests against `to-memory`.\n * 2. **Consumers pick their signaling story.** Matrix rooms, QR codes,\n * pastebin, Firebase Realtime DB — the handshake is out of scope.\n * 3. **Future transports slot in cheaply.** WebTransport (HTTP/3),\n * libp2p, Iroh, or a plain relay WebSocket become additional\n * bindings without touching the RPC layer.\n *\n * @module\n */\n\n/**\n * Minimal duplex message primitive.\n *\n * Implementations MUST deliver every `send` payload in order exactly\n * once to every live `on('message')` subscriber. `close()` is best-effort\n * — once called, further `send()` calls MAY throw and `on('close')` MUST\n * fire once.\n */\nexport interface PeerChannel {\n /** Enqueue a payload for delivery to the remote end. */\n send(payload: string): void\n /** Subscribe to incoming payloads or lifecycle events. Returns unsubscribe. */\n on(event: 'message', listener: (payload: string) => void): () => void\n on(event: 'close', listener: () => void): () => void\n /** Close the channel. Idempotent. */\n close(): void\n /** True once the channel is ready for `send`. */\n readonly isOpen: boolean\n}\n\n/**\n * Create a pair of in-memory `PeerChannel`s wired to each other.\n * Intended for tests and multi-tab simulations inside a single process.\n */\nexport function pairInMemory(): [PeerChannel, PeerChannel] {\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n\n function make(): { ch: PeerChannel; listeners: Listeners; closed: { v: boolean } } {\n const listeners: Listeners = { message: new Set(), close: new Set() }\n const closed = { v: false }\n const ch: PeerChannel = {\n get isOpen() {\n return !closed.v\n },\n send() {\n // Placeholder — wired below.\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed.v) return\n closed.v = true\n for (const fn of listeners.close) fn()\n },\n } as PeerChannel\n return { ch, listeners, closed }\n }\n\n const a = make()\n const b = make()\n\n function closeBoth(): void {\n if (!a.closed.v) {\n a.closed.v = true\n for (const fn of a.listeners.close) fn()\n }\n if (!b.closed.v) {\n b.closed.v = true\n for (const fn of b.listeners.close) fn()\n }\n }\n\n a.ch.send = (payload) => {\n if (b.closed.v) throw new Error('PeerChannel closed')\n queueMicrotask(() => {\n for (const fn of b.listeners.message) fn(payload)\n })\n }\n b.ch.send = (payload) => {\n if (a.closed.v) throw new Error('PeerChannel closed')\n queueMicrotask(() => {\n for (const fn of a.listeners.message) fn(payload)\n })\n }\n a.ch.close = closeBoth\n b.ch.close = closeBoth\n\n return [a.ch, b.ch]\n}\n\n/**\n * Wrap a WebRTC `RTCDataChannel` as a `PeerChannel`.\n *\n * Browser-only — the caller is responsible for establishing the\n * `RTCPeerConnection`, exchanging SDP offers/answers out of band, and\n * passing the opened DataChannel here. When the remote peer is only\n * reachable via TURN, the relay sees DTLS-wrapped ciphertext (noy-db\n * already encrypts at rest, so even a TURN compromise leaks nothing).\n */\nexport function fromDataChannel(dc: RTCDataChannel): PeerChannel {\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n dc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n dc.addEventListener('close', () => {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n })\n\n return {\n get isOpen() {\n return !closed && dc.readyState === 'open'\n },\n send(payload) {\n if (closed || dc.readyState !== 'open') {\n throw new Error(`PeerChannel not open (readyState: ${dc.readyState})`)\n }\n dc.send(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n closed = true\n try {\n dc.close()\n } catch {\n // ignore — channel already torn down\n }\n for (const fn of listeners.close) fn()\n },\n } as PeerChannel\n}\n","/**\n * JSON-RPC protocol over a `PeerChannel`.\n *\n * Request shape:\n * `{ t: 'req', id, method, args }`\n * Response shape (success):\n * `{ t: 'res', id, ok: true, result }`\n * Response shape (error):\n * `{ t: 'res', id, ok: false, error: { name, message, version? } }`\n *\n * Why not reuse msgpack/protobuf? The payloads are already base64-encoded\n * ciphertext — further binary packing saves ~8-12% at a large dependency\n * cost. JSON over UTF-8 is inspectable, fits the zero-dependency ethos,\n * and WebRTC DataChannel string mode already frames for us.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\n\n/** Wire format discriminator for RPC messages. */\nexport type RpcMessage = RpcRequest | RpcResponse\n\nexport interface RpcRequest {\n readonly t: 'req'\n readonly id: string\n readonly method: string\n readonly args: readonly unknown[]\n}\n\nexport interface RpcResponse {\n readonly t: 'res'\n readonly id: string\n readonly ok: boolean\n readonly result?: unknown\n readonly error?: { name: string; message: string; version?: number }\n}\n\n/** Handler invoked when an RPC request arrives. Return value is serialized as `result`. */\nexport type RpcHandler = (method: string, args: readonly unknown[]) => Promise<unknown>\n\n/** Options for a client-side RPC caller. */\nexport interface RpcClientOptions {\n /** Max milliseconds to wait for a response before rejecting. */\n timeoutMs?: number\n}\n\n/** Client: wrap a `PeerChannel` in a `call(method, args)` helper. */\nexport function createRpcClient(channel: PeerChannel, opts: RpcClientOptions = {}) {\n const timeoutMs = opts.timeoutMs ?? 30_000\n type Pending = {\n resolve: (v: unknown) => void\n reject: (err: Error) => void\n timer: ReturnType<typeof setTimeout>\n }\n const pending = new Map<string, Pending>()\n let counter = 0\n\n const offMessage = channel.on('message', (payload) => {\n let msg: RpcMessage\n try {\n msg = JSON.parse(payload) as RpcMessage\n } catch {\n return\n }\n if (msg.t !== 'res') return\n const entry = pending.get(msg.id)\n if (!entry) return\n clearTimeout(entry.timer)\n pending.delete(msg.id)\n if (msg.ok) {\n entry.resolve(msg.result)\n } else {\n const e = msg.error ?? { name: 'Error', message: 'unknown remote error' }\n const err = new Error(e.message)\n err.name = e.name\n if (typeof e.version === 'number') {\n ;(err as Error & { version?: number }).version = e.version\n }\n entry.reject(err)\n }\n })\n\n const offClose = channel.on('close', () => {\n for (const [, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new Error('PeerChannel closed before response'))\n }\n pending.clear()\n })\n\n return {\n async call<T = unknown>(method: string, args: readonly unknown[]): Promise<T> {\n const id = `${Date.now().toString(36)}-${(++counter).toString(36)}`\n const req: RpcRequest = { t: 'req', id, method, args }\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id)\n reject(new Error(`RPC ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n })\n try {\n channel.send(JSON.stringify(req))\n } catch (err) {\n clearTimeout(timer)\n pending.delete(id)\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n })\n },\n dispose() {\n offMessage()\n offClose()\n for (const [, entry] of pending) {\n clearTimeout(entry.timer)\n entry.reject(new Error('RPC client disposed'))\n }\n pending.clear()\n },\n }\n}\n\n/** Server: dispatch incoming RPC requests through a handler. Returns a dispose fn. */\nexport function serveRpc(channel: PeerChannel, handler: RpcHandler): () => void {\n async function handle(payload: string): Promise<void> {\n let msg: RpcMessage\n try {\n msg = JSON.parse(payload) as RpcMessage\n } catch {\n return\n }\n if (msg.t !== 'req') return\n\n let response: RpcResponse\n try {\n const result = await handler(msg.method, msg.args)\n response = { t: 'res', id: msg.id, ok: true, result }\n } catch (err) {\n const e = err as Error & { version?: number }\n response = {\n t: 'res',\n id: msg.id,\n ok: false,\n error: {\n name: e.name ?? 'Error',\n message: e.message ?? String(err),\n ...(typeof e.version === 'number' && { version: e.version }),\n },\n }\n }\n\n if (!channel.isOpen) return\n try {\n channel.send(JSON.stringify(response))\n } catch {\n // Channel closed mid-response — nothing to do.\n }\n }\n\n const offMessage = channel.on('message', (payload) => {\n void handle(payload)\n })\n\n return () => offMessage()\n}\n","/**\n * `peerStore()` — a `NoydbStore` backed by RPC calls over a `PeerChannel`.\n *\n * The local peer calls `get`/`put`/`delete`/… against this store as if\n * it were any other backend; every call is serialized as an RPC request\n * to the remote peer, which runs `servePeerStore()` to funnel the RPCs\n * into its own local `NoydbStore`.\n *\n * Error re-hydration: the remote handler re-throws `ConflictError` with\n * a `.version` field when a CAS check fails. The RPC layer carries\n * `version` in the error envelope so the local caller can catch\n * `ConflictError` with the same semantics as a direct store call.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport type { PeerChannel } from './channel.js'\nimport { createRpcClient } from './rpc.js'\n\nexport interface PeerStoreOptions {\n /** The duplex channel to the remote peer. */\n readonly channel: PeerChannel\n /** Max ms to wait for any single RPC response. Default 30s. */\n readonly timeoutMs?: number\n /** Optional display name used in diagnostics. Default `'by-peer'`. */\n readonly name?: string\n}\n\n/**\n * Create a `NoydbStore` that forwards every operation to a remote peer\n * over the supplied `PeerChannel`. The remote peer must be running\n * `servePeerStore()` against its own local store.\n */\nexport function peerStore(opts: PeerStoreOptions): NoydbStore & { dispose: () => void } {\n const rpc = createRpcClient(opts.channel, { timeoutMs: opts.timeoutMs ?? 30_000 })\n\n async function call<T>(method: string, args: readonly unknown[]): Promise<T> {\n try {\n return await rpc.call<T>(method, args)\n } catch (err) {\n // Re-hydrate ConflictError so CAS semantics survive the wire hop.\n const e = err as Error & { version?: number }\n if (e.name === 'ConflictError' && typeof e.version === 'number') {\n throw new ConflictError(e.version, e.message)\n }\n throw err\n }\n }\n\n return {\n name: opts.name ?? 'by-peer',\n\n async get(vault, collection, id) {\n return call<EncryptedEnvelope | null>('get', [vault, collection, id])\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n await call<void>('put', [vault, collection, id, envelope, expectedVersion])\n },\n\n async delete(vault, collection, id) {\n await call<void>('delete', [vault, collection, id])\n },\n\n async list(vault, collection) {\n return call<string[]>('list', [vault, collection])\n },\n\n async loadAll(vault) {\n return call<VaultSnapshot>('loadAll', [vault])\n },\n\n async saveAll(vault, data) {\n await call<void>('saveAll', [vault, data])\n },\n\n async ping() {\n try {\n return await call<boolean>('ping', [])\n } catch {\n return false\n }\n },\n\n dispose() {\n rpc.dispose()\n },\n }\n}\n","/**\n * `servePeerStore()` — runs on the peer that owns the data. Listens on\n * a `PeerChannel` for RPC requests from a remote `peerStore()` client\n * and executes each one against the local `NoydbStore`.\n *\n * The 6 core methods plus the optional `ping` / `listSince` / `listPage`\n * extensions are exposed. Unknown methods surface as a remote Error.\n *\n * **Leader-election (issue #3).** When a `BroadcastChannel`-backed\n * `PeerChannel` is shared by 3+ tabs each running `servePeerStore`,\n * every non-sending tab responds to every RPC — producing duplicate\n * responses for the same request id and O(N²) channel traffic at scale.\n * Pass `leaderElection: { lockName, locks? }` to gate serving on the\n * Web Locks API: only the lock-holding tab registers an RPC handler;\n * others queue and take over when the holder's tab closes (lock auto-\n * releases). Default behaviour (no `leaderElection`) is unchanged for\n * the 2-tab case.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport type { PeerChannel } from './channel.js'\nimport { serveRpc } from './rpc.js'\n\n/**\n * Minimal subset of the Web Locks API used by leader-election. The\n * browser's `navigator.locks` satisfies this; tests inject a stub.\n *\n * @public\n */\nexport interface MinimalLockManager {\n request<T>(\n name: string,\n options: { mode?: 'exclusive' | 'shared'; signal?: AbortSignal },\n callback: (lock: unknown) => Promise<T>,\n ): Promise<T>\n}\n\nexport interface ServePeerStoreOptions {\n /** The duplex channel from the remote peer. */\n readonly channel: PeerChannel\n /** The local store to serve. */\n readonly store: NoydbStore\n /**\n * Optional method whitelist. When provided, any method not in the set\n * is rejected with \"method not allowed\". Useful for read-only peers.\n */\n readonly allow?: ReadonlySet<string>\n /**\n * Opt in to leader-election semantics for cross-tab coordination via\n * the Web Locks API. Required when 3+ tabs share a `BroadcastChannel`-\n * backed `PeerChannel` — without leader election, every non-sending tab\n * responds to every RPC, producing duplicate responses (issue #3).\n *\n * Browser support: Chrome 69+, Firefox 96+, Safari 15.4+.\n *\n * - `lockName` — globally unique lock name. Convention:\n * `noy-db:peer-server:<channel-name>`. The same `lockName` MUST be\n * used by every tab that wants to share the leader role.\n * - `locks` — defaults to `navigator.locks` in browsers. Pass a stub\n * for tests or for non-browser hosts that polyfill the Web Locks API.\n *\n * Behaviour: at most one tab holds the lock at a time. Only that tab\n * registers an RPC handler on the channel; other tabs queue (their\n * `servePeerStore` call returns a working dispose function but does\n * not respond to incoming RPCs). When the holder's tab closes (or its\n * `servePeerStore` is disposed), the next queued tab takes over.\n */\n readonly leaderElection?: {\n readonly lockName: string\n readonly locks?: MinimalLockManager\n }\n}\n\nconst CORE_METHODS = new Set<string>([\n 'get',\n 'put',\n 'delete',\n 'list',\n 'loadAll',\n 'saveAll',\n 'ping',\n 'listSince',\n 'listPage',\n 'listVaults',\n])\n\n/**\n * Start serving the local store on the channel. Returns a dispose\n * function that stops the RPC listener. The underlying channel is NOT\n * closed by dispose — ownership stays with the caller.\n *\n * When `leaderElection` is set, the RPC handler is only registered while\n * this tab holds the named Web Lock. The returned dispose function\n * always works: if called before the lock is acquired, it cancels the\n * wait; if called while holding the lock, it releases the lock and\n * stops serving.\n */\nexport function servePeerStore(opts: ServePeerStoreOptions): () => void {\n if (!opts.leaderElection) {\n return startServing(opts)\n }\n return startServingWithLeaderElection(opts)\n}\n\n/**\n * Register the RPC handler immediately. Used when leader-election is off,\n * and inside the lock callback when on.\n */\nfunction startServing(opts: ServePeerStoreOptions): () => void {\n const { store, channel, allow } = opts\n\n return serveRpc(channel, async (method, args) => {\n if (!CORE_METHODS.has(method)) {\n throw new Error(`Unknown RPC method: ${method}`)\n }\n if (allow && !allow.has(method)) {\n throw new Error(`Method not allowed: ${method}`)\n }\n\n switch (method) {\n case 'get': {\n const [vault, collection, id] = args as [string, string, string]\n return store.get(vault, collection, id)\n }\n case 'put': {\n const [vault, collection, id, envelope, expectedVersion] = args as [\n string,\n string,\n string,\n EncryptedEnvelope,\n number | undefined,\n ]\n await store.put(vault, collection, id, envelope, expectedVersion)\n return null\n }\n case 'delete': {\n const [vault, collection, id] = args as [string, string, string]\n await store.delete(vault, collection, id)\n return null\n }\n case 'list': {\n const [vault, collection] = args as [string, string]\n return store.list(vault, collection)\n }\n case 'loadAll': {\n const [vault] = args as [string]\n return store.loadAll(vault)\n }\n case 'saveAll': {\n const [vault, data] = args as [string, VaultSnapshot]\n await store.saveAll(vault, data)\n return null\n }\n case 'ping': {\n if (!store.ping) return true\n return store.ping()\n }\n case 'listSince': {\n if (!store.listSince) throw new Error('listSince not supported by remote store')\n const [vault, collection, since] = args as [string, string, string]\n return store.listSince(vault, collection, since)\n }\n case 'listPage': {\n if (!store.listPage) throw new Error('listPage not supported by remote store')\n const [vault, collection, cursor, limit] = args as [\n string,\n string,\n string | undefined,\n number | undefined,\n ]\n return store.listPage(vault, collection, cursor, limit)\n }\n case 'listVaults': {\n if (!store.listVaults) throw new Error('listVaults not supported by remote store')\n return store.listVaults()\n }\n }\n /* istanbul ignore next — CORE_METHODS gate makes this unreachable */\n throw new Error(`Unhandled method: ${method}`)\n })\n}\n\n/**\n * Acquire a Web Lock first; only register the RPC listener while\n * holding it. Returns a dispose that cancels the wait (if not yet\n * acquired) or releases the lock (if held).\n */\nfunction startServingWithLeaderElection(opts: ServePeerStoreOptions): () => void {\n // Asserted by the caller branch in `servePeerStore`.\n const leader = opts.leaderElection as { lockName: string; locks?: MinimalLockManager }\n const locks = leader.locks ?? getDefaultLocks()\n const ac = new AbortController()\n let stopServing: (() => void) | null = null\n let releaseLock: (() => void) | null = null\n let disposed = false\n\n void locks\n .request(leader.lockName, { mode: 'exclusive', signal: ac.signal }, () => {\n if (disposed) return Promise.resolve()\n stopServing = startServing(opts)\n // Hold the lock for the lifetime of this tab's leadership. The\n // returned promise stays pending until `releaseLock` is called by\n // the dispose function.\n return new Promise<void>((resolve) => {\n releaseLock = resolve\n })\n })\n .catch((err: unknown) => {\n // AbortError is the expected cancellation when dispose runs before\n // the lock was acquired. Anything else is unexpected — surface it\n // at the next tick so the host can observe it.\n if ((err as { name?: string } | null)?.name === 'AbortError') return\n queueMicrotask(() => {\n throw err\n })\n })\n\n return () => {\n if (disposed) return\n disposed = true\n if (stopServing) {\n stopServing()\n stopServing = null\n }\n if (releaseLock) {\n releaseLock()\n releaseLock = null\n } else {\n ac.abort()\n }\n }\n}\n\nfunction getDefaultLocks(): MinimalLockManager {\n const nav = (globalThis as { navigator?: { locks?: MinimalLockManager } }).navigator\n if (nav && nav.locks) return nav.locks\n throw new Error(\n 'leaderElection requires the Web Locks API (navigator.locks). ' +\n 'Use Chrome 69+, Firefox 96+, Safari 15.4+, or pass `leaderElection.locks: <stub>` ' +\n 'for tests / non-browser hosts.',\n )\n}\n","/**\n * WebRTC handshake helper — opinionated wrapper around\n * `RTCPeerConnection` that produces a ready-to-use `PeerChannel`.\n *\n * The handshake is split into two halves so the caller can ferry the\n * SDP blobs over whatever signaling channel they prefer (QR code,\n * Matrix room, pastebin, Firebase, signed URL…). Signaling is\n * intentionally out of scope — noy-db has no opinion on how peers\n * discover each other, only on what flows once they do.\n *\n * ```ts\n * // Peer A (initiator)\n * const a = await createOffer({ iceServers })\n * send(a.offer) // → signaling channel\n * const answer = await receive() // ← signaling channel\n * await a.accept(answer)\n * const channel = await a.channel // ready PeerChannel\n *\n * // Peer B (responder)\n * const offer = await receive() // ← signaling channel\n * const b = await acceptOffer(offer, { iceServers })\n * send(b.answer) // → signaling channel\n * const channel = await b.channel // ready PeerChannel\n * ```\n *\n * Browser-only. Node consumers who want to interconnect with browsers\n * can plug `@roamhq/wrtc` into the global `RTCPeerConnection` slot; this\n * module does not pull it in so the package has zero runtime deps.\n *\n * @module\n */\n\nimport type { PeerChannel } from './channel.js'\nimport { fromDataChannel } from './channel.js'\n\ntype PeerConnection = RTCPeerConnection\ntype SessionDescription = RTCSessionDescriptionInit\n\nexport interface WebRTCOptions {\n /** Optional ICE servers (STUN / TURN). */\n readonly iceServers?: RTCIceServer[]\n /** Label for the `RTCDataChannel`. Default `'noydb'`. */\n readonly label?: string\n}\n\nexport interface Initiator {\n readonly offer: SessionDescription\n /** Feed in the remote peer's SDP answer to complete the handshake. */\n accept(answer: SessionDescription): Promise<void>\n /** Resolves with the opened `PeerChannel` once the DataChannel is live. */\n readonly channel: Promise<PeerChannel>\n readonly connection: PeerConnection\n}\n\nexport interface Responder {\n readonly answer: SessionDescription\n /** Resolves with the opened `PeerChannel` once the DataChannel is live. */\n readonly channel: Promise<PeerChannel>\n readonly connection: PeerConnection\n}\n\nfunction requireRTC(): typeof RTCPeerConnection {\n const g = globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }\n if (!g.RTCPeerConnection) {\n throw new Error(\n '[@noy-db/by-peer] globalThis.RTCPeerConnection is undefined — use this module in a browser, or polyfill with @roamhq/wrtc in Node',\n )\n }\n return g.RTCPeerConnection\n}\n\n/**\n * Build an offer as the initiating peer. Returns the SDP offer to send\n * to the remote peer and a promise for the opened `PeerChannel`.\n */\nexport async function createOffer(opts: WebRTCOptions = {}): Promise<Initiator> {\n const RTC = requireRTC()\n const pc = new RTC({\n ...(opts.iceServers && { iceServers: opts.iceServers }),\n })\n\n const dc = pc.createDataChannel(opts.label ?? 'noydb', {\n ordered: true,\n })\n\n const channelPromise = new Promise<PeerChannel>((resolve, reject) => {\n dc.addEventListener('open', () => resolve(fromDataChannel(dc)))\n dc.addEventListener('error', () => reject(new Error('DataChannel error')))\n })\n\n const offer = await pc.createOffer()\n await pc.setLocalDescription(offer)\n await waitForIceComplete(pc)\n\n return {\n offer: pc.localDescription ?? offer,\n connection: pc,\n channel: channelPromise,\n async accept(answer) {\n await pc.setRemoteDescription(answer)\n },\n }\n}\n\n/**\n * Accept an incoming offer as the responding peer. Returns the SDP\n * answer to send back to the initiator and a promise for the opened\n * `PeerChannel`.\n */\nexport async function acceptOffer(\n offer: SessionDescription,\n opts: WebRTCOptions = {},\n): Promise<Responder> {\n const RTC = requireRTC()\n const pc = new RTC({\n ...(opts.iceServers && { iceServers: opts.iceServers }),\n })\n\n const channelPromise = new Promise<PeerChannel>((resolve, reject) => {\n pc.addEventListener('datachannel', (ev) => {\n const dc = ev.channel\n if (dc.readyState === 'open') {\n resolve(fromDataChannel(dc))\n } else {\n dc.addEventListener('open', () => resolve(fromDataChannel(dc)))\n dc.addEventListener('error', () => reject(new Error('DataChannel error')))\n }\n })\n })\n\n await pc.setRemoteDescription(offer)\n const answer = await pc.createAnswer()\n await pc.setLocalDescription(answer)\n await waitForIceComplete(pc)\n\n return {\n answer: pc.localDescription ?? answer,\n connection: pc,\n channel: channelPromise,\n }\n}\n\n/**\n * Resolve once ICE gathering reaches `'complete'`. Using non-trickle ICE\n * keeps the signaling exchange to a single round-trip — at the cost of\n * a slightly longer initial handshake. Consumers that want trickle ICE\n * can bypass this helper and drive `RTCPeerConnection` directly.\n */\nfunction waitForIceComplete(pc: PeerConnection): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve()\n return new Promise((resolve) => {\n const check = () => {\n if (pc.iceGatheringState === 'complete') {\n pc.removeEventListener('icegatheringstatechange', check)\n resolve()\n }\n }\n pc.addEventListener('icegatheringstatechange', check)\n })\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts DELETED
@@ -1,288 +0,0 @@
1
- import { NoydbStore } from '@noy-db/hub';
2
-
3
- /**
4
- * `PeerChannel` — the minimal duplex message primitive used by the p2p
5
- * NoydbStore wrapper. Any transport that can deliver UTF-8 strings
6
- * reliably and in-order qualifies: WebRTC DataChannel, BroadcastChannel,
7
- * MessagePort, WebSocket, even postMessage pairs.
8
- *
9
- * Keeping the transport abstract has three payoffs:
10
- *
11
- * 1. **Tests run without a WebRTC polyfill.** `pairInMemory()` returns
12
- * two wired channels for conformance tests against `to-memory`.
13
- * 2. **Consumers pick their signaling story.** Matrix rooms, QR codes,
14
- * pastebin, Firebase Realtime DB — the handshake is out of scope.
15
- * 3. **Future transports slot in cheaply.** WebTransport (HTTP/3),
16
- * libp2p, Iroh, or a plain relay WebSocket become additional
17
- * bindings without touching the RPC layer.
18
- *
19
- * @module
20
- */
21
- /**
22
- * Minimal duplex message primitive.
23
- *
24
- * Implementations MUST deliver every `send` payload in order exactly
25
- * once to every live `on('message')` subscriber. `close()` is best-effort
26
- * — once called, further `send()` calls MAY throw and `on('close')` MUST
27
- * fire once.
28
- */
29
- interface PeerChannel {
30
- /** Enqueue a payload for delivery to the remote end. */
31
- send(payload: string): void;
32
- /** Subscribe to incoming payloads or lifecycle events. Returns unsubscribe. */
33
- on(event: 'message', listener: (payload: string) => void): () => void;
34
- on(event: 'close', listener: () => void): () => void;
35
- /** Close the channel. Idempotent. */
36
- close(): void;
37
- /** True once the channel is ready for `send`. */
38
- readonly isOpen: boolean;
39
- }
40
- /**
41
- * Create a pair of in-memory `PeerChannel`s wired to each other.
42
- * Intended for tests and multi-tab simulations inside a single process.
43
- */
44
- declare function pairInMemory(): [PeerChannel, PeerChannel];
45
- /**
46
- * Wrap a WebRTC `RTCDataChannel` as a `PeerChannel`.
47
- *
48
- * Browser-only — the caller is responsible for establishing the
49
- * `RTCPeerConnection`, exchanging SDP offers/answers out of band, and
50
- * passing the opened DataChannel here. When the remote peer is only
51
- * reachable via TURN, the relay sees DTLS-wrapped ciphertext (noy-db
52
- * already encrypts at rest, so even a TURN compromise leaks nothing).
53
- */
54
- declare function fromDataChannel(dc: RTCDataChannel): PeerChannel;
55
-
56
- /**
57
- * JSON-RPC protocol over a `PeerChannel`.
58
- *
59
- * Request shape:
60
- * `{ t: 'req', id, method, args }`
61
- * Response shape (success):
62
- * `{ t: 'res', id, ok: true, result }`
63
- * Response shape (error):
64
- * `{ t: 'res', id, ok: false, error: { name, message, version? } }`
65
- *
66
- * Why not reuse msgpack/protobuf? The payloads are already base64-encoded
67
- * ciphertext — further binary packing saves ~8-12% at a large dependency
68
- * cost. JSON over UTF-8 is inspectable, fits the zero-dependency ethos,
69
- * and WebRTC DataChannel string mode already frames for us.
70
- *
71
- * @module
72
- */
73
-
74
- /** Wire format discriminator for RPC messages. */
75
- type RpcMessage = RpcRequest | RpcResponse;
76
- interface RpcRequest {
77
- readonly t: 'req';
78
- readonly id: string;
79
- readonly method: string;
80
- readonly args: readonly unknown[];
81
- }
82
- interface RpcResponse {
83
- readonly t: 'res';
84
- readonly id: string;
85
- readonly ok: boolean;
86
- readonly result?: unknown;
87
- readonly error?: {
88
- name: string;
89
- message: string;
90
- version?: number;
91
- };
92
- }
93
- /** Handler invoked when an RPC request arrives. Return value is serialized as `result`. */
94
- type RpcHandler = (method: string, args: readonly unknown[]) => Promise<unknown>;
95
- /** Options for a client-side RPC caller. */
96
- interface RpcClientOptions {
97
- /** Max milliseconds to wait for a response before rejecting. */
98
- timeoutMs?: number;
99
- }
100
- /** Client: wrap a `PeerChannel` in a `call(method, args)` helper. */
101
- declare function createRpcClient(channel: PeerChannel, opts?: RpcClientOptions): {
102
- call<T = unknown>(method: string, args: readonly unknown[]): Promise<T>;
103
- dispose(): void;
104
- };
105
- /** Server: dispatch incoming RPC requests through a handler. Returns a dispose fn. */
106
- declare function serveRpc(channel: PeerChannel, handler: RpcHandler): () => void;
107
-
108
- /**
109
- * `peerStore()` — a `NoydbStore` backed by RPC calls over a `PeerChannel`.
110
- *
111
- * The local peer calls `get`/`put`/`delete`/… against this store as if
112
- * it were any other backend; every call is serialized as an RPC request
113
- * to the remote peer, which runs `servePeerStore()` to funnel the RPCs
114
- * into its own local `NoydbStore`.
115
- *
116
- * Error re-hydration: the remote handler re-throws `ConflictError` with
117
- * a `.version` field when a CAS check fails. The RPC layer carries
118
- * `version` in the error envelope so the local caller can catch
119
- * `ConflictError` with the same semantics as a direct store call.
120
- *
121
- * @module
122
- */
123
-
124
- interface PeerStoreOptions {
125
- /** The duplex channel to the remote peer. */
126
- readonly channel: PeerChannel;
127
- /** Max ms to wait for any single RPC response. Default 30s. */
128
- readonly timeoutMs?: number;
129
- /** Optional display name used in diagnostics. Default `'by-peer'`. */
130
- readonly name?: string;
131
- }
132
- /**
133
- * Create a `NoydbStore` that forwards every operation to a remote peer
134
- * over the supplied `PeerChannel`. The remote peer must be running
135
- * `servePeerStore()` against its own local store.
136
- */
137
- declare function peerStore(opts: PeerStoreOptions): NoydbStore & {
138
- dispose: () => void;
139
- };
140
-
141
- /**
142
- * `servePeerStore()` — runs on the peer that owns the data. Listens on
143
- * a `PeerChannel` for RPC requests from a remote `peerStore()` client
144
- * and executes each one against the local `NoydbStore`.
145
- *
146
- * The 6 core methods plus the optional `ping` / `listSince` / `listPage`
147
- * extensions are exposed. Unknown methods surface as a remote Error.
148
- *
149
- * **Leader-election (issue #3).** When a `BroadcastChannel`-backed
150
- * `PeerChannel` is shared by 3+ tabs each running `servePeerStore`,
151
- * every non-sending tab responds to every RPC — producing duplicate
152
- * responses for the same request id and O(N²) channel traffic at scale.
153
- * Pass `leaderElection: { lockName, locks? }` to gate serving on the
154
- * Web Locks API: only the lock-holding tab registers an RPC handler;
155
- * others queue and take over when the holder's tab closes (lock auto-
156
- * releases). Default behaviour (no `leaderElection`) is unchanged for
157
- * the 2-tab case.
158
- *
159
- * @module
160
- */
161
-
162
- /**
163
- * Minimal subset of the Web Locks API used by leader-election. The
164
- * browser's `navigator.locks` satisfies this; tests inject a stub.
165
- *
166
- * @public
167
- */
168
- interface MinimalLockManager {
169
- request<T>(name: string, options: {
170
- mode?: 'exclusive' | 'shared';
171
- signal?: AbortSignal;
172
- }, callback: (lock: unknown) => Promise<T>): Promise<T>;
173
- }
174
- interface ServePeerStoreOptions {
175
- /** The duplex channel from the remote peer. */
176
- readonly channel: PeerChannel;
177
- /** The local store to serve. */
178
- readonly store: NoydbStore;
179
- /**
180
- * Optional method whitelist. When provided, any method not in the set
181
- * is rejected with "method not allowed". Useful for read-only peers.
182
- */
183
- readonly allow?: ReadonlySet<string>;
184
- /**
185
- * Opt in to leader-election semantics for cross-tab coordination via
186
- * the Web Locks API. Required when 3+ tabs share a `BroadcastChannel`-
187
- * backed `PeerChannel` — without leader election, every non-sending tab
188
- * responds to every RPC, producing duplicate responses (issue #3).
189
- *
190
- * Browser support: Chrome 69+, Firefox 96+, Safari 15.4+.
191
- *
192
- * - `lockName` — globally unique lock name. Convention:
193
- * `noy-db:peer-server:<channel-name>`. The same `lockName` MUST be
194
- * used by every tab that wants to share the leader role.
195
- * - `locks` — defaults to `navigator.locks` in browsers. Pass a stub
196
- * for tests or for non-browser hosts that polyfill the Web Locks API.
197
- *
198
- * Behaviour: at most one tab holds the lock at a time. Only that tab
199
- * registers an RPC handler on the channel; other tabs queue (their
200
- * `servePeerStore` call returns a working dispose function but does
201
- * not respond to incoming RPCs). When the holder's tab closes (or its
202
- * `servePeerStore` is disposed), the next queued tab takes over.
203
- */
204
- readonly leaderElection?: {
205
- readonly lockName: string;
206
- readonly locks?: MinimalLockManager;
207
- };
208
- }
209
- /**
210
- * Start serving the local store on the channel. Returns a dispose
211
- * function that stops the RPC listener. The underlying channel is NOT
212
- * closed by dispose — ownership stays with the caller.
213
- *
214
- * When `leaderElection` is set, the RPC handler is only registered while
215
- * this tab holds the named Web Lock. The returned dispose function
216
- * always works: if called before the lock is acquired, it cancels the
217
- * wait; if called while holding the lock, it releases the lock and
218
- * stops serving.
219
- */
220
- declare function servePeerStore(opts: ServePeerStoreOptions): () => void;
221
-
222
- /**
223
- * WebRTC handshake helper — opinionated wrapper around
224
- * `RTCPeerConnection` that produces a ready-to-use `PeerChannel`.
225
- *
226
- * The handshake is split into two halves so the caller can ferry the
227
- * SDP blobs over whatever signaling channel they prefer (QR code,
228
- * Matrix room, pastebin, Firebase, signed URL…). Signaling is
229
- * intentionally out of scope — noy-db has no opinion on how peers
230
- * discover each other, only on what flows once they do.
231
- *
232
- * ```ts
233
- * // Peer A (initiator)
234
- * const a = await createOffer({ iceServers })
235
- * send(a.offer) // → signaling channel
236
- * const answer = await receive() // ← signaling channel
237
- * await a.accept(answer)
238
- * const channel = await a.channel // ready PeerChannel
239
- *
240
- * // Peer B (responder)
241
- * const offer = await receive() // ← signaling channel
242
- * const b = await acceptOffer(offer, { iceServers })
243
- * send(b.answer) // → signaling channel
244
- * const channel = await b.channel // ready PeerChannel
245
- * ```
246
- *
247
- * Browser-only. Node consumers who want to interconnect with browsers
248
- * can plug `@roamhq/wrtc` into the global `RTCPeerConnection` slot; this
249
- * module does not pull it in so the package has zero runtime deps.
250
- *
251
- * @module
252
- */
253
-
254
- type PeerConnection = RTCPeerConnection;
255
- type SessionDescription = RTCSessionDescriptionInit;
256
- interface WebRTCOptions {
257
- /** Optional ICE servers (STUN / TURN). */
258
- readonly iceServers?: RTCIceServer[];
259
- /** Label for the `RTCDataChannel`. Default `'noydb'`. */
260
- readonly label?: string;
261
- }
262
- interface Initiator {
263
- readonly offer: SessionDescription;
264
- /** Feed in the remote peer's SDP answer to complete the handshake. */
265
- accept(answer: SessionDescription): Promise<void>;
266
- /** Resolves with the opened `PeerChannel` once the DataChannel is live. */
267
- readonly channel: Promise<PeerChannel>;
268
- readonly connection: PeerConnection;
269
- }
270
- interface Responder {
271
- readonly answer: SessionDescription;
272
- /** Resolves with the opened `PeerChannel` once the DataChannel is live. */
273
- readonly channel: Promise<PeerChannel>;
274
- readonly connection: PeerConnection;
275
- }
276
- /**
277
- * Build an offer as the initiating peer. Returns the SDP offer to send
278
- * to the remote peer and a promise for the opened `PeerChannel`.
279
- */
280
- declare function createOffer(opts?: WebRTCOptions): Promise<Initiator>;
281
- /**
282
- * Accept an incoming offer as the responding peer. Returns the SDP
283
- * answer to send back to the initiator and a promise for the opened
284
- * `PeerChannel`.
285
- */
286
- declare function acceptOffer(offer: SessionDescription, opts?: WebRTCOptions): Promise<Responder>;
287
-
288
- export { type Initiator, type MinimalLockManager, type PeerChannel, type PeerStoreOptions, type Responder, type RpcClientOptions, type RpcHandler, type RpcMessage, type RpcRequest, type RpcResponse, type ServePeerStoreOptions, type WebRTCOptions, acceptOffer, createOffer, createRpcClient, fromDataChannel, pairInMemory, peerStore, servePeerStore, serveRpc };