@vielzeug/mesh 26.10.0 → 26.10.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 +2 -2
- package/dist/_base45.cjs.map +1 -1
- package/dist/_base45.js.map +1 -1
- package/dist/_proof.cjs +1 -1
- package/dist/_proof.cjs.map +1 -1
- package/dist/_proof.d.ts +1 -1
- package/dist/_proof.js +1 -1
- package/dist/_proof.js.map +1 -1
- package/dist/_random.cjs.map +1 -1
- package/dist/_random.d.ts +1 -1
- package/dist/_random.d.ts.map +1 -1
- package/dist/_random.js.map +1 -1
- package/dist/_rtc.cjs +1 -1
- package/dist/_rtc.cjs.map +1 -1
- package/dist/_rtc.d.ts +3 -3
- package/dist/_rtc.js +1 -1
- package/dist/_rtc.js.map +1 -1
- package/dist/_sdp.cjs.map +1 -1
- package/dist/_sdp.d.ts +1 -1
- package/dist/_sdp.js.map +1 -1
- package/dist/guest.cjs.map +1 -1
- package/dist/guest.js.map +1 -1
- package/dist/mesh.cjs +1 -1
- package/dist/mesh.cjs.map +1 -1
- package/dist/mesh.iife.js +1 -1
- package/dist/mesh.iife.js.map +1 -1
- package/dist/mesh.js +1 -1
- package/dist/mesh.js.map +1 -1
- package/dist/qr-codec.cjs.map +1 -1
- package/dist/qr-codec.js.map +1 -1
- package/dist/testing/fake-rtc.cjs.map +1 -1
- package/dist/testing/fake-rtc.d.ts +1 -1
- package/dist/testing/fake-rtc.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/mesh.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mesh.js","names":[],"sources":["../src/errors.ts","../src/codec.ts","../src/_defaults.ts","../src/_random.ts","../src/_wire.ts","../src/_node.ts","../src/_sdp.ts","../src/_proof.ts","../src/_rtc.ts","../src/guest.ts","../src/host.ts","../src/_base45.ts","../src/qr-codec.ts"],"sourcesContent":["/**\n * Base class for all mesh errors.\n * Use `instanceof MeshError` to catch any mesh-originated error in one branch.\n */\nexport class MeshError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Thrown when a pairing payload is malformed, expired, unknown, already\n * consumed, carries a mismatched proof, or is rejected by the host's\n * `approvePeer` hook.\n */\nexport class MeshPairingError extends MeshError {}\n\n/**\n * Thrown when the peer connection fails (ICE/DTLS failure, channel closed\n * unexpectedly) or a send targets an unknown or unconnected peer.\n */\nexport class MeshConnectionError extends MeshError {\n readonly peerId: string | null;\n\n constructor(message: string, peerId: string | null = null, opts?: ErrorOptions) {\n super(message, opts);\n this.peerId = peerId;\n }\n}\n\n/**\n * Thrown when an outbound message exceeds `maxMessageBytes` or cannot be\n * serialized.\n */\nexport class MeshPayloadError extends MeshError {}\n\n/**\n * Thrown when ICE gathering or the data-channel open handshake exceeds the\n * configured timeout.\n */\nexport class MeshTimeoutError extends MeshError {}\n\n/**\n * Thrown when a method is called on a disposed mesh node.\n */\nexport class MeshDisposedError extends MeshError {\n constructor(target = 'Mesh node', opts?: ErrorOptions) {\n super(`${target} is disposed`, opts);\n }\n}\n\n/**\n * Thrown when WebRTC is unavailable in the current environment and no `rtc`\n * factory was injected. Raised at first use, never at import time.\n */\nexport class MeshUnsupportedError extends MeshError {}\n","import { base64UrlToText, textToBase64Url } from '@vielzeug/arsenal';\nimport { MeshPairingError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\n/** Structural guard shared by the plain and QR pairing codecs. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isInvitation(value: Record<string, unknown>): value is Record<string, unknown> & MeshInvitation {\n return (\n value.v === 1 &&\n typeof value.sessionId === 'string' &&\n typeof value.secret === 'string' &&\n typeof value.expiresAt === 'number' &&\n typeof value.sdp === 'string'\n );\n}\n\nfunction isAnswer(value: Record<string, unknown>): value is Record<string, unknown> & MeshAnswer {\n return (\n value.v === 1 &&\n typeof value.sessionId === 'string' &&\n typeof value.proof === 'string' &&\n isRecord(value.peer) &&\n typeof value.peer.id === 'string' &&\n value.peer.id !== '' &&\n (value.peer.name === undefined || typeof value.peer.name === 'string') &&\n typeof value.sdp === 'string'\n );\n}\n\n/**\n * Validate a decoded pairing payload. Shared by `meshCodec` and `meshQrCodec`\n * so both encodings enforce the same shape/version contract.\n *\n * @throws {MeshPairingError} On unsupported version or unrecognized shape.\n */\nexport function parsePairingPayload(parsed: unknown): MeshInvitation | MeshAnswer {\n if (!isRecord(parsed) || parsed.v !== 1) {\n throw new MeshPairingError('Unsupported pairing payload version');\n }\n if (isInvitation(parsed) || isAnswer(parsed)) {\n return parsed;\n }\n throw new MeshPairingError('Unrecognized pairing payload shape');\n}\n\n/**\n * Compact JSON → base64url codec for pairing payloads. Pure and transport-free\n * so alternative encodings (compressed, QR-optimized) can be added later\n * without touching the transport.\n *\n * @example\n * ```ts\n * const invitation = await host.createInvitation();\n * const text = meshCodec.encode(invitation); // copy/paste or navigator.share\n * const answer = await guest.acceptInvitation(meshCodec.decode(text));\n * await host.acceptAnswer(meshCodec.decode(meshCodec.encode(answer)));\n * ```\n *\n * @throws {MeshPairingError} On malformed text or unsupported payload shape/version.\n */\nexport const meshCodec = {\n decode(text: string): MeshInvitation | MeshAnswer {\n let parsed: unknown;\n try {\n parsed = JSON.parse(base64UrlToText(text.trim()));\n } catch (cause) {\n throw new MeshPairingError('Malformed pairing payload', { cause });\n }\n return parsePairingPayload(parsed);\n },\n encode(payload: MeshInvitation | MeshAnswer): string {\n return textToBase64Url(JSON.stringify(payload));\n },\n};\n","/** Option defaults shared by host and guest so the two never drift. */\nexport const DEFAULT_CHANNEL_OPEN_TIMEOUT_MS = 60_000;\nexport const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 5_000;\nexport const DEFAULT_INVITATION_TTL_MS = 300_000;\nexport const DEFAULT_MAX_MESSAGE_BYTES = 65_536;\n","import { bytesToBase64Url, type RandomSource, randomFloat } from '@vielzeug/arsenal';\n\n/**\n * Random bytes from `crypto.getRandomValues`, or from an injected\n * `RandomSource` for deterministic tests.\n */\nexport function randomBytes(length: number, source?: RandomSource): Uint8Array {\n const bytes = new Uint8Array(length);\n if (source) {\n for (let index = 0; index < length; index++) bytes[index] = Math.floor(randomFloat(source) * 256);\n } else {\n crypto.getRandomValues(bytes);\n }\n return bytes;\n}\n\n/** Random 128-bit base64url id — used for peer ids, session ids, and message ids. */\nexport function randomId(source?: RandomSource): string {\n return bytesToBase64Url(randomBytes(16, source));\n}\n","/** Wire envelope serialized through the configured `serialize` function. */\nexport interface WireEnvelope {\n readonly id: string;\n readonly p: unknown;\n readonly t: string;\n readonly ts: number;\n readonly v: 1;\n}\n\n/** Returns the envelope, or `null` when the value violates the wire shape. */\nexport function toEnvelope(value: unknown): WireEnvelope | null {\n if (typeof value !== 'object' || value === null) return null;\n const env = value as Record<string, unknown>;\n if (env.v !== 1 || typeof env.id !== 'string' || typeof env.t !== 'string' || typeof env.ts !== 'number') return null;\n return value as WireEnvelope;\n}\n\n/** Per-peer duplicate guard: remembers the last `capacity` message ids (LRU). */\nexport class MessageDedupe {\n private readonly seen = new Set<string>();\n private readonly order: string[] = [];\n private readonly capacity: number;\n\n constructor(capacity = 256) {\n this.capacity = capacity;\n }\n\n /** `true` when the id is new (and records it); `false` for duplicates. */\n check(id: string): boolean {\n if (this.seen.has(id)) return false;\n this.seen.add(id);\n this.order.push(id);\n if (this.order.length > this.capacity) this.seen.delete(this.order.shift()!);\n return true;\n }\n}\n","import { type RandomSource, tapper, utf8Bytes } from '@vielzeug/arsenal';\nimport { randomId } from './_random';\nimport { MessageDedupe, toEnvelope } from './_wire';\nimport { MeshConnectionError, MeshDisposedError, MeshError, MeshPayloadError } from './errors';\nimport type {\n MeshEvent,\n MeshInbound,\n MeshPeer,\n MeshPeerStatus,\n MeshStatus,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n Unsubscribe,\n} from './types';\n\ntype Tapper = (event: MeshEvent) => void;\n\n/** Shared lifecycle core: status, tappers, disposal. */\nexport interface NodeCore {\n readonly disposalCtrl: AbortController;\n disposed: boolean;\n emitTap(event: MeshEvent): void;\n ensureLive(): void;\n /** Emits the final status-change + dispose events, clears tappers, aborts. */\n markDisposed(): void;\n setStatus(next: MeshStatus): void;\n status: MeshStatus;\n tap(handler: Tapper, options?: { readonly signal?: AbortSignal }): Unsubscribe;\n}\n\nexport function createNodeCore(signal?: AbortSignal): NodeCore {\n const tappers = tapper<MeshEvent>();\n const disposalCtrl = new AbortController();\n\n const core: NodeCore = {\n disposalCtrl,\n disposed: false,\n\n emitTap(event) {\n tappers.emit(event);\n },\n\n ensureLive() {\n if (core.disposed) throw new MeshDisposedError();\n },\n\n markDisposed() {\n if (core.disposed) return;\n core.disposed = true;\n core.status = 'disposed';\n core.emitTap({ status: 'disposed', type: 'status-change' });\n core.emitTap({ type: 'dispose' });\n tappers.clear();\n disposalCtrl.abort();\n },\n\n setStatus(next) {\n if (core.disposed || core.status === next) return;\n core.status = next;\n core.emitTap({ status: next, type: 'status-change' });\n },\n status: 'idle',\n\n tap(handler, options) {\n if (core.disposed) return () => {};\n return tappers.tap(handler, options);\n },\n };\n\n if (signal) {\n if (signal.aborted) {\n core.markDisposed();\n } else {\n signal.addEventListener('abort', () => core.markDisposed(), { once: true });\n }\n }\n\n return core;\n}\n\n// ─── Peers ───────────────────────────────────────────────────────────────────\n\n/** Internal peer record; `publicPeer` exposes live status through getters. */\nexport interface PeerRecord {\n dc: RTCDataChannelLike | null;\n readonly dedupe: MessageDedupe;\n readonly id: string;\n readonly name: string | undefined;\n pc: RTCPeerConnectionLike | null;\n readonly publicPeer: MeshPeer;\n readonly role: 'host' | 'guest';\n status: MeshPeerStatus;\n}\n\nexport function createPeerRecord(id: string, name: string | undefined, role: 'host' | 'guest'): PeerRecord {\n const record: PeerRecord = {\n dc: null,\n dedupe: new MessageDedupe(),\n id,\n name,\n pc: null,\n publicPeer: {\n get id() {\n return record.id;\n },\n get name() {\n return record.name;\n },\n get role() {\n return record.role;\n },\n get status() {\n return record.status;\n },\n },\n role,\n status: 'connecting',\n };\n return record;\n}\n\n// ─── Messenger ───────────────────────────────────────────────────────────────\n\ntype InboundHandler = (message: MeshInbound<unknown>) => void;\n\nexport interface MessengerOptions {\n readonly clock: () => number;\n readonly emitTap: (event: MeshEvent) => void;\n readonly maxMessageBytes: number;\n readonly random?: RandomSource;\n}\n\nexport interface Messenger {\n clear(): void;\n /** Entry point for `dc` 'message' events. */\n handleInbound(peer: PeerRecord, data: unknown): void;\n on(type: string, handler: InboundHandler): Unsubscribe;\n /** Sends a typed envelope to a peer. Throws `MeshConnectionError`/`MeshPayloadError`. */\n sendTo(peer: PeerRecord, type: string, payload: unknown): void;\n}\n\nexport function createMessenger(options: MessengerOptions): Messenger {\n const handlers = new Map<string, Set<InboundHandler>>();\n\n function reject(peerId: string, reason: 'too-large' | 'duplicate' | 'malformed' | 'unknown-type'): void {\n options.emitTap({ peerId, reason, type: 'message-rejected' });\n }\n\n return {\n clear() {\n handlers.clear();\n },\n\n handleInbound(peer, data) {\n const peerId = peer.id;\n\n if (typeof data !== 'string') {\n reject(peerId, 'malformed');\n return;\n }\n\n const bytes = utf8Bytes(data);\n if (bytes > options.maxMessageBytes) {\n reject(peerId, 'too-large');\n return;\n }\n\n let decoded: unknown;\n try {\n decoded = JSON.parse(data);\n } catch {\n reject(peerId, 'malformed');\n return;\n }\n\n const envelope = toEnvelope(decoded);\n if (!envelope) {\n reject(peerId, 'malformed');\n return;\n }\n\n if (!peer.dedupe.check(envelope.id)) {\n reject(peerId, 'duplicate');\n return;\n }\n\n const set = handlers.get(envelope.t);\n if (!set || set.size === 0) {\n reject(peerId, 'unknown-type');\n return;\n }\n\n options.emitTap({ bytes, messageType: envelope.t, peerId, type: 'message-received' });\n const message: MeshInbound<unknown> = {\n messageId: envelope.id,\n payload: envelope.p,\n peerId,\n sentAt: envelope.ts,\n };\n for (const handler of set) {\n try {\n handler(message);\n } catch (cause) {\n options.emitTap({ error: new MeshError('Message handler threw', { cause }), type: 'error' });\n }\n }\n },\n\n on(type, handler) {\n let set = handlers.get(type);\n if (!set) {\n set = new Set();\n handlers.set(type, set);\n }\n set.add(handler);\n return () => {\n set.delete(handler);\n if (set.size === 0) handlers.delete(type);\n };\n },\n\n sendTo(peer, type, payload) {\n const dc = peer.dc;\n if (!dc || dc.readyState !== 'open' || peer.status !== 'connected') {\n throw new MeshConnectionError(`Peer \"${peer.id}\" is not connected`, peer.id);\n }\n\n const envelope = { id: randomId(options.random), p: payload, t: type, ts: options.clock(), v: 1 as const };\n let text: string;\n try {\n text = JSON.stringify(envelope);\n } catch (cause) {\n throw new MeshPayloadError(`Failed to serialize message \"${type}\"`, { cause });\n }\n\n const bytes = utf8Bytes(text);\n if (bytes > options.maxMessageBytes) {\n throw new MeshPayloadError(`Message \"${type}\" is ${bytes} bytes (max ${options.maxMessageBytes})`);\n }\n\n try {\n dc.send(text);\n } catch (cause) {\n throw new MeshConnectionError(`Failed to send message \"${type}\"`, peer.id, { cause });\n }\n options.emitTap({ bytes, messageType: type, peerId: peer.id, type: 'message-sent' });\n },\n };\n}\n","import { MeshPairingError } from './errors';\n\n/**\n * Compact SDP serialization for pairing payloads. A data-channel-only\n * SDP is mostly boilerplate; the remote side only needs the ICE credentials,\n * DTLS fingerprint, setup role, mid, and candidates. `compactSdp` extracts\n * those fields and `expandSdp` rebuilds a minimal valid SDP for\n * `setRemoteDescription`.\n *\n * `compactSdp` returns `null` for shapes it cannot safely represent\n * (multiple m-lines, missing credentials), letting callers keep the raw SDP.\n */\n\nexport interface CompactSdp {\n /** `a=candidate:` lines, prefix stripped. */\n c: string[];\n /** sha-256 fingerprint, hex without colons. */\n f: string;\n /** `a=mid`. */\n m: string;\n /** `a=max-message-size`. */\n mm?: number;\n /** `a=ice-pwd`. */\n p: string;\n /** `a=setup` role. */\n s: string;\n /** `a=sctp-port`. */\n sp?: number;\n /** `a=ice-ufrag`. */\n u: string;\n}\n\nfunction isCompactSdp(value: unknown): value is CompactSdp {\n if (typeof value !== 'object' || value === null) return false;\n const c = value as Record<string, unknown>;\n return (\n typeof c.u === 'string' &&\n typeof c.p === 'string' &&\n typeof c.f === 'string' &&\n typeof c.s === 'string' &&\n typeof c.m === 'string' &&\n Array.isArray(c.c) &&\n c.c.every((line) => typeof line === 'string')\n );\n}\n\n/** Returns the expanded SDP unchanged when `value` is already a string. */\nexport function restoreSdp(value: unknown): string {\n if (typeof value === 'string') return value;\n if (isCompactSdp(value)) return expandSdp(value);\n throw new MeshPairingError('Malformed pairing payload');\n}\n\nexport function compactSdp(sdp: string): CompactSdp | null {\n let ufrag = '';\n let pwd = '';\n let fingerprint = '';\n let setup = '';\n let mid = '0';\n let sctpPort: number | undefined;\n let maxMessageSize: number | undefined;\n const candidates: string[] = [];\n let mLines = 0;\n\n for (const line of sdp.split(/\\r?\\n/)) {\n if (line.startsWith('m=')) mLines += 1;\n else if (line.startsWith('a=ice-ufrag:')) ufrag = line.slice(12);\n else if (line.startsWith('a=ice-pwd:')) pwd = line.slice(10);\n else if (line.startsWith('a=fingerprint:sha-256 ')) fingerprint = line.slice(22).replaceAll(':', '').toUpperCase();\n else if (line.startsWith('a=setup:')) setup = line.slice(8);\n else if (line.startsWith('a=mid:')) mid = line.slice(6);\n else if (line.startsWith('a=sctp-port:')) sctpPort = Number(line.slice(12));\n else if (line.startsWith('a=max-message-size:')) maxMessageSize = Number(line.slice(19));\n else if (line.startsWith('a=candidate:')) candidates.push(line.slice(12));\n }\n\n if (mLines !== 1 || !ufrag || !pwd || !fingerprint) return null;\n return {\n c: candidates,\n f: fingerprint,\n m: mid,\n p: pwd,\n s: setup || 'actpass',\n ...(maxMessageSize ? { mm: maxMessageSize } : {}),\n ...(sctpPort ? { sp: sctpPort } : {}),\n u: ufrag,\n };\n}\n\n/**\n * Canonical signing form: the compact serialization when the SDP is\n * recognizable, else the raw text. `compactSdp` is lossless over the fields\n * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the\n * original — a proof computed over one verifies against the other.\n */\nexport function canonicalSdp(sdp: string): string {\n const compact = compactSdp(sdp);\n return compact ? JSON.stringify(compact) : sdp;\n}\n\nexport function expandSdp(compact: CompactSdp): string {\n const colonHex = compact.f.replace(/(..)/g, '$1:').slice(0, -1);\n return `${[\n 'v=0',\n 'o=- 0 2 IN IP4 0.0.0.0',\n 's=-',\n 't=0 0',\n `a=group:BUNDLE ${compact.m}`,\n 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',\n 'c=IN IP4 0.0.0.0',\n `a=ice-ufrag:${compact.u}`,\n `a=ice-pwd:${compact.p}`,\n `a=fingerprint:sha-256 ${colonHex}`,\n `a=setup:${compact.s}`,\n `a=mid:${compact.m}`,\n `a=sctp-port:${compact.sp ?? 5000}`,\n ...(compact.mm ? [`a=max-message-size:${compact.mm}`] : []),\n ...compact.c.map((line) => `a=candidate:${line}`),\n '',\n ].join('\\r\\n')}`;\n}\n","import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport { canonicalSdp } from './_sdp';\nimport { MeshUnsupportedError } from './errors';\n\n/**\n * Proof of invitation possession: HMAC-SHA-256 of the guest SDP keyed by the\n * invitation secret. The SDP is canonicalized first so the proof is stable\n * across pairing codecs that rebuild the description (see `mq2.`). The host\n * recomputes the proof without receiving the secret back.\n *\n * WebCrypto is required — a non-cryptographic fallback would make the proof\n * forgeable, which defeats its purpose. Environments without `SubtleCrypto`\n * (insecure contexts) must pair over `https://` or `localhost` instead.\n *\n * @throws {MeshUnsupportedError} When `SubtleCrypto` is unavailable.\n */\nexport async function createProof(secret: string, sdp: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new MeshUnsupportedError('WebCrypto is unavailable — pairing requires a secure context');\n }\n const encoder = new TextEncoder();\n const key = await subtle.importKey('raw', encoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);\n const signature = await subtle.sign('HMAC', key, encoder.encode(canonicalSdp(sdp)));\n return bytesToBase64Url(new Uint8Array(signature));\n}\n","import { MeshConnectionError, MeshDisposedError, MeshTimeoutError, MeshUnsupportedError } from './errors';\nimport type { MeshRtcFactory, RTCDataChannelLike, RTCPeerConnectionLike } from './types';\n\n/**\n * Wraps `globalThis.RTCPeerConnection`. Evaluated lazily at first use so\n * importing the package never throws in non-WebRTC environments.\n */\nexport const nativeRtcFactory: MeshRtcFactory = {\n createPeerConnection(config) {\n const Ctor = (globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }).RTCPeerConnection;\n if (!Ctor) {\n throw new MeshUnsupportedError('WebRTC is unavailable in this environment — inject an `rtc` factory');\n }\n return new Ctor(config);\n },\n};\n\n/**\n * Textareas, clipboards, and share sheets normalize CRLF→LF;\n * `setRemoteDescription` requires CRLF line endings.\n */\nexport function normalizeSdp(sdp: string): string {\n const normalized = sdp.replace(/\\r?\\n/g, '\\r\\n');\n return normalized.endsWith('\\r\\n') ? normalized : `${normalized}\\r\\n`;\n}\n\n/**\n * Resolves when ICE gathering finishes or after `timeoutMs` — the caller\n * proceeds with whatever candidates were gathered (non-trickle semantics).\n */\nexport function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve();\n return new Promise((resolve) => {\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener('icegatheringstatechange', onChange);\n };\n const onChange = () => {\n if (pc.iceGatheringState === 'complete') {\n cleanup();\n resolve();\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, timeoutMs);\n pc.addEventListener('icegatheringstatechange', onChange);\n });\n}\n\n/**\n * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after\n * `timeoutMs` — or when the channel closes while that deadline is still\n * pending, since a peer tearing down a never-opened channel races the same\n * deadline — `MeshConnectionError` when the ICE transport dies first, and\n * `MeshDisposedError` when `disposalSignal` aborts.\n */\nexport function waitChannelOpen(\n dc: RTCDataChannelLike,\n pc: RTCPeerConnectionLike,\n timeoutMs: number,\n peerId: string,\n disposalSignal: AbortSignal,\n): Promise<void> {\n if (dc.readyState === 'open') return Promise.resolve();\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const timeout = () => new MeshTimeoutError(`Timed out waiting ${timeoutMs}ms for the data channel to open`);\n const fail = (error: Error) => {\n cleanup();\n reject(error);\n };\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n dc.removeEventListener('open', onOpen);\n dc.removeEventListener('close', onClose);\n pc.removeEventListener('iceconnectionstatechange', onIce);\n disposalSignal.removeEventListener('abort', onDispose);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n // A close while our open deadline is still armed is the timeout it\n // effectively is; mirror the guest's open-timer guard so both ends agree\n // on the cause instead of racing to report whichever event landed first.\n const onClose = () => fail(timeout());\n const onIce = () => {\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed' || state === 'disconnected') {\n fail(new MeshConnectionError(`ICE connection ${state} before channel open`, peerId));\n }\n };\n const onDispose = () => fail(new MeshDisposedError());\n timer = setTimeout(() => fail(timeout()), timeoutMs);\n dc.addEventListener('open', onOpen);\n dc.addEventListener('close', onClose);\n pc.addEventListener('iceconnectionstatechange', onIce);\n disposalSignal.addEventListener('abort', onDispose, { once: true });\n });\n}\n","import {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, type MeshError, MeshPairingError, MeshTimeoutError } from './errors';\nimport type {\n MeshGuest,\n MeshGuestOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\n/**\n * Create a guest mesh node. The guest pairs with one host by consuming an\n * out-of-band invitation and returning an answer.\n */\nexport function createMeshGuest<P extends MeshProtocol>(options: MeshGuestOptions = {}): MeshGuest<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n let hostPeer: PeerRecord | null = null;\n let openTimer: ReturnType<typeof setTimeout> | null = null;\n\n const openTimeoutError = (): MeshTimeoutError =>\n new MeshTimeoutError(`Timed out waiting ${channelOpenTimeoutMs}ms for the data channel to open`);\n\n function failPeer(peer: PeerRecord, error: MeshError): void {\n // Ignore events from a superseded peer after re-pairing replaced hostPeer.\n if (core.disposed || peer !== hostPeer || peer.status === 'failed') return;\n peer.status = 'failed';\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n // Teardown is deferred a macrotask so a peer racing its own open\n // deadline reports its timeout rather than this hangup.\n setTimeout(() => {\n peer.dc?.close();\n peer.pc?.close();\n }, 0);\n core.emitTap({ peerId: peer.id, status: 'failed', type: 'peer-status-change' });\n core.emitTap({ error, type: 'error' });\n core.emitTap({ peer: peer.publicPeer, reason: error.message, type: 'peer-left' });\n core.setStatus('failed');\n }\n\n function disconnectPeer(peer: PeerRecord, reason: string): void {\n if (core.disposed || peer !== hostPeer) return;\n if (peer.status === 'disconnected' || peer.status === 'failed') return;\n peer.status = 'disconnected';\n peer.pc?.close();\n core.emitTap({ peerId: peer.id, status: 'disconnected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n core.setStatus('disconnected');\n }\n\n function wireChannel(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.dc = dc;\n\n dc.addEventListener('open', () => {\n if (core.disposed) return;\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n peer.status = 'connected';\n core.emitTap({ peerId: peer.id, status: 'connected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, type: 'peer-joined' });\n core.setStatus('connected');\n });\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n if (peer.status === 'connecting')\n failPeer(\n peer,\n openTimer ? openTimeoutError() : new MeshConnectionError('channel closed before opening', peer.id),\n );\n else disconnectPeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n failPeer(peer, new MeshConnectionError('channel error', peer.id));\n });\n\n // The ice-state emission lives on the pairing-scoped listener above; this\n // one owns channel-lifecycle transitions only, so each ICE change emits\n // exactly one ice-state event.\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed') {\n failPeer(peer, new MeshConnectionError(`ice ${state}`, peer.id));\n } else if (state === 'disconnected' && peer.status === 'connected') {\n disconnectPeer(peer, 'ice disconnected');\n }\n });\n }\n\n const guest: MeshGuest<P> = {\n async acceptInvitation(invitation, meta) {\n core.ensureLive();\n // A terminal host peer (failed/disconnected) may be replaced by a fresh\n // pairing on the same node — listeners stay attached and keep working.\n if (hostPeer && hostPeer.status !== 'failed' && hostPeer.status !== 'disconnected') {\n throw new MeshPairingError('Guest is already paired');\n }\n if (invitation?.v !== 1) throw new MeshPairingError('Unsupported invitation version');\n if (clock() >= invitation.expiresAt) throw new MeshPairingError('Invitation expired');\n\n if (hostPeer) {\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n hostPeer.pc?.close();\n hostPeer = null;\n }\n\n core.setStatus('pairing');\n let pc: RTCPeerConnectionLike;\n try {\n pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n } catch (error) {\n core.setStatus('idle');\n throw error;\n }\n\n // The invitation carries no host identity beyond an optional display\n // name — the session id doubles as the host-side peer id for inbound\n // metadata and tap events.\n const peer = createPeerRecord(invitation.sessionId, invitation.hostName, 'host');\n peer.pc = pc;\n hostPeer = peer;\n\n pc.addEventListener('datachannel', (event) => {\n wireChannel(peer, pc, event.channel);\n });\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n core.emitTap({ peerId: peer.id, state: pc.iceConnectionState, type: 'ice-state' });\n });\n\n try {\n await pc.setRemoteDescription({ sdp: normalizeSdp(invitation.sdp), type: 'offer' });\n } catch (cause) {\n hostPeer = null;\n core.setStatus('idle');\n throw new MeshPairingError('Invalid invitation SDP', { cause });\n }\n\n let answerSdp: string;\n try {\n await pc.setLocalDescription(await pc.createAnswer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n answerSdp = pc.localDescription?.sdp ?? '';\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Failed to create answer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n if (!answerSdp) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const peerId = randomId(options.random);\n let proof: string;\n try {\n proof = await createProof(invitation.secret, answerSdp);\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw cause;\n }\n core.setStatus('connecting');\n\n // If the channel never opens (host never accepts the answer), fail loudly\n // instead of hanging in 'connecting' forever. The deadline must cover\n // the human carry-back of the answer, not just machine negotiation.\n openTimer = setTimeout(() => {\n if (hostPeer?.status === 'connecting') {\n failPeer(hostPeer, openTimeoutError());\n }\n }, channelOpenTimeoutMs);\n\n return {\n peer: meta?.name === undefined ? { id: peerId } : { id: peerId, name: meta.name },\n proof,\n sdp: answerSdp,\n sessionId: invitation.sessionId,\n v: 1,\n };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n if (hostPeer) {\n hostPeer.dc?.close();\n hostPeer.pc?.close();\n }\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n get host(): MeshPeer | null {\n return hostPeer?.publicPeer ?? null;\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n\n send(type, payload) {\n core.ensureLive();\n if (!hostPeer) throw new MeshConnectionError('Guest is not paired');\n messenger.sendTo(hostPeer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return guest;\n}\n","import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_INVITATION_TTL_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomBytes, randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitChannelOpen, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, MeshPairingError } from './errors';\nimport type {\n MeshHost,\n MeshHostOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n MeshStatus,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\ninterface PendingInvitation {\n answered: boolean;\n readonly dc: RTCDataChannelLike;\n readonly expiresAt: number;\n readonly expiryTimer: ReturnType<typeof setTimeout>;\n readonly pc: RTCPeerConnectionLike;\n readonly secret: string;\n readonly sessionId: string;\n}\n\n/**\n * Create a host-authoritative mesh node. The host holds one peer connection\n * per guest and pairs each one through a manual invitation/answer exchange.\n */\nexport function createMeshHost<P extends MeshProtocol>(options: MeshHostOptions = {}): MeshHost<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const invitationTtlMs = options.invitationTtlMs ?? DEFAULT_INVITATION_TTL_MS;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n const channelInit = { label: 'mesh', ordered: true, ...options.channel };\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n const peers = new Map<string, PeerRecord>();\n const pending = new Map<string, PendingInvitation>();\n\n let sawPeer = false;\n let sawFailure = false;\n\n function recomputeStatus(): void {\n let next: MeshStatus = pending.size > 0 ? 'pairing' : sawPeer ? 'disconnected' : sawFailure ? 'failed' : 'idle';\n for (const peer of peers.values()) {\n if (peer.status === 'connected') {\n next = 'connected';\n break;\n }\n if (peer.status === 'connecting') next = 'connecting';\n }\n core.setStatus(next);\n }\n\n function expireInvitation(sessionId: string): void {\n const invitation = pending.get(sessionId);\n if (!invitation) return;\n pending.delete(sessionId);\n clearTimeout(invitation.expiryTimer);\n invitation.pc.close();\n core.emitTap({ sessionId, type: 'invitation-expired' });\n recomputeStatus();\n }\n\n function setPeerStatus(peer: PeerRecord, status: PeerRecord['status']): void {\n if (peer.status === status) return;\n peer.status = status;\n core.emitTap({ peerId: peer.id, status, type: 'peer-status-change' });\n recomputeStatus();\n }\n\n function leavePeer(peer: PeerRecord, reason: string | undefined, failed = false): void {\n if (!peers.has(peer.id)) return;\n peers.delete(peer.id);\n peer.status = failed ? 'failed' : 'disconnected';\n peer.pc?.close();\n peer.dc?.close();\n core.emitTap({ peerId: peer.id, status: peer.status, type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n if (!failed) sawPeer = true;\n sawFailure = sawFailure || failed;\n recomputeStatus();\n }\n\n function wirePeerTransport(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.pc = pc;\n peer.dc = dc;\n\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n leavePeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n leavePeer(peer, 'channel error', true);\n });\n\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n core.emitTap({ peerId: peer.id, state, type: 'ice-state' });\n if (state === 'failed' || state === 'closed') {\n leavePeer(peer, `ice ${state}`, state === 'failed');\n } else if (state === 'disconnected' && peer.status === 'connected') {\n setPeerStatus(peer, 'disconnected');\n }\n });\n }\n\n function markPeerConnected(peer: PeerRecord): void {\n sawPeer = true;\n peers.set(peer.id, peer);\n setPeerStatus(peer, 'connected');\n core.emitTap({ peer, type: 'peer-joined' });\n }\n\n const host: MeshHost<P> = {\n async acceptAnswer(answer) {\n core.ensureLive();\n\n if (answer?.v !== 1) throw new MeshPairingError('Unsupported answer version');\n if (!answer.peer?.id) throw new MeshPairingError('Answer is missing a peer id');\n const invitation = pending.get(answer.sessionId);\n if (!invitation) throw new MeshPairingError('Unknown or expired session');\n if (invitation.answered) throw new MeshPairingError('Invitation already answered');\n if (peers.has(answer.peer.id)) throw new MeshPairingError(`Peer \"${answer.peer.id}\" is already connected`);\n if (clock() >= invitation.expiresAt) {\n expireInvitation(answer.sessionId);\n throw new MeshPairingError('Invitation expired');\n }\n\n const expected = await createProof(invitation.secret, answer.sdp);\n if (answer.proof !== expected) throw new MeshPairingError('Proof mismatch');\n\n const approved = (await options.approvePeer?.(answer.peer)) ?? true;\n if (!approved) {\n core.emitTap({ peerId: answer.peer.id, type: 'peer-rejected' });\n throw new MeshPairingError(`Peer \"${answer.peer.id}\" rejected by host`);\n }\n core.emitTap({ peerId: answer.peer.id, type: 'peer-approved' });\n\n // Re-check after the proof/approval awaits: concurrent answers for\n // different sessions could both have passed the check above.\n if (peers.has(answer.peer.id)) throw new MeshPairingError(`Peer \"${answer.peer.id}\" is already connected`);\n\n invitation.answered = true;\n pending.delete(answer.sessionId);\n clearTimeout(invitation.expiryTimer);\n\n const peer = createPeerRecord(answer.peer.id, answer.peer.name, 'guest');\n const opened = waitChannelOpen(\n invitation.dc,\n invitation.pc,\n channelOpenTimeoutMs,\n peer.id,\n core.disposalCtrl.signal,\n );\n wirePeerTransport(peer, invitation.pc, invitation.dc);\n peers.set(peer.id, peer);\n recomputeStatus();\n\n try {\n await invitation.pc.setRemoteDescription({ sdp: normalizeSdp(answer.sdp), type: 'answer' });\n } catch (cause) {\n leavePeer(peer, 'invalid answer sdp', true);\n throw new MeshPairingError('Invalid answer SDP', { cause });\n }\n\n try {\n await opened;\n } catch (error) {\n const failed = !(error instanceof MeshDisposedError);\n leavePeer(peer, error instanceof Error ? error.message : 'open failed', failed);\n throw error;\n }\n\n markPeerConnected(peer);\n return peer.publicPeer;\n },\n\n broadcast(type, payload, broadcastOptions) {\n core.ensureLive();\n for (const peer of peers.values()) {\n if (peer.status !== 'connected' || broadcastOptions?.except?.includes(peer.id)) continue;\n messenger.sendTo(peer, type, payload);\n }\n },\n\n async createInvitation(meta) {\n core.ensureLive();\n\n const pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n const dc = pc.createDataChannel(channelInit.label, {\n maxRetransmits: channelInit.maxRetransmits,\n ordered: channelInit.ordered,\n });\n\n const sessionId = randomId(options.random);\n const secret = bytesToBase64Url(randomBytes(32, options.random));\n const expiresAt = clock() + invitationTtlMs;\n\n try {\n await pc.setLocalDescription(await pc.createOffer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n } catch (cause) {\n pc.close();\n throw new MeshConnectionError('Failed to create offer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n\n const sdp = pc.localDescription?.sdp;\n if (!sdp) {\n pc.close();\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const expiryTimer = setTimeout(() => expireInvitation(sessionId), invitationTtlMs);\n pending.set(sessionId, { answered: false, dc, expiresAt, expiryTimer, pc, secret, sessionId });\n core.emitTap({ sessionId, type: 'invitation-created' });\n recomputeStatus();\n return { expiresAt, sdp, secret, sessionId, v: 1, ...(meta?.name === undefined ? {} : { hostName: meta.name }) };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n for (const invitation of pending.values()) {\n clearTimeout(invitation.expiryTimer);\n invitation.pc.close();\n }\n pending.clear();\n for (const peer of peers.values()) {\n peer.dc?.close();\n peer.pc?.close();\n }\n peers.clear();\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n\n kick(peerId, reason) {\n core.ensureLive();\n const peer = peers.get(peerId);\n if (!peer) throw new MeshConnectionError(`Unknown peer \"${peerId}\"`, peerId);\n leavePeer(peer, reason ?? 'kicked');\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n get peers() {\n return peers as ReadonlyMap<string, MeshPeer>;\n },\n\n send(peerId, type, payload) {\n core.ensureLive();\n const peer = peers.get(peerId);\n if (!peer) throw new MeshConnectionError(`Unknown peer \"${peerId}\"`, peerId);\n messenger.sendTo(peer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return host;\n}\n","import { MeshPairingError } from './errors';\n\n/**\n * Base45 (RFC 9285): two bytes → three chars, one leftover byte → two chars.\n * The alphabet sits inside the QR alphanumeric charset, so QR encoders can use\n * alphanumeric mode (~5.5 bits/char) instead of byte mode — about a third\n * denser than base64 for the same payload.\n */\n\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';\nconst INDEX = new Map([...ALPHABET].map((ch, i) => [ch, i]));\n\nexport function bytesToBase45(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i + 1 < bytes.length; i += 2) {\n const value = bytes[i] * 256 + bytes[i + 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45) % 45] + ALPHABET[Math.floor(value / 2025)];\n }\n if (bytes.length % 2) {\n const value = bytes[bytes.length - 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45)];\n }\n return out;\n}\n\nexport function base45ToBytes(text: string): Uint8Array {\n const out: number[] = [];\n let i = 0;\n while (i + 2 < text.length) {\n const value = digit(text, i) + 45 * digit(text, i + 1) + 2025 * digit(text, i + 2);\n if (value > 0xffff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value >> 8, value & 0xff);\n i += 3;\n }\n if (text.length - i === 2) {\n const value = digit(text, i) + 45 * digit(text, i + 1);\n if (value > 0xff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value);\n } else if (text.length - i === 1) {\n throw new MeshPairingError('Malformed base45 payload');\n }\n return new Uint8Array(out);\n}\n\nfunction digit(text: string, at: number): number {\n const value = INDEX.get(text[at]);\n if (value === undefined) throw new MeshPairingError('Malformed base45 payload');\n return value;\n}\n","import { base45ToBytes, bytesToBase45 } from './_base45';\nimport { compactSdp, restoreSdp } from './_sdp';\nimport { isRecord, meshCodec, parsePairingPayload } from './codec';\nimport { MeshPairingError, MeshUnsupportedError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\ninterface QrCodec {\n decode(text: string): Promise<MeshInvitation | MeshAnswer>;\n encode(payload: MeshInvitation | MeshAnswer): Promise<string>;\n}\n\n/**\n * QR-friendly async codec. `encode` emits `\"mq2.\"` payloads: the SDP is\n * reduced to the fields the remote needs (see `_sdp.ts`), the JSON is\n * deflate-raw compressed (`CompressionStream`), and the bytes are base45 —\n * an alphabet inside the QR alphanumeric charset, so encoders use ~5.5\n * bits/char instead of 8. Where `CompressionStream` is unavailable it\n * falls back to plain `meshCodec` output, which `decode` also accepts —\n * encoding never fails on a missing capability.\n *\n * `decode` accepts `\"mq2.\"` and plain `meshCodec` output — a guest can paste\n * or scan interchangeably.\n */\n\nconst PREFIX_V2 = 'mq2.';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nfunction decompressionStream(kind: 'deflate-raw'): DecompressionStream {\n if (typeof globalThis.DecompressionStream !== 'function')\n throw new MeshUnsupportedError('DecompressionStream is not available in this environment');\n return new globalThis.DecompressionStream(kind);\n}\n\nasync function pump(\n data: Uint8Array,\n stream: {\n readable: ReadableStream<Uint8Array>;\n writable: WritableStream<BufferSource>;\n },\n): Promise<Uint8Array> {\n const writer = stream.writable.getWriter();\n // When the read side fails first (corrupt input), the write side rejects too;\n // swallow it so the real read error is the one that surfaces.\n const write = writer\n .write(data as BufferSource)\n .then(() => writer.close())\n .catch(() => {});\n const chunks: Uint8Array[] = [];\n const reader = stream.readable.getReader();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n await write;\n const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport const meshQrCodec: QrCodec = {\n async decode(text) {\n const trimmed = text.trim();\n if (trimmed.startsWith(PREFIX_V2)) {\n let parsed: unknown;\n try {\n const compressed = base45ToBytes(trimmed.slice(PREFIX_V2.length));\n parsed = JSON.parse(decoder.decode(await pump(compressed, decompressionStream('deflate-raw'))));\n } catch (cause) {\n if (cause instanceof MeshUnsupportedError) throw cause;\n if (cause instanceof MeshPairingError) throw cause;\n throw new MeshPairingError('Malformed pairing payload', { cause });\n }\n if (isRecord(parsed)) parsed = { ...parsed, sdp: restoreSdp(parsed.sdp) };\n return parsePairingPayload(parsed);\n }\n // Plain meshCodec output — paste and scan stay interchangeable.\n return meshCodec.decode(trimmed);\n },\n async encode(payload) {\n // Missing capability, not a failure: plain output still pairs everywhere.\n if (typeof globalThis.CompressionStream !== 'function') return meshCodec.encode(payload);\n const compact = compactSdp(payload.sdp);\n const json = encoder.encode(JSON.stringify(compact ? { ...payload, sdp: compact } : payload));\n const compressed = await pump(json, new globalThis.CompressionStream('deflate-raw'));\n return PREFIX_V2 + bytesToBase45(compressed);\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mesh.js","names":[],"sources":["../src/errors.ts","../src/codec.ts","../src/_defaults.ts","../src/_random.ts","../src/_wire.ts","../src/_node.ts","../src/_sdp.ts","../src/_proof.ts","../src/_rtc.ts","../src/guest.ts","../src/host.ts","../src/_base45.ts","../src/qr-codec.ts"],"sourcesContent":["/**\n * Base class for all mesh errors.\n * Use `instanceof MeshError` to catch any mesh-originated error in one branch.\n */\nexport class MeshError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Thrown when a pairing payload is malformed, expired, unknown, already\n * consumed, carries a mismatched proof, or is rejected by the host's\n * `approvePeer` hook.\n */\nexport class MeshPairingError extends MeshError {}\n\n/**\n * Thrown when the peer connection fails (ICE/DTLS failure, channel closed\n * unexpectedly) or a send targets an unknown or unconnected peer.\n */\nexport class MeshConnectionError extends MeshError {\n readonly peerId: string | null;\n\n constructor(message: string, peerId: string | null = null, opts?: ErrorOptions) {\n super(message, opts);\n this.peerId = peerId;\n }\n}\n\n/**\n * Thrown when an outbound message exceeds `maxMessageBytes` or cannot be\n * serialized.\n */\nexport class MeshPayloadError extends MeshError {}\n\n/**\n * Thrown when ICE gathering or the data-channel open handshake exceeds the\n * configured timeout.\n */\nexport class MeshTimeoutError extends MeshError {}\n\n/**\n * Thrown when a method is called on a disposed mesh node.\n */\nexport class MeshDisposedError extends MeshError {\n constructor(target = 'Mesh node', opts?: ErrorOptions) {\n super(`${target} is disposed`, opts);\n }\n}\n\n/**\n * Thrown when WebRTC is unavailable in the current environment and no `rtc`\n * factory was injected. Raised at first use, never at import time.\n */\nexport class MeshUnsupportedError extends MeshError {}\n","import { base64UrlToText, textToBase64Url } from '@vielzeug/arsenal';\nimport { MeshPairingError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\n/** Structural guard shared by the plain and QR pairing codecs. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isInvitation(value: Record<string, unknown>): value is Record<string, unknown> & MeshInvitation {\n return (\n value.v === 1 &&\n typeof value.sessionId === 'string' &&\n typeof value.secret === 'string' &&\n typeof value.expiresAt === 'number' &&\n typeof value.sdp === 'string'\n );\n}\n\nfunction isAnswer(value: Record<string, unknown>): value is Record<string, unknown> & MeshAnswer {\n return (\n value.v === 1 &&\n typeof value.sessionId === 'string' &&\n typeof value.proof === 'string' &&\n isRecord(value.peer) &&\n typeof value.peer.id === 'string' &&\n value.peer.id !== '' &&\n (value.peer.name === undefined || typeof value.peer.name === 'string') &&\n typeof value.sdp === 'string'\n );\n}\n\n/**\n * Validate a decoded pairing payload. Shared by `meshCodec` and `meshQrCodec`\n * so both encodings enforce the same shape/version contract.\n *\n * @throws {MeshPairingError} On unsupported version or unrecognized shape.\n */\nexport function parsePairingPayload(parsed: unknown): MeshInvitation | MeshAnswer {\n if (!isRecord(parsed) || parsed.v !== 1) {\n throw new MeshPairingError('Unsupported pairing payload version');\n }\n if (isInvitation(parsed) || isAnswer(parsed)) {\n return parsed;\n }\n throw new MeshPairingError('Unrecognized pairing payload shape');\n}\n\n/**\n * Compact JSON → base64url codec for pairing payloads. Pure and transport-free\n * so alternative encodings (compressed, QR-optimized) can be added later\n * without touching the transport.\n *\n * @example\n * ```ts\n * const invitation = await host.createInvitation();\n * const text = meshCodec.encode(invitation); // copy/paste or navigator.share\n * const answer = await guest.acceptInvitation(meshCodec.decode(text));\n * await host.acceptAnswer(meshCodec.decode(meshCodec.encode(answer)));\n * ```\n *\n * @throws {MeshPairingError} On malformed text or unsupported payload shape/version.\n */\nexport const meshCodec = {\n decode(text: string): MeshInvitation | MeshAnswer {\n let parsed: unknown;\n try {\n parsed = JSON.parse(base64UrlToText(text.trim()));\n } catch (cause) {\n throw new MeshPairingError('Malformed pairing payload', { cause });\n }\n return parsePairingPayload(parsed);\n },\n encode(payload: MeshInvitation | MeshAnswer): string {\n return textToBase64Url(JSON.stringify(payload));\n },\n};\n","/** Option defaults shared by host and guest so the two never drift. */\nexport const DEFAULT_CHANNEL_OPEN_TIMEOUT_MS = 60_000;\nexport const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 5_000;\nexport const DEFAULT_INVITATION_TTL_MS = 300_000;\nexport const DEFAULT_MAX_MESSAGE_BYTES = 65_536;\n","import { bytesToBase64Url, type RandomSource, randomFloat } from '@vielzeug/arsenal';\n\n/**\n * Random bytes from `crypto.getRandomValues`, or from an injected\n * `RandomSource` for deterministic tests.\n */\nexport function randomBytes(length: number, source?: RandomSource): Uint8Array {\n const bytes = new Uint8Array(length);\n if (source) {\n for (let index = 0; index < length; index++) bytes[index] = Math.floor(randomFloat(source) * 256);\n } else {\n crypto.getRandomValues(bytes);\n }\n return bytes;\n}\n\n/** Random 128-bit base64url id: used for peer ids, session ids, and message ids. */\nexport function randomId(source?: RandomSource): string {\n return bytesToBase64Url(randomBytes(16, source));\n}\n","/** Wire envelope serialized through the configured `serialize` function. */\nexport interface WireEnvelope {\n readonly id: string;\n readonly p: unknown;\n readonly t: string;\n readonly ts: number;\n readonly v: 1;\n}\n\n/** Returns the envelope, or `null` when the value violates the wire shape. */\nexport function toEnvelope(value: unknown): WireEnvelope | null {\n if (typeof value !== 'object' || value === null) return null;\n const env = value as Record<string, unknown>;\n if (env.v !== 1 || typeof env.id !== 'string' || typeof env.t !== 'string' || typeof env.ts !== 'number') return null;\n return value as WireEnvelope;\n}\n\n/** Per-peer duplicate guard: remembers the last `capacity` message ids (LRU). */\nexport class MessageDedupe {\n private readonly seen = new Set<string>();\n private readonly order: string[] = [];\n private readonly capacity: number;\n\n constructor(capacity = 256) {\n this.capacity = capacity;\n }\n\n /** `true` when the id is new (and records it); `false` for duplicates. */\n check(id: string): boolean {\n if (this.seen.has(id)) return false;\n this.seen.add(id);\n this.order.push(id);\n if (this.order.length > this.capacity) this.seen.delete(this.order.shift()!);\n return true;\n }\n}\n","import { type RandomSource, tapper, utf8Bytes } from '@vielzeug/arsenal';\nimport { randomId } from './_random';\nimport { MessageDedupe, toEnvelope } from './_wire';\nimport { MeshConnectionError, MeshDisposedError, MeshError, MeshPayloadError } from './errors';\nimport type {\n MeshEvent,\n MeshInbound,\n MeshPeer,\n MeshPeerStatus,\n MeshStatus,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n Unsubscribe,\n} from './types';\n\ntype Tapper = (event: MeshEvent) => void;\n\n/** Shared lifecycle core: status, tappers, disposal. */\nexport interface NodeCore {\n readonly disposalCtrl: AbortController;\n disposed: boolean;\n emitTap(event: MeshEvent): void;\n ensureLive(): void;\n /** Emits the final status-change + dispose events, clears tappers, aborts. */\n markDisposed(): void;\n setStatus(next: MeshStatus): void;\n status: MeshStatus;\n tap(handler: Tapper, options?: { readonly signal?: AbortSignal }): Unsubscribe;\n}\n\nexport function createNodeCore(signal?: AbortSignal): NodeCore {\n const tappers = tapper<MeshEvent>();\n const disposalCtrl = new AbortController();\n\n const core: NodeCore = {\n disposalCtrl,\n disposed: false,\n\n emitTap(event) {\n tappers.emit(event);\n },\n\n ensureLive() {\n if (core.disposed) throw new MeshDisposedError();\n },\n\n markDisposed() {\n if (core.disposed) return;\n core.disposed = true;\n core.status = 'disposed';\n core.emitTap({ status: 'disposed', type: 'status-change' });\n core.emitTap({ type: 'dispose' });\n tappers.clear();\n disposalCtrl.abort();\n },\n\n setStatus(next) {\n if (core.disposed || core.status === next) return;\n core.status = next;\n core.emitTap({ status: next, type: 'status-change' });\n },\n status: 'idle',\n\n tap(handler, options) {\n if (core.disposed) return () => {};\n return tappers.tap(handler, options);\n },\n };\n\n if (signal) {\n if (signal.aborted) {\n core.markDisposed();\n } else {\n signal.addEventListener('abort', () => core.markDisposed(), { once: true });\n }\n }\n\n return core;\n}\n\n// ─── Peers ───────────────────────────────────────────────────────────────────\n\n/** Internal peer record; `publicPeer` exposes live status through getters. */\nexport interface PeerRecord {\n dc: RTCDataChannelLike | null;\n readonly dedupe: MessageDedupe;\n readonly id: string;\n readonly name: string | undefined;\n pc: RTCPeerConnectionLike | null;\n readonly publicPeer: MeshPeer;\n readonly role: 'host' | 'guest';\n status: MeshPeerStatus;\n}\n\nexport function createPeerRecord(id: string, name: string | undefined, role: 'host' | 'guest'): PeerRecord {\n const record: PeerRecord = {\n dc: null,\n dedupe: new MessageDedupe(),\n id,\n name,\n pc: null,\n publicPeer: {\n get id() {\n return record.id;\n },\n get name() {\n return record.name;\n },\n get role() {\n return record.role;\n },\n get status() {\n return record.status;\n },\n },\n role,\n status: 'connecting',\n };\n return record;\n}\n\n// ─── Messenger ───────────────────────────────────────────────────────────────\n\ntype InboundHandler = (message: MeshInbound<unknown>) => void;\n\nexport interface MessengerOptions {\n readonly clock: () => number;\n readonly emitTap: (event: MeshEvent) => void;\n readonly maxMessageBytes: number;\n readonly random?: RandomSource;\n}\n\nexport interface Messenger {\n clear(): void;\n /** Entry point for `dc` 'message' events. */\n handleInbound(peer: PeerRecord, data: unknown): void;\n on(type: string, handler: InboundHandler): Unsubscribe;\n /** Sends a typed envelope to a peer. Throws `MeshConnectionError`/`MeshPayloadError`. */\n sendTo(peer: PeerRecord, type: string, payload: unknown): void;\n}\n\nexport function createMessenger(options: MessengerOptions): Messenger {\n const handlers = new Map<string, Set<InboundHandler>>();\n\n function reject(peerId: string, reason: 'too-large' | 'duplicate' | 'malformed' | 'unknown-type'): void {\n options.emitTap({ peerId, reason, type: 'message-rejected' });\n }\n\n return {\n clear() {\n handlers.clear();\n },\n\n handleInbound(peer, data) {\n const peerId = peer.id;\n\n if (typeof data !== 'string') {\n reject(peerId, 'malformed');\n return;\n }\n\n const bytes = utf8Bytes(data);\n if (bytes > options.maxMessageBytes) {\n reject(peerId, 'too-large');\n return;\n }\n\n let decoded: unknown;\n try {\n decoded = JSON.parse(data);\n } catch {\n reject(peerId, 'malformed');\n return;\n }\n\n const envelope = toEnvelope(decoded);\n if (!envelope) {\n reject(peerId, 'malformed');\n return;\n }\n\n if (!peer.dedupe.check(envelope.id)) {\n reject(peerId, 'duplicate');\n return;\n }\n\n const set = handlers.get(envelope.t);\n if (!set || set.size === 0) {\n reject(peerId, 'unknown-type');\n return;\n }\n\n options.emitTap({ bytes, messageType: envelope.t, peerId, type: 'message-received' });\n const message: MeshInbound<unknown> = {\n messageId: envelope.id,\n payload: envelope.p,\n peerId,\n sentAt: envelope.ts,\n };\n for (const handler of set) {\n try {\n handler(message);\n } catch (cause) {\n options.emitTap({ error: new MeshError('Message handler threw', { cause }), type: 'error' });\n }\n }\n },\n\n on(type, handler) {\n let set = handlers.get(type);\n if (!set) {\n set = new Set();\n handlers.set(type, set);\n }\n set.add(handler);\n return () => {\n set.delete(handler);\n if (set.size === 0) handlers.delete(type);\n };\n },\n\n sendTo(peer, type, payload) {\n const dc = peer.dc;\n if (!dc || dc.readyState !== 'open' || peer.status !== 'connected') {\n throw new MeshConnectionError(`Peer \"${peer.id}\" is not connected`, peer.id);\n }\n\n const envelope = { id: randomId(options.random), p: payload, t: type, ts: options.clock(), v: 1 as const };\n let text: string;\n try {\n text = JSON.stringify(envelope);\n } catch (cause) {\n throw new MeshPayloadError(`Failed to serialize message \"${type}\"`, { cause });\n }\n\n const bytes = utf8Bytes(text);\n if (bytes > options.maxMessageBytes) {\n throw new MeshPayloadError(`Message \"${type}\" is ${bytes} bytes (max ${options.maxMessageBytes})`);\n }\n\n try {\n dc.send(text);\n } catch (cause) {\n throw new MeshConnectionError(`Failed to send message \"${type}\"`, peer.id, { cause });\n }\n options.emitTap({ bytes, messageType: type, peerId: peer.id, type: 'message-sent' });\n },\n };\n}\n","import { MeshPairingError } from './errors';\n\n/**\n * Compact SDP serialization for pairing payloads. A data-channel-only\n * SDP is mostly boilerplate; the remote side only needs the ICE credentials,\n * DTLS fingerprint, setup role, mid, and candidates. `compactSdp` extracts\n * those fields and `expandSdp` rebuilds a minimal valid SDP for\n * `setRemoteDescription`.\n *\n * `compactSdp` returns `null` for shapes it cannot safely represent\n * (multiple m-lines, missing credentials), letting callers keep the raw SDP.\n */\n\nexport interface CompactSdp {\n /** `a=candidate:` lines, prefix stripped. */\n c: string[];\n /** sha-256 fingerprint, hex without colons. */\n f: string;\n /** `a=mid`. */\n m: string;\n /** `a=max-message-size`. */\n mm?: number;\n /** `a=ice-pwd`. */\n p: string;\n /** `a=setup` role. */\n s: string;\n /** `a=sctp-port`. */\n sp?: number;\n /** `a=ice-ufrag`. */\n u: string;\n}\n\nfunction isCompactSdp(value: unknown): value is CompactSdp {\n if (typeof value !== 'object' || value === null) return false;\n const c = value as Record<string, unknown>;\n return (\n typeof c.u === 'string' &&\n typeof c.p === 'string' &&\n typeof c.f === 'string' &&\n typeof c.s === 'string' &&\n typeof c.m === 'string' &&\n Array.isArray(c.c) &&\n c.c.every((line) => typeof line === 'string')\n );\n}\n\n/** Returns the expanded SDP unchanged when `value` is already a string. */\nexport function restoreSdp(value: unknown): string {\n if (typeof value === 'string') return value;\n if (isCompactSdp(value)) return expandSdp(value);\n throw new MeshPairingError('Malformed pairing payload');\n}\n\nexport function compactSdp(sdp: string): CompactSdp | null {\n let ufrag = '';\n let pwd = '';\n let fingerprint = '';\n let setup = '';\n let mid = '0';\n let sctpPort: number | undefined;\n let maxMessageSize: number | undefined;\n const candidates: string[] = [];\n let mLines = 0;\n\n for (const line of sdp.split(/\\r?\\n/)) {\n if (line.startsWith('m=')) mLines += 1;\n else if (line.startsWith('a=ice-ufrag:')) ufrag = line.slice(12);\n else if (line.startsWith('a=ice-pwd:')) pwd = line.slice(10);\n else if (line.startsWith('a=fingerprint:sha-256 ')) fingerprint = line.slice(22).replaceAll(':', '').toUpperCase();\n else if (line.startsWith('a=setup:')) setup = line.slice(8);\n else if (line.startsWith('a=mid:')) mid = line.slice(6);\n else if (line.startsWith('a=sctp-port:')) sctpPort = Number(line.slice(12));\n else if (line.startsWith('a=max-message-size:')) maxMessageSize = Number(line.slice(19));\n else if (line.startsWith('a=candidate:')) candidates.push(line.slice(12));\n }\n\n if (mLines !== 1 || !ufrag || !pwd || !fingerprint) return null;\n return {\n c: candidates,\n f: fingerprint,\n m: mid,\n p: pwd,\n s: setup || 'actpass',\n ...(maxMessageSize ? { mm: maxMessageSize } : {}),\n ...(sctpPort ? { sp: sctpPort } : {}),\n u: ufrag,\n };\n}\n\n/**\n * Canonical signing form: the compact serialization when the SDP is\n * recognizable, else the raw text. `compactSdp` is lossless over the fields\n * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the\n * original: a proof computed over one verifies against the other.\n */\nexport function canonicalSdp(sdp: string): string {\n const compact = compactSdp(sdp);\n return compact ? JSON.stringify(compact) : sdp;\n}\n\nexport function expandSdp(compact: CompactSdp): string {\n const colonHex = compact.f.replace(/(..)/g, '$1:').slice(0, -1);\n return `${[\n 'v=0',\n 'o=- 0 2 IN IP4 0.0.0.0',\n 's=-',\n 't=0 0',\n `a=group:BUNDLE ${compact.m}`,\n 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',\n 'c=IN IP4 0.0.0.0',\n `a=ice-ufrag:${compact.u}`,\n `a=ice-pwd:${compact.p}`,\n `a=fingerprint:sha-256 ${colonHex}`,\n `a=setup:${compact.s}`,\n `a=mid:${compact.m}`,\n `a=sctp-port:${compact.sp ?? 5000}`,\n ...(compact.mm ? [`a=max-message-size:${compact.mm}`] : []),\n ...compact.c.map((line) => `a=candidate:${line}`),\n '',\n ].join('\\r\\n')}`;\n}\n","import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport { canonicalSdp } from './_sdp';\nimport { MeshUnsupportedError } from './errors';\n\n/**\n * Proof of invitation possession: HMAC-SHA-256 of the guest SDP keyed by the\n * invitation secret. The SDP is canonicalized first so the proof is stable\n * across pairing codecs that rebuild the description (see `mq2.`). The host\n * recomputes the proof without receiving the secret back.\n *\n * WebCrypto is required: a non-cryptographic fallback would make the proof\n * forgeable, which defeats its purpose. Environments without `SubtleCrypto`\n * (insecure contexts) must pair over `https://` or `localhost` instead.\n *\n * @throws {MeshUnsupportedError} When `SubtleCrypto` is unavailable.\n */\nexport async function createProof(secret: string, sdp: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new MeshUnsupportedError('WebCrypto is unavailable: pairing requires a secure context');\n }\n const encoder = new TextEncoder();\n const key = await subtle.importKey('raw', encoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);\n const signature = await subtle.sign('HMAC', key, encoder.encode(canonicalSdp(sdp)));\n return bytesToBase64Url(new Uint8Array(signature));\n}\n","import { MeshConnectionError, MeshDisposedError, MeshTimeoutError, MeshUnsupportedError } from './errors';\nimport type { MeshRtcFactory, RTCDataChannelLike, RTCPeerConnectionLike } from './types';\n\n/**\n * Wraps `globalThis.RTCPeerConnection`. Evaluated lazily at first use so\n * importing the package never throws in non-WebRTC environments.\n */\nexport const nativeRtcFactory: MeshRtcFactory = {\n createPeerConnection(config) {\n const Ctor = (globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }).RTCPeerConnection;\n if (!Ctor) {\n throw new MeshUnsupportedError('WebRTC is unavailable in this environment: inject an `rtc` factory');\n }\n return new Ctor(config);\n },\n};\n\n/**\n * Textareas, clipboards, and share sheets normalize CRLF→LF;\n * `setRemoteDescription` requires CRLF line endings.\n */\nexport function normalizeSdp(sdp: string): string {\n const normalized = sdp.replace(/\\r?\\n/g, '\\r\\n');\n return normalized.endsWith('\\r\\n') ? normalized : `${normalized}\\r\\n`;\n}\n\n/**\n * Resolves when ICE gathering finishes or after `timeoutMs`: the caller\n * proceeds with whatever candidates were gathered (non-trickle semantics).\n */\nexport function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve();\n return new Promise((resolve) => {\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener('icegatheringstatechange', onChange);\n };\n const onChange = () => {\n if (pc.iceGatheringState === 'complete') {\n cleanup();\n resolve();\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, timeoutMs);\n pc.addEventListener('icegatheringstatechange', onChange);\n });\n}\n\n/**\n * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after\n * `timeoutMs`, or when the channel closes while that deadline is still\n * pending, since a peer tearing down a never-opened channel races the same\n * deadline: `MeshConnectionError` when the ICE transport dies first, and\n * `MeshDisposedError` when `disposalSignal` aborts.\n */\nexport function waitChannelOpen(\n dc: RTCDataChannelLike,\n pc: RTCPeerConnectionLike,\n timeoutMs: number,\n peerId: string,\n disposalSignal: AbortSignal,\n): Promise<void> {\n if (dc.readyState === 'open') return Promise.resolve();\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const timeout = () => new MeshTimeoutError(`Timed out waiting ${timeoutMs}ms for the data channel to open`);\n const fail = (error: Error) => {\n cleanup();\n reject(error);\n };\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n dc.removeEventListener('open', onOpen);\n dc.removeEventListener('close', onClose);\n pc.removeEventListener('iceconnectionstatechange', onIce);\n disposalSignal.removeEventListener('abort', onDispose);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n // A close while our open deadline is still armed is the timeout it\n // effectively is; mirror the guest's open-timer guard so both ends agree\n // on the cause instead of racing to report whichever event landed first.\n const onClose = () => fail(timeout());\n const onIce = () => {\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed' || state === 'disconnected') {\n fail(new MeshConnectionError(`ICE connection ${state} before channel open`, peerId));\n }\n };\n const onDispose = () => fail(new MeshDisposedError());\n timer = setTimeout(() => fail(timeout()), timeoutMs);\n dc.addEventListener('open', onOpen);\n dc.addEventListener('close', onClose);\n pc.addEventListener('iceconnectionstatechange', onIce);\n disposalSignal.addEventListener('abort', onDispose, { once: true });\n });\n}\n","import {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, type MeshError, MeshPairingError, MeshTimeoutError } from './errors';\nimport type {\n MeshGuest,\n MeshGuestOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\n/**\n * Create a guest mesh node. The guest pairs with one host by consuming an\n * out-of-band invitation and returning an answer.\n */\nexport function createMeshGuest<P extends MeshProtocol>(options: MeshGuestOptions = {}): MeshGuest<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n let hostPeer: PeerRecord | null = null;\n let openTimer: ReturnType<typeof setTimeout> | null = null;\n\n const openTimeoutError = (): MeshTimeoutError =>\n new MeshTimeoutError(`Timed out waiting ${channelOpenTimeoutMs}ms for the data channel to open`);\n\n function failPeer(peer: PeerRecord, error: MeshError): void {\n // Ignore events from a superseded peer after re-pairing replaced hostPeer.\n if (core.disposed || peer !== hostPeer || peer.status === 'failed') return;\n peer.status = 'failed';\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n // Teardown is deferred a macrotask so a peer racing its own open\n // deadline reports its timeout rather than this hangup.\n setTimeout(() => {\n peer.dc?.close();\n peer.pc?.close();\n }, 0);\n core.emitTap({ peerId: peer.id, status: 'failed', type: 'peer-status-change' });\n core.emitTap({ error, type: 'error' });\n core.emitTap({ peer: peer.publicPeer, reason: error.message, type: 'peer-left' });\n core.setStatus('failed');\n }\n\n function disconnectPeer(peer: PeerRecord, reason: string): void {\n if (core.disposed || peer !== hostPeer) return;\n if (peer.status === 'disconnected' || peer.status === 'failed') return;\n peer.status = 'disconnected';\n peer.pc?.close();\n core.emitTap({ peerId: peer.id, status: 'disconnected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n core.setStatus('disconnected');\n }\n\n function wireChannel(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.dc = dc;\n\n dc.addEventListener('open', () => {\n if (core.disposed) return;\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n peer.status = 'connected';\n core.emitTap({ peerId: peer.id, status: 'connected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, type: 'peer-joined' });\n core.setStatus('connected');\n });\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n if (peer.status === 'connecting')\n failPeer(\n peer,\n openTimer ? openTimeoutError() : new MeshConnectionError('channel closed before opening', peer.id),\n );\n else disconnectPeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n failPeer(peer, new MeshConnectionError('channel error', peer.id));\n });\n\n // The ice-state emission lives on the pairing-scoped listener above; this\n // one owns channel-lifecycle transitions only, so each ICE change emits\n // exactly one ice-state event.\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed') {\n failPeer(peer, new MeshConnectionError(`ice ${state}`, peer.id));\n } else if (state === 'disconnected' && peer.status === 'connected') {\n disconnectPeer(peer, 'ice disconnected');\n }\n });\n }\n\n const guest: MeshGuest<P> = {\n async acceptInvitation(invitation, meta) {\n core.ensureLive();\n // A terminal host peer (failed/disconnected) may be replaced by a fresh\n // pairing on the same node: listeners stay attached and keep working.\n if (hostPeer && hostPeer.status !== 'failed' && hostPeer.status !== 'disconnected') {\n throw new MeshPairingError('Guest is already paired');\n }\n if (invitation?.v !== 1) throw new MeshPairingError('Unsupported invitation version');\n if (clock() >= invitation.expiresAt) throw new MeshPairingError('Invitation expired');\n\n if (hostPeer) {\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n hostPeer.pc?.close();\n hostPeer = null;\n }\n\n core.setStatus('pairing');\n let pc: RTCPeerConnectionLike;\n try {\n pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n } catch (error) {\n core.setStatus('idle');\n throw error;\n }\n\n // The invitation carries no host identity beyond an optional display\n // name: the session id doubles as the host-side peer id for inbound\n // metadata and tap events.\n const peer = createPeerRecord(invitation.sessionId, invitation.hostName, 'host');\n peer.pc = pc;\n hostPeer = peer;\n\n pc.addEventListener('datachannel', (event) => {\n wireChannel(peer, pc, event.channel);\n });\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n core.emitTap({ peerId: peer.id, state: pc.iceConnectionState, type: 'ice-state' });\n });\n\n try {\n await pc.setRemoteDescription({ sdp: normalizeSdp(invitation.sdp), type: 'offer' });\n } catch (cause) {\n hostPeer = null;\n core.setStatus('idle');\n throw new MeshPairingError('Invalid invitation SDP', { cause });\n }\n\n let answerSdp: string;\n try {\n await pc.setLocalDescription(await pc.createAnswer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n answerSdp = pc.localDescription?.sdp ?? '';\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Failed to create answer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n if (!answerSdp) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const peerId = randomId(options.random);\n let proof: string;\n try {\n proof = await createProof(invitation.secret, answerSdp);\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw cause;\n }\n core.setStatus('connecting');\n\n // If the channel never opens (host never accepts the answer), fail loudly\n // instead of hanging in 'connecting' forever. The deadline must cover\n // the human carry-back of the answer, not just machine negotiation.\n openTimer = setTimeout(() => {\n if (hostPeer?.status === 'connecting') {\n failPeer(hostPeer, openTimeoutError());\n }\n }, channelOpenTimeoutMs);\n\n return {\n peer: meta?.name === undefined ? { id: peerId } : { id: peerId, name: meta.name },\n proof,\n sdp: answerSdp,\n sessionId: invitation.sessionId,\n v: 1,\n };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n if (hostPeer) {\n hostPeer.dc?.close();\n hostPeer.pc?.close();\n }\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n get host(): MeshPeer | null {\n return hostPeer?.publicPeer ?? null;\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n\n send(type, payload) {\n core.ensureLive();\n if (!hostPeer) throw new MeshConnectionError('Guest is not paired');\n messenger.sendTo(hostPeer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return guest;\n}\n","import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_INVITATION_TTL_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomBytes, randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitChannelOpen, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, MeshPairingError } from './errors';\nimport type {\n MeshHost,\n MeshHostOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n MeshStatus,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\ninterface PendingInvitation {\n answered: boolean;\n readonly dc: RTCDataChannelLike;\n readonly expiresAt: number;\n readonly expiryTimer: ReturnType<typeof setTimeout>;\n readonly pc: RTCPeerConnectionLike;\n readonly secret: string;\n readonly sessionId: string;\n}\n\n/**\n * Create a host-authoritative mesh node. The host holds one peer connection\n * per guest and pairs each one through a manual invitation/answer exchange.\n */\nexport function createMeshHost<P extends MeshProtocol>(options: MeshHostOptions = {}): MeshHost<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const invitationTtlMs = options.invitationTtlMs ?? DEFAULT_INVITATION_TTL_MS;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n const channelInit = { label: 'mesh', ordered: true, ...options.channel };\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n const peers = new Map<string, PeerRecord>();\n const pending = new Map<string, PendingInvitation>();\n\n let sawPeer = false;\n let sawFailure = false;\n\n function recomputeStatus(): void {\n let next: MeshStatus = pending.size > 0 ? 'pairing' : sawPeer ? 'disconnected' : sawFailure ? 'failed' : 'idle';\n for (const peer of peers.values()) {\n if (peer.status === 'connected') {\n next = 'connected';\n break;\n }\n if (peer.status === 'connecting') next = 'connecting';\n }\n core.setStatus(next);\n }\n\n function expireInvitation(sessionId: string): void {\n const invitation = pending.get(sessionId);\n if (!invitation) return;\n pending.delete(sessionId);\n clearTimeout(invitation.expiryTimer);\n invitation.pc.close();\n core.emitTap({ sessionId, type: 'invitation-expired' });\n recomputeStatus();\n }\n\n function setPeerStatus(peer: PeerRecord, status: PeerRecord['status']): void {\n if (peer.status === status) return;\n peer.status = status;\n core.emitTap({ peerId: peer.id, status, type: 'peer-status-change' });\n recomputeStatus();\n }\n\n function leavePeer(peer: PeerRecord, reason: string | undefined, failed = false): void {\n if (!peers.has(peer.id)) return;\n peers.delete(peer.id);\n peer.status = failed ? 'failed' : 'disconnected';\n peer.pc?.close();\n peer.dc?.close();\n core.emitTap({ peerId: peer.id, status: peer.status, type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n if (!failed) sawPeer = true;\n sawFailure = sawFailure || failed;\n recomputeStatus();\n }\n\n function wirePeerTransport(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.pc = pc;\n peer.dc = dc;\n\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n leavePeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n leavePeer(peer, 'channel error', true);\n });\n\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n core.emitTap({ peerId: peer.id, state, type: 'ice-state' });\n if (state === 'failed' || state === 'closed') {\n leavePeer(peer, `ice ${state}`, state === 'failed');\n } else if (state === 'disconnected' && peer.status === 'connected') {\n setPeerStatus(peer, 'disconnected');\n }\n });\n }\n\n function markPeerConnected(peer: PeerRecord): void {\n sawPeer = true;\n peers.set(peer.id, peer);\n setPeerStatus(peer, 'connected');\n core.emitTap({ peer, type: 'peer-joined' });\n }\n\n const host: MeshHost<P> = {\n async acceptAnswer(answer) {\n core.ensureLive();\n\n if (answer?.v !== 1) throw new MeshPairingError('Unsupported answer version');\n if (!answer.peer?.id) throw new MeshPairingError('Answer is missing a peer id');\n const invitation = pending.get(answer.sessionId);\n if (!invitation) throw new MeshPairingError('Unknown or expired session');\n if (invitation.answered) throw new MeshPairingError('Invitation already answered');\n if (peers.has(answer.peer.id)) throw new MeshPairingError(`Peer \"${answer.peer.id}\" is already connected`);\n if (clock() >= invitation.expiresAt) {\n expireInvitation(answer.sessionId);\n throw new MeshPairingError('Invitation expired');\n }\n\n const expected = await createProof(invitation.secret, answer.sdp);\n if (answer.proof !== expected) throw new MeshPairingError('Proof mismatch');\n\n const approved = (await options.approvePeer?.(answer.peer)) ?? true;\n if (!approved) {\n core.emitTap({ peerId: answer.peer.id, type: 'peer-rejected' });\n throw new MeshPairingError(`Peer \"${answer.peer.id}\" rejected by host`);\n }\n core.emitTap({ peerId: answer.peer.id, type: 'peer-approved' });\n\n // Re-check after the proof/approval awaits: concurrent answers for\n // different sessions could both have passed the check above.\n if (peers.has(answer.peer.id)) throw new MeshPairingError(`Peer \"${answer.peer.id}\" is already connected`);\n\n invitation.answered = true;\n pending.delete(answer.sessionId);\n clearTimeout(invitation.expiryTimer);\n\n const peer = createPeerRecord(answer.peer.id, answer.peer.name, 'guest');\n const opened = waitChannelOpen(\n invitation.dc,\n invitation.pc,\n channelOpenTimeoutMs,\n peer.id,\n core.disposalCtrl.signal,\n );\n wirePeerTransport(peer, invitation.pc, invitation.dc);\n peers.set(peer.id, peer);\n recomputeStatus();\n\n try {\n await invitation.pc.setRemoteDescription({ sdp: normalizeSdp(answer.sdp), type: 'answer' });\n } catch (cause) {\n leavePeer(peer, 'invalid answer sdp', true);\n throw new MeshPairingError('Invalid answer SDP', { cause });\n }\n\n try {\n await opened;\n } catch (error) {\n const failed = !(error instanceof MeshDisposedError);\n leavePeer(peer, error instanceof Error ? error.message : 'open failed', failed);\n throw error;\n }\n\n markPeerConnected(peer);\n return peer.publicPeer;\n },\n\n broadcast(type, payload, broadcastOptions) {\n core.ensureLive();\n for (const peer of peers.values()) {\n if (peer.status !== 'connected' || broadcastOptions?.except?.includes(peer.id)) continue;\n messenger.sendTo(peer, type, payload);\n }\n },\n\n async createInvitation(meta) {\n core.ensureLive();\n\n const pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n const dc = pc.createDataChannel(channelInit.label, {\n maxRetransmits: channelInit.maxRetransmits,\n ordered: channelInit.ordered,\n });\n\n const sessionId = randomId(options.random);\n const secret = bytesToBase64Url(randomBytes(32, options.random));\n const expiresAt = clock() + invitationTtlMs;\n\n try {\n await pc.setLocalDescription(await pc.createOffer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n } catch (cause) {\n pc.close();\n throw new MeshConnectionError('Failed to create offer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n\n const sdp = pc.localDescription?.sdp;\n if (!sdp) {\n pc.close();\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const expiryTimer = setTimeout(() => expireInvitation(sessionId), invitationTtlMs);\n pending.set(sessionId, { answered: false, dc, expiresAt, expiryTimer, pc, secret, sessionId });\n core.emitTap({ sessionId, type: 'invitation-created' });\n recomputeStatus();\n return { expiresAt, sdp, secret, sessionId, v: 1, ...(meta?.name === undefined ? {} : { hostName: meta.name }) };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n for (const invitation of pending.values()) {\n clearTimeout(invitation.expiryTimer);\n invitation.pc.close();\n }\n pending.clear();\n for (const peer of peers.values()) {\n peer.dc?.close();\n peer.pc?.close();\n }\n peers.clear();\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n\n kick(peerId, reason) {\n core.ensureLive();\n const peer = peers.get(peerId);\n if (!peer) throw new MeshConnectionError(`Unknown peer \"${peerId}\"`, peerId);\n leavePeer(peer, reason ?? 'kicked');\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n get peers() {\n return peers as ReadonlyMap<string, MeshPeer>;\n },\n\n send(peerId, type, payload) {\n core.ensureLive();\n const peer = peers.get(peerId);\n if (!peer) throw new MeshConnectionError(`Unknown peer \"${peerId}\"`, peerId);\n messenger.sendTo(peer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return host;\n}\n","import { MeshPairingError } from './errors';\n\n/**\n * Base45 (RFC 9285): two bytes → three chars, one leftover byte → two chars.\n * The alphabet sits inside the QR alphanumeric charset, so QR encoders can use\n * alphanumeric mode (~5.5 bits/char) instead of byte mode: about a third\n * denser than base64 for the same payload.\n */\n\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';\nconst INDEX = new Map([...ALPHABET].map((ch, i) => [ch, i]));\n\nexport function bytesToBase45(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i + 1 < bytes.length; i += 2) {\n const value = bytes[i] * 256 + bytes[i + 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45) % 45] + ALPHABET[Math.floor(value / 2025)];\n }\n if (bytes.length % 2) {\n const value = bytes[bytes.length - 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45)];\n }\n return out;\n}\n\nexport function base45ToBytes(text: string): Uint8Array {\n const out: number[] = [];\n let i = 0;\n while (i + 2 < text.length) {\n const value = digit(text, i) + 45 * digit(text, i + 1) + 2025 * digit(text, i + 2);\n if (value > 0xffff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value >> 8, value & 0xff);\n i += 3;\n }\n if (text.length - i === 2) {\n const value = digit(text, i) + 45 * digit(text, i + 1);\n if (value > 0xff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value);\n } else if (text.length - i === 1) {\n throw new MeshPairingError('Malformed base45 payload');\n }\n return new Uint8Array(out);\n}\n\nfunction digit(text: string, at: number): number {\n const value = INDEX.get(text[at]);\n if (value === undefined) throw new MeshPairingError('Malformed base45 payload');\n return value;\n}\n","import { base45ToBytes, bytesToBase45 } from './_base45';\nimport { compactSdp, restoreSdp } from './_sdp';\nimport { isRecord, meshCodec, parsePairingPayload } from './codec';\nimport { MeshPairingError, MeshUnsupportedError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\ninterface QrCodec {\n decode(text: string): Promise<MeshInvitation | MeshAnswer>;\n encode(payload: MeshInvitation | MeshAnswer): Promise<string>;\n}\n\n/**\n * QR-friendly async codec. `encode` emits `\"mq2.\"` payloads: the SDP is\n * reduced to the fields the remote needs (see `_sdp.ts`), the JSON is\n * deflate-raw compressed (`CompressionStream`), and the bytes are base45 :\n * an alphabet inside the QR alphanumeric charset, so encoders use ~5.5\n * bits/char instead of 8. Where `CompressionStream` is unavailable it\n * falls back to plain `meshCodec` output, which `decode` also accepts :\n * encoding never fails on a missing capability.\n *\n * `decode` accepts `\"mq2.\"` and plain `meshCodec` output: a guest can paste\n * or scan interchangeably.\n */\n\nconst PREFIX_V2 = 'mq2.';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nfunction decompressionStream(kind: 'deflate-raw'): DecompressionStream {\n if (typeof globalThis.DecompressionStream !== 'function')\n throw new MeshUnsupportedError('DecompressionStream is not available in this environment');\n return new globalThis.DecompressionStream(kind);\n}\n\nasync function pump(\n data: Uint8Array,\n stream: {\n readable: ReadableStream<Uint8Array>;\n writable: WritableStream<BufferSource>;\n },\n): Promise<Uint8Array> {\n const writer = stream.writable.getWriter();\n // When the read side fails first (corrupt input), the write side rejects too;\n // swallow it so the real read error is the one that surfaces.\n const write = writer\n .write(data as BufferSource)\n .then(() => writer.close())\n .catch(() => {});\n const chunks: Uint8Array[] = [];\n const reader = stream.readable.getReader();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n await write;\n const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport const meshQrCodec: QrCodec = {\n async decode(text) {\n const trimmed = text.trim();\n if (trimmed.startsWith(PREFIX_V2)) {\n let parsed: unknown;\n try {\n const compressed = base45ToBytes(trimmed.slice(PREFIX_V2.length));\n parsed = JSON.parse(decoder.decode(await pump(compressed, decompressionStream('deflate-raw'))));\n } catch (cause) {\n if (cause instanceof MeshUnsupportedError) throw cause;\n if (cause instanceof MeshPairingError) throw cause;\n throw new MeshPairingError('Malformed pairing payload', { cause });\n }\n if (isRecord(parsed)) parsed = { ...parsed, sdp: restoreSdp(parsed.sdp) };\n return parsePairingPayload(parsed);\n }\n // Plain meshCodec output: paste and scan stay interchangeable.\n return meshCodec.decode(trimmed);\n },\n async encode(payload) {\n // Missing capability, not a failure: plain output still pairs everywhere.\n if (typeof globalThis.CompressionStream !== 'function') return meshCodec.encode(payload);\n const compact = compactSdp(payload.sdp);\n const json = encoder.encode(JSON.stringify(compact ? { ...payload, sdp: compact } : payload));\n const compressed = await pump(json, new globalThis.CompressionStream('deflate-raw'));\n return PREFIX_V2 + bytesToBase45(compressed);\n },\n};\n"],"mappings":"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"}
|
package/dist/qr-codec.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr-codec.cjs","names":[],"sources":["../src/qr-codec.ts"],"sourcesContent":["import { base45ToBytes, bytesToBase45 } from './_base45';\nimport { compactSdp, restoreSdp } from './_sdp';\nimport { isRecord, meshCodec, parsePairingPayload } from './codec';\nimport { MeshPairingError, MeshUnsupportedError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\ninterface QrCodec {\n decode(text: string): Promise<MeshInvitation | MeshAnswer>;\n encode(payload: MeshInvitation | MeshAnswer): Promise<string>;\n}\n\n/**\n * QR-friendly async codec. `encode` emits `\"mq2.\"` payloads: the SDP is\n * reduced to the fields the remote needs (see `_sdp.ts`), the JSON is\n * deflate-raw compressed (`CompressionStream`), and the bytes are base45
|
|
1
|
+
{"version":3,"file":"qr-codec.cjs","names":[],"sources":["../src/qr-codec.ts"],"sourcesContent":["import { base45ToBytes, bytesToBase45 } from './_base45';\nimport { compactSdp, restoreSdp } from './_sdp';\nimport { isRecord, meshCodec, parsePairingPayload } from './codec';\nimport { MeshPairingError, MeshUnsupportedError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\ninterface QrCodec {\n decode(text: string): Promise<MeshInvitation | MeshAnswer>;\n encode(payload: MeshInvitation | MeshAnswer): Promise<string>;\n}\n\n/**\n * QR-friendly async codec. `encode` emits `\"mq2.\"` payloads: the SDP is\n * reduced to the fields the remote needs (see `_sdp.ts`), the JSON is\n * deflate-raw compressed (`CompressionStream`), and the bytes are base45 :\n * an alphabet inside the QR alphanumeric charset, so encoders use ~5.5\n * bits/char instead of 8. Where `CompressionStream` is unavailable it\n * falls back to plain `meshCodec` output, which `decode` also accepts :\n * encoding never fails on a missing capability.\n *\n * `decode` accepts `\"mq2.\"` and plain `meshCodec` output: a guest can paste\n * or scan interchangeably.\n */\n\nconst PREFIX_V2 = 'mq2.';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nfunction decompressionStream(kind: 'deflate-raw'): DecompressionStream {\n if (typeof globalThis.DecompressionStream !== 'function')\n throw new MeshUnsupportedError('DecompressionStream is not available in this environment');\n return new globalThis.DecompressionStream(kind);\n}\n\nasync function pump(\n data: Uint8Array,\n stream: {\n readable: ReadableStream<Uint8Array>;\n writable: WritableStream<BufferSource>;\n },\n): Promise<Uint8Array> {\n const writer = stream.writable.getWriter();\n // When the read side fails first (corrupt input), the write side rejects too;\n // swallow it so the real read error is the one that surfaces.\n const write = writer\n .write(data as BufferSource)\n .then(() => writer.close())\n .catch(() => {});\n const chunks: Uint8Array[] = [];\n const reader = stream.readable.getReader();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n await write;\n const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport const meshQrCodec: QrCodec = {\n async decode(text) {\n const trimmed = text.trim();\n if (trimmed.startsWith(PREFIX_V2)) {\n let parsed: unknown;\n try {\n const compressed = base45ToBytes(trimmed.slice(PREFIX_V2.length));\n parsed = JSON.parse(decoder.decode(await pump(compressed, decompressionStream('deflate-raw'))));\n } catch (cause) {\n if (cause instanceof MeshUnsupportedError) throw cause;\n if (cause instanceof MeshPairingError) throw cause;\n throw new MeshPairingError('Malformed pairing payload', { cause });\n }\n if (isRecord(parsed)) parsed = { ...parsed, sdp: restoreSdp(parsed.sdp) };\n return parsePairingPayload(parsed);\n }\n // Plain meshCodec output: paste and scan stay interchangeable.\n return meshCodec.decode(trimmed);\n },\n async encode(payload) {\n // Missing capability, not a failure: plain output still pairs everywhere.\n if (typeof globalThis.CompressionStream !== 'function') return meshCodec.encode(payload);\n const compact = compactSdp(payload.sdp);\n const json = encoder.encode(JSON.stringify(compact ? { ...payload, sdp: compact } : payload));\n const compressed = await pump(json, new globalThis.CompressionStream('deflate-raw'));\n return PREFIX_V2 + bytesToBase45(compressed);\n },\n};\n"],"mappings":"4GAwBA,IAAM,EAAY,OAEZ,EAAU,IAAI,YACd,EAAU,IAAI,YAEpB,SAAS,oBAAoB,EAA0C,CACrE,GAAI,OAAO,WAAW,qBAAwB,WAC5C,MAAM,IAAI,EAAA,qBAAqB,0DAA0D,EAC3F,OAAO,IAAI,WAAW,oBAAoB,CAAI,CAChD,CAEA,eAAe,KACb,EACA,EAIqB,CACrB,IAAM,EAAS,EAAO,SAAS,UAAU,EAGnC,EAAQ,EACX,MAAM,CAAoB,CAAC,CAC3B,SAAW,EAAO,MAAM,CAAC,CAAC,CAC1B,UAAY,CAAC,CAAC,EACX,EAAuB,CAAC,EACxB,EAAS,EAAO,SAAS,UAAU,EACzC,OAAS,CACP,GAAM,CAAE,OAAM,SAAU,MAAM,EAAO,KAAK,EAC1C,GAAI,EAAM,MACV,EAAO,KAAK,CAAK,CACnB,CACA,MAAM,EACN,IAAM,EAAM,IAAI,WAAW,EAAO,QAAQ,EAAG,IAAM,EAAI,EAAE,OAAQ,CAAC,CAAC,EAC/D,EAAS,EACb,IAAK,IAAM,KAAS,EAClB,EAAI,IAAI,EAAO,CAAM,EACrB,GAAU,EAAM,OAElB,OAAO,CACT,CAEA,IAAa,EAAuB,CAClC,MAAM,OAAO,EAAM,CACjB,IAAM,EAAU,EAAK,KAAK,EAC1B,GAAI,EAAQ,WAAW,CAAS,EAAG,CACjC,IAAI,EACJ,GAAI,CACF,IAAM,EAAa,EAAA,cAAc,EAAQ,MAAM,CAAgB,CAAC,EAChE,EAAS,KAAK,MAAM,EAAQ,OAAO,MAAM,KAAK,EAAY,oBAAoB,aAAa,CAAC,CAAC,CAAC,CAChG,OAAS,EAAO,CAGd,MAFI,aAAiB,EAAA,sBACjB,aAAiB,EAAA,iBAAwB,EACvC,IAAI,EAAA,iBAAiB,4BAA6B,CAAE,OAAM,CAAC,CACnE,CAEA,OADI,EAAA,SAAS,CAAM,IAAG,EAAS,CAAE,GAAG,EAAQ,IAAK,EAAA,WAAW,EAAO,GAAG,CAAE,GACjE,EAAA,oBAAoB,CAAM,CACnC,CAEA,OAAO,EAAA,UAAU,OAAO,CAAO,CACjC,EACA,MAAM,OAAO,EAAS,CAEpB,GAAI,OAAO,WAAW,mBAAsB,WAAY,OAAO,EAAA,UAAU,OAAO,CAAO,EACvF,IAAM,EAAU,EAAA,WAAW,EAAQ,GAAG,EAEhC,EAAa,MAAM,KADZ,EAAQ,OAAO,KAAK,UAAU,EAAU,CAAE,GAAG,EAAS,IAAK,CAAQ,EAAI,CAAO,CAC7D,EAAM,IAAI,WAAW,kBAAkB,aAAa,CAAC,EACnF,OAAO,EAAY,EAAA,cAAc,CAAU,CAC7C,CACF"}
|
package/dist/qr-codec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr-codec.js","names":[],"sources":["../src/qr-codec.ts"],"sourcesContent":["import { base45ToBytes, bytesToBase45 } from './_base45';\nimport { compactSdp, restoreSdp } from './_sdp';\nimport { isRecord, meshCodec, parsePairingPayload } from './codec';\nimport { MeshPairingError, MeshUnsupportedError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\ninterface QrCodec {\n decode(text: string): Promise<MeshInvitation | MeshAnswer>;\n encode(payload: MeshInvitation | MeshAnswer): Promise<string>;\n}\n\n/**\n * QR-friendly async codec. `encode` emits `\"mq2.\"` payloads: the SDP is\n * reduced to the fields the remote needs (see `_sdp.ts`), the JSON is\n * deflate-raw compressed (`CompressionStream`), and the bytes are base45
|
|
1
|
+
{"version":3,"file":"qr-codec.js","names":[],"sources":["../src/qr-codec.ts"],"sourcesContent":["import { base45ToBytes, bytesToBase45 } from './_base45';\nimport { compactSdp, restoreSdp } from './_sdp';\nimport { isRecord, meshCodec, parsePairingPayload } from './codec';\nimport { MeshPairingError, MeshUnsupportedError } from './errors';\nimport type { MeshAnswer, MeshInvitation } from './types';\n\ninterface QrCodec {\n decode(text: string): Promise<MeshInvitation | MeshAnswer>;\n encode(payload: MeshInvitation | MeshAnswer): Promise<string>;\n}\n\n/**\n * QR-friendly async codec. `encode` emits `\"mq2.\"` payloads: the SDP is\n * reduced to the fields the remote needs (see `_sdp.ts`), the JSON is\n * deflate-raw compressed (`CompressionStream`), and the bytes are base45 :\n * an alphabet inside the QR alphanumeric charset, so encoders use ~5.5\n * bits/char instead of 8. Where `CompressionStream` is unavailable it\n * falls back to plain `meshCodec` output, which `decode` also accepts :\n * encoding never fails on a missing capability.\n *\n * `decode` accepts `\"mq2.\"` and plain `meshCodec` output: a guest can paste\n * or scan interchangeably.\n */\n\nconst PREFIX_V2 = 'mq2.';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nfunction decompressionStream(kind: 'deflate-raw'): DecompressionStream {\n if (typeof globalThis.DecompressionStream !== 'function')\n throw new MeshUnsupportedError('DecompressionStream is not available in this environment');\n return new globalThis.DecompressionStream(kind);\n}\n\nasync function pump(\n data: Uint8Array,\n stream: {\n readable: ReadableStream<Uint8Array>;\n writable: WritableStream<BufferSource>;\n },\n): Promise<Uint8Array> {\n const writer = stream.writable.getWriter();\n // When the read side fails first (corrupt input), the write side rejects too;\n // swallow it so the real read error is the one that surfaces.\n const write = writer\n .write(data as BufferSource)\n .then(() => writer.close())\n .catch(() => {});\n const chunks: Uint8Array[] = [];\n const reader = stream.readable.getReader();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n await write;\n const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport const meshQrCodec: QrCodec = {\n async decode(text) {\n const trimmed = text.trim();\n if (trimmed.startsWith(PREFIX_V2)) {\n let parsed: unknown;\n try {\n const compressed = base45ToBytes(trimmed.slice(PREFIX_V2.length));\n parsed = JSON.parse(decoder.decode(await pump(compressed, decompressionStream('deflate-raw'))));\n } catch (cause) {\n if (cause instanceof MeshUnsupportedError) throw cause;\n if (cause instanceof MeshPairingError) throw cause;\n throw new MeshPairingError('Malformed pairing payload', { cause });\n }\n if (isRecord(parsed)) parsed = { ...parsed, sdp: restoreSdp(parsed.sdp) };\n return parsePairingPayload(parsed);\n }\n // Plain meshCodec output: paste and scan stay interchangeable.\n return meshCodec.decode(trimmed);\n },\n async encode(payload) {\n // Missing capability, not a failure: plain output still pairs everywhere.\n if (typeof globalThis.CompressionStream !== 'function') return meshCodec.encode(payload);\n const compact = compactSdp(payload.sdp);\n const json = encoder.encode(JSON.stringify(compact ? { ...payload, sdp: compact } : payload));\n const compressed = await pump(json, new globalThis.CompressionStream('deflate-raw'));\n return PREFIX_V2 + bytesToBase45(compressed);\n },\n};\n"],"mappings":"8QAwBA,IAAM,EAAY,OAEZ,EAAU,IAAI,YACd,EAAU,IAAI,YAEpB,SAAS,oBAAoB,EAA0C,CACrE,GAAI,OAAO,WAAW,qBAAwB,WAC5C,MAAM,IAAI,EAAqB,0DAA0D,EAC3F,OAAO,IAAI,WAAW,oBAAoB,CAAI,CAChD,CAEA,eAAe,KACb,EACA,EAIqB,CACrB,IAAM,EAAS,EAAO,SAAS,UAAU,EAGnC,EAAQ,EACX,MAAM,CAAoB,CAAC,CAC3B,SAAW,EAAO,MAAM,CAAC,CAAC,CAC1B,UAAY,CAAC,CAAC,EACX,EAAuB,CAAC,EACxB,EAAS,EAAO,SAAS,UAAU,EACzC,OAAS,CACP,GAAM,CAAE,OAAM,SAAU,MAAM,EAAO,KAAK,EAC1C,GAAI,EAAM,MACV,EAAO,KAAK,CAAK,CACnB,CACA,MAAM,EACN,IAAM,EAAM,IAAI,WAAW,EAAO,QAAQ,EAAG,IAAM,EAAI,EAAE,OAAQ,CAAC,CAAC,EAC/D,EAAS,EACb,IAAK,IAAM,KAAS,EAClB,EAAI,IAAI,EAAO,CAAM,EACrB,GAAU,EAAM,OAElB,OAAO,CACT,CAEA,IAAa,EAAuB,CAClC,MAAM,OAAO,EAAM,CACjB,IAAM,EAAU,EAAK,KAAK,EAC1B,GAAI,EAAQ,WAAW,CAAS,EAAG,CACjC,IAAI,EACJ,GAAI,CACF,IAAM,EAAa,EAAc,EAAQ,MAAM,CAAgB,CAAC,EAChE,EAAS,KAAK,MAAM,EAAQ,OAAO,MAAM,KAAK,EAAY,oBAAoB,aAAa,CAAC,CAAC,CAAC,CAChG,OAAS,EAAO,CAGd,MAFI,aAAiB,GACjB,aAAiB,EAAwB,EACvC,IAAI,EAAiB,4BAA6B,CAAE,OAAM,CAAC,CACnE,CAEA,OADI,EAAS,CAAM,IAAG,EAAS,CAAE,GAAG,EAAQ,IAAK,EAAW,EAAO,GAAG,CAAE,GACjE,EAAoB,CAAM,CACnC,CAEA,OAAO,EAAU,OAAO,CAAO,CACjC,EACA,MAAM,OAAO,EAAS,CAEpB,GAAI,OAAO,WAAW,mBAAsB,WAAY,OAAO,EAAU,OAAO,CAAO,EACvF,IAAM,EAAU,EAAW,EAAQ,GAAG,EAEhC,EAAa,MAAM,KADZ,EAAQ,OAAO,KAAK,UAAU,EAAU,CAAE,GAAG,EAAS,IAAK,CAAQ,EAAI,CAAO,CAC7D,EAAM,IAAI,WAAW,kBAAkB,aAAa,CAAC,EACnF,OAAO,EAAY,EAAc,CAAU,CAC7C,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-rtc.cjs","names":[],"sources":["../../src/testing/fake-rtc.ts"],"sourcesContent":["import type {\n MeshRtcEvent,\n MeshRtcFactory,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n RTCSessionDescriptionLike,\n} from '../types';\n\ntype Listener = (event: MeshRtcEvent) => void;\n\nclass ListenerBag {\n private readonly listeners = new Map<string, Set<Listener>>();\n\n add(type: string, listener: Listener): void {\n let set = this.listeners.get(type);\n if (!set) {\n set = new Set();\n this.listeners.set(type, set);\n }\n set.add(listener);\n }\n\n remove(type: string, listener: Listener): void {\n this.listeners.get(type)?.delete(listener);\n }\n\n emit(type: string, event: MeshRtcEvent = {}): void {\n for (const listener of this.listeners.get(type) ?? []) listener(event);\n }\n\n // `never` accepts every listener shape declared by the `*Like` overloads.\n addEventListener(type: string, listener: (event: never) => void): void {\n this.add(type, listener as Listener);\n }\n\n removeEventListener(type: string, listener: (event: never) => void): void {\n this.remove(type, listener as Listener);\n }\n}\n\nexport interface FakeRtc {\n /** Close both ends of the link created at `index` (0-based, pairing order). */\n closeChannel(index: number): void;\n /** Silently drop the next `n` channel sends. */\n dropNext(n: number): void;\n /** Make the next pairing fail at the ICE stage. */\n failIce(): void;\n /** Make the next completed link connect at ICE level but never open the channel. */\n hangOpen(): void;\n /** Delivery delay for messages and channel-open in milliseconds. Default: 0 (microtask). */\n latencyMs: number;\n /** Number of links initiated (one per created offer). */\n readonly linkCount: number;\n /** Every SDP handed to `setRemoteDescription`, in call order. */\n readonly remoteSdps: readonly string[];\n readonly rtc: MeshRtcFactory;\n /** Make the next `setLocalDescription` never finish ICE gathering. */\n stallIce(): void;\n /** Drain all latency-queued deliveries immediately. */\n tick(): void;\n}\n\n/**\n * In-memory WebRTC stand-in for testing mesh nodes without a browser.\n * Peer connections created from the same fixture wire together through fake\n * SDP tokens — no network, no real ICE. Pass `fixture.rtc` as the `rtc`\n * option to `createMeshHost`/`createMeshGuest`.\n */\nexport function createFakeRtc(options: { latencyMs?: number } = {}): FakeRtc {\n interface FakeLink {\n completed: boolean;\n guestChannel: FakeDataChannel | null;\n guestPc: FakePeerConnection | null;\n hostChannel: FakeDataChannel | null;\n hostPc: FakePeerConnection;\n readonly hostToken: string;\n }\n\n const pendingDeliveries: Array<() => void> = [];\n const links = new Map<string, FakeLink>();\n const remoteSdps: string[] = [];\n let pcCounter = 0;\n let dropCount = 0;\n let failNextIce = false;\n let stallNextGathering = false;\n let hangNextOpen = false;\n\n const fixture: FakeRtc = {\n closeChannel(index) {\n const link = [...links.values()][index];\n link?.hostChannel?.close();\n link?.guestChannel?.close();\n if (link) {\n link.hostPc.iceConnectionState = 'disconnected';\n link.hostPc.emit('iceconnectionstatechange');\n if (link.guestPc) {\n link.guestPc.iceConnectionState = 'disconnected';\n link.guestPc.emit('iceconnectionstatechange');\n }\n }\n },\n dropNext(n) {\n dropCount += n;\n },\n failIce() {\n failNextIce = true;\n },\n hangOpen() {\n hangNextOpen = true;\n },\n latencyMs: options.latencyMs ?? 0,\n get linkCount() {\n return links.size;\n },\n get remoteSdps() {\n return remoteSdps;\n },\n rtc: {\n createPeerConnection: () => new FakePeerConnection(`pc${++pcCounter}`),\n },\n stallIce() {\n stallNextGathering = true;\n },\n tick() {\n while (pendingDeliveries.length) pendingDeliveries.shift()?.();\n },\n };\n\n function schedule(delivery: () => void): void {\n if (fixture.latencyMs > 0) {\n pendingDeliveries.push(delivery);\n setTimeout(fixture.tick, fixture.latencyMs);\n } else {\n queueMicrotask(delivery);\n }\n }\n\n class FakeDataChannel extends ListenerBag implements RTCDataChannelLike {\n readyState = 'connecting';\n remote: FakeDataChannel | null = null;\n\n send(data: string): void {\n if (this.readyState !== 'open') throw new Error(`Channel is ${this.readyState}`);\n if (dropCount > 0) {\n dropCount--;\n return;\n }\n const remote = this.remote;\n if (!remote || remote.readyState !== 'open') return;\n schedule(() => remote.emit('message', { data }));\n }\n\n open(): void {\n if (this.readyState !== 'connecting') return;\n this.readyState = 'open';\n this.emit('open');\n }\n\n close(): void {\n if (this.readyState === 'closed') return;\n this.readyState = 'closed';\n this.emit('close');\n const remote = this.remote;\n if (remote && remote.readyState !== 'closed') {\n remote.readyState = 'closed';\n remote.emit('close');\n }\n }\n }\n\n class FakePeerConnection extends ListenerBag implements RTCPeerConnectionLike {\n connectionState = 'new';\n iceConnectionState = 'new';\n iceGatheringState = 'new';\n localDescription: RTCSessionDescriptionLike | null = null;\n /** Last description received through `setRemoteDescription`, for assertions. */\n remoteDescription: RTCSessionDescriptionLike | null = null;\n pendingChannel: FakeDataChannel | null = null;\n closed = false;\n\n readonly token: string;\n\n constructor(token: string) {\n super();\n this.token = token;\n }\n\n createDataChannel(): RTCDataChannelLike {\n this.pendingChannel = new FakeDataChannel();\n return this.pendingChannel;\n }\n\n createOffer(): Promise<RTCSessionDescriptionLike> {\n links.set(this.token, {\n completed: false,\n guestChannel: null,\n guestPc: null,\n hostChannel: null,\n hostPc: this,\n hostToken: this.token,\n });\n return Promise.resolve({ sdp: `fake-offer:${this.token}`, type: 'offer' });\n }\n\n createAnswer(): Promise<RTCSessionDescriptionLike> {\n const link = this.linkFor();\n return Promise.resolve({\n sdp: `fake-answer:${link?.hostToken ?? 'none'}:guest:${this.token}`,\n type: 'answer',\n });\n }\n\n setLocalDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.localDescription = description;\n if (stallNextGathering) {\n stallNextGathering = false;\n this.iceGatheringState = 'gathering';\n } else {\n this.iceGatheringState = 'complete';\n }\n this.emit('icegatheringstatechange');\n return Promise.resolve();\n }\n\n setRemoteDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.remoteDescription = description;\n remoteSdps.push(description.sdp ?? '');\n if (description.type === 'offer') {\n const hostToken = /fake-offer:(\\w+)/.exec(description.sdp ?? '')?.[1];\n const link = hostToken ? links.get(hostToken) : undefined;\n if (!link) return Promise.reject(new Error('Unknown offer'));\n link.guestPc = this;\n return Promise.resolve();\n }\n\n const match = /fake-answer:(\\w+):guest:(\\w+)/.exec(description.sdp ?? '');\n const link = match?.[1] ? links.get(match[1]) : undefined;\n if (!link || link.guestPc?.token !== match?.[2]) {\n return Promise.reject(new Error('Unknown answer'));\n }\n this.completeLink(link);\n return Promise.resolve();\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.connectionState = 'closed';\n this.iceConnectionState = 'closed';\n this.emit('iceconnectionstatechange');\n this.emit('connectionstatechange');\n this.pendingChannel?.close();\n const link = this.linkFor();\n if (link) {\n link.guestChannel?.close();\n link.hostChannel?.close();\n const remote = link.guestPc === this ? link.hostPc : link.guestPc;\n if (remote && !remote.closed) {\n remote.iceConnectionState = 'disconnected';\n remote.emit('iceconnectionstatechange');\n }\n }\n }\n\n private linkFor(): FakeLink | undefined {\n return [...links.values()].find((link) => link.hostPc === this || link.guestPc === this);\n }\n\n private completeLink(link: FakeLink): void {\n if (link.completed) return;\n link.completed = true;\n\n if (failNextIce) {\n failNextIce = false;\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc) continue;\n pc.iceConnectionState = 'failed';\n pc.emit('iceconnectionstatechange');\n }\n return;\n }\n\n const hostChannel = link.hostPc.pendingChannel ?? new FakeDataChannel();\n const guestChannel = new FakeDataChannel();\n hostChannel.remote = guestChannel;\n guestChannel.remote = hostChannel;\n link.hostChannel = hostChannel;\n link.guestChannel = guestChannel;\n link.guestPc?.emit('datachannel', { channel: guestChannel });\n\n schedule(() => {\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc || pc.closed) continue;\n pc.iceConnectionState = 'connected';\n pc.connectionState = 'connected';\n pc.emit('iceconnectionstatechange');\n pc.emit('connectionstatechange');\n }\n if (hangNextOpen) {\n hangNextOpen = false;\n return;\n }\n hostChannel.open();\n guestChannel.open();\n });\n }\n }\n\n return fixture;\n}\n"],"mappings":"AAUA,IAAM,YAAN,KAAkB,CAChB,UAA6B,IAAI,IAEjC,IAAI,EAAc,EAA0B,CAC1C,IAAI,EAAM,KAAK,UAAU,IAAI,CAAI,EAC5B,IACH,EAAM,IAAI,IACV,KAAK,UAAU,IAAI,EAAM,CAAG,GAE9B,EAAI,IAAI,CAAQ,CAClB,CAEA,OAAO,EAAc,EAA0B,CAC7C,KAAK,UAAU,IAAI,CAAI,CAAC,EAAE,OAAO,CAAQ,CAC3C,CAEA,KAAK,EAAc,EAAsB,CAAC,EAAS,CACjD,IAAK,IAAM,KAAY,KAAK,UAAU,IAAI,CAAI,GAAK,CAAC,EAAG,EAAS,CAAK,CACvE,CAGA,iBAAiB,EAAc,EAAwC,CACrE,KAAK,IAAI,EAAM,CAAoB,CACrC,CAEA,oBAAoB,EAAc,EAAwC,CACxE,KAAK,OAAO,EAAM,CAAoB,CACxC,CACF,EA8BA,SAAgB,cAAc,EAAkC,CAAC,EAAY,CAU3E,IAAM,EAAuC,CAAC,EACxC,EAAQ,IAAI,IACZ,EAAuB,CAAC,EAC1B,EAAY,EACZ,EAAY,EACZ,EAAc,GACd,EAAqB,GACrB,EAAe,GAEb,EAAmB,CACvB,aAAa,EAAO,CAClB,IAAM,EAAO,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,GACjC,GAAM,aAAa,MAAM,EACzB,GAAM,cAAc,MAAM,EACtB,IACF,EAAK,OAAO,mBAAqB,eACjC,EAAK,OAAO,KAAK,0BAA0B,EACvC,EAAK,UACP,EAAK,QAAQ,mBAAqB,eAClC,EAAK,QAAQ,KAAK,0BAA0B,GAGlD,EACA,SAAS,EAAG,CACV,GAAa,CACf,EACA,SAAU,CACR,EAAc,EAChB,EACA,UAAW,CACT,EAAe,EACjB,EACA,UAAW,EAAQ,WAAa,EAChC,IAAI,WAAY,CACd,OAAO,EAAM,IACf,EACA,IAAI,YAAa,CACf,OAAO,CACT,EACA,IAAK,CACH,yBAA4B,IAAI,mBAAmB,KAAK,EAAE,GAAW,CACvE,EACA,UAAW,CACT,EAAqB,EACvB,EACA,MAAO,CACL,KAAO,EAAkB,QAAQ,EAAkB,MAAM,CAAC,GAAG,CAC/D,CACF,EAEA,SAAS,SAAS,EAA4B,CACxC,EAAQ,UAAY,GACtB,EAAkB,KAAK,CAAQ,EAC/B,WAAW,EAAQ,KAAM,EAAQ,SAAS,GAE1C,eAAe,CAAQ,CAE3B,CAEA,MAAM,wBAAwB,WAA0C,CACtE,WAAa,aACb,OAAiC,KAEjC,KAAK,EAAoB,CACvB,GAAI,KAAK,aAAe,OAAQ,MAAU,MAAM,cAAc,KAAK,YAAY,EAC/E,GAAI,EAAY,EAAG,CACjB,IACA,MACF,CACA,IAAM,EAAS,KAAK,OACf,GAAU,EAAO,aAAe,QACrC,aAAe,EAAO,KAAK,UAAW,CAAE,MAAK,CAAC,CAAC,CACjD,CAEA,MAAa,CACP,KAAK,aAAe,eACxB,KAAK,WAAa,OAClB,KAAK,KAAK,MAAM,EAClB,CAEA,OAAc,CACZ,GAAI,KAAK,aAAe,SAAU,OAClC,KAAK,WAAa,SAClB,KAAK,KAAK,OAAO,EACjB,IAAM,EAAS,KAAK,OAChB,GAAU,EAAO,aAAe,WAClC,EAAO,WAAa,SACpB,EAAO,KAAK,OAAO,EAEvB,CACF,CAEA,MAAM,2BAA2B,WAA6C,CAC5E,gBAAkB,MAClB,mBAAqB,MACrB,kBAAoB,MACpB,iBAAqD,KAErD,kBAAsD,KACtD,eAAyC,KACzC,OAAS,GAET,MAEA,YAAY,EAAe,CACzB,MAAM,EACN,KAAK,MAAQ,CACf,CAEA,mBAAwC,CAEtC,MADA,MAAK,eAAiB,IAAI,gBACnB,KAAK,cACd,CAEA,aAAkD,CAShD,OARA,EAAM,IAAI,KAAK,MAAO,CACpB,UAAW,GACX,aAAc,KACd,QAAS,KACT,YAAa,KACb,OAAQ,KACR,UAAW,KAAK,KAClB,CAAC,EACM,QAAQ,QAAQ,CAAE,IAAK,cAAc,KAAK,QAAS,KAAM,OAAQ,CAAC,CAC3E,CAEA,cAAmD,CACjD,IAAM,EAAO,KAAK,QAAQ,EAC1B,OAAO,QAAQ,QAAQ,CACrB,IAAK,eAAe,GAAM,WAAa,OAAO,SAAS,KAAK,QAC5D,KAAM,QACR,CAAC,CACH,CAEA,oBAAoB,EAAuD,CASzE,MARA,MAAK,iBAAmB,EACpB,GACF,EAAqB,GACrB,KAAK,kBAAoB,aAEzB,KAAK,kBAAoB,WAE3B,KAAK,KAAK,yBAAyB,EAC5B,QAAQ,QAAQ,CACzB,CAEA,qBAAqB,EAAuD,CAG1E,GAFA,KAAK,kBAAoB,EACzB,EAAW,KAAK,EAAY,KAAO,EAAE,EACjC,EAAY,OAAS,QAAS,CAChC,IAAM,EAAY,mBAAmB,KAAK,EAAY,KAAO,EAAE,CAAC,GAAG,GAC7D,EAAO,EAAY,EAAM,IAAI,CAAS,EAAI,IAAA,GAGhD,OAFK,GACL,EAAK,QAAU,KACR,QAAQ,QAAQ,GAFL,QAAQ,OAAW,MAAM,eAAe,CAAC,CAG7D,CAEA,IAAM,EAAQ,gCAAgC,KAAK,EAAY,KAAO,EAAE,EAClE,EAAO,IAAQ,GAAK,EAAM,IAAI,EAAM,EAAE,EAAI,IAAA,GAKhD,MAJI,CAAC,GAAQ,EAAK,SAAS,QAAU,IAAQ,GACpC,QAAQ,OAAW,MAAM,gBAAgB,CAAC,GAEnD,KAAK,aAAa,CAAI,EACf,QAAQ,QAAQ,EACzB,CAEA,OAAc,CACZ,GAAI,KAAK,OAAQ,OACjB,KAAK,OAAS,GACd,KAAK,gBAAkB,SACvB,KAAK,mBAAqB,SAC1B,KAAK,KAAK,0BAA0B,EACpC,KAAK,KAAK,uBAAuB,EACjC,KAAK,gBAAgB,MAAM,EAC3B,IAAM,EAAO,KAAK,QAAQ,EAC1B,GAAI,EAAM,CACR,EAAK,cAAc,MAAM,EACzB,EAAK,aAAa,MAAM,EACxB,IAAM,EAAS,EAAK,UAAY,KAAO,EAAK,OAAS,EAAK,QACtD,GAAU,CAAC,EAAO,SACpB,EAAO,mBAAqB,eAC5B,EAAO,KAAK,0BAA0B,EAE1C,CACF,CAEA,SAAwC,CACtC,MAAO,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,KAAM,GAAS,EAAK,SAAW,MAAQ,EAAK,UAAY,IAAI,CACzF,CAEA,aAAqB,EAAsB,CACzC,GAAI,EAAK,UAAW,OAGpB,GAFA,EAAK,UAAY,GAEb,EAAa,CACf,EAAc,GACd,IAAK,IAAM,IAAM,CAAC,EAAK,OAAQ,EAAK,OAAO,EACpC,IACL,EAAG,mBAAqB,SACxB,EAAG,KAAK,0BAA0B,GAEpC,MACF,CAEA,IAAM,EAAc,EAAK,OAAO,gBAAkB,IAAI,gBAChD,EAAe,IAAI,gBACzB,EAAY,OAAS,EACrB,EAAa,OAAS,EACtB,EAAK,YAAc,EACnB,EAAK,aAAe,EACpB,EAAK,SAAS,KAAK,cAAe,CAAE,QAAS,CAAa,CAAC,EAE3D,aAAe,CACb,IAAK,IAAM,IAAM,CAAC,EAAK,OAAQ,EAAK,OAAO,EACpC,GAAM,GAAG,SACd,EAAG,mBAAqB,YACxB,EAAG,gBAAkB,YACrB,EAAG,KAAK,0BAA0B,EAClC,EAAG,KAAK,uBAAuB,GAEjC,GAAI,EAAc,CAChB,EAAe,GACf,MACF,CACA,EAAY,KAAK,EACjB,EAAa,KAAK,CACpB,CAAC,CACH,CACF,CAEA,OAAO,CACT"}
|
|
1
|
+
{"version":3,"file":"fake-rtc.cjs","names":[],"sources":["../../src/testing/fake-rtc.ts"],"sourcesContent":["import type {\n MeshRtcEvent,\n MeshRtcFactory,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n RTCSessionDescriptionLike,\n} from '../types';\n\ntype Listener = (event: MeshRtcEvent) => void;\n\nclass ListenerBag {\n private readonly listeners = new Map<string, Set<Listener>>();\n\n add(type: string, listener: Listener): void {\n let set = this.listeners.get(type);\n if (!set) {\n set = new Set();\n this.listeners.set(type, set);\n }\n set.add(listener);\n }\n\n remove(type: string, listener: Listener): void {\n this.listeners.get(type)?.delete(listener);\n }\n\n emit(type: string, event: MeshRtcEvent = {}): void {\n for (const listener of this.listeners.get(type) ?? []) listener(event);\n }\n\n // `never` accepts every listener shape declared by the `*Like` overloads.\n addEventListener(type: string, listener: (event: never) => void): void {\n this.add(type, listener as Listener);\n }\n\n removeEventListener(type: string, listener: (event: never) => void): void {\n this.remove(type, listener as Listener);\n }\n}\n\nexport interface FakeRtc {\n /** Close both ends of the link created at `index` (0-based, pairing order). */\n closeChannel(index: number): void;\n /** Silently drop the next `n` channel sends. */\n dropNext(n: number): void;\n /** Make the next pairing fail at the ICE stage. */\n failIce(): void;\n /** Make the next completed link connect at ICE level but never open the channel. */\n hangOpen(): void;\n /** Delivery delay for messages and channel-open in milliseconds. Default: 0 (microtask). */\n latencyMs: number;\n /** Number of links initiated (one per created offer). */\n readonly linkCount: number;\n /** Every SDP handed to `setRemoteDescription`, in call order. */\n readonly remoteSdps: readonly string[];\n readonly rtc: MeshRtcFactory;\n /** Make the next `setLocalDescription` never finish ICE gathering. */\n stallIce(): void;\n /** Drain all latency-queued deliveries immediately. */\n tick(): void;\n}\n\n/**\n * In-memory WebRTC stand-in for testing mesh nodes without a browser.\n * Peer connections created from the same fixture wire together through fake\n * SDP tokens: no network, no real ICE. Pass `fixture.rtc` as the `rtc`\n * option to `createMeshHost`/`createMeshGuest`.\n */\nexport function createFakeRtc(options: { latencyMs?: number } = {}): FakeRtc {\n interface FakeLink {\n completed: boolean;\n guestChannel: FakeDataChannel | null;\n guestPc: FakePeerConnection | null;\n hostChannel: FakeDataChannel | null;\n hostPc: FakePeerConnection;\n readonly hostToken: string;\n }\n\n const pendingDeliveries: Array<() => void> = [];\n const links = new Map<string, FakeLink>();\n const remoteSdps: string[] = [];\n let pcCounter = 0;\n let dropCount = 0;\n let failNextIce = false;\n let stallNextGathering = false;\n let hangNextOpen = false;\n\n const fixture: FakeRtc = {\n closeChannel(index) {\n const link = [...links.values()][index];\n link?.hostChannel?.close();\n link?.guestChannel?.close();\n if (link) {\n link.hostPc.iceConnectionState = 'disconnected';\n link.hostPc.emit('iceconnectionstatechange');\n if (link.guestPc) {\n link.guestPc.iceConnectionState = 'disconnected';\n link.guestPc.emit('iceconnectionstatechange');\n }\n }\n },\n dropNext(n) {\n dropCount += n;\n },\n failIce() {\n failNextIce = true;\n },\n hangOpen() {\n hangNextOpen = true;\n },\n latencyMs: options.latencyMs ?? 0,\n get linkCount() {\n return links.size;\n },\n get remoteSdps() {\n return remoteSdps;\n },\n rtc: {\n createPeerConnection: () => new FakePeerConnection(`pc${++pcCounter}`),\n },\n stallIce() {\n stallNextGathering = true;\n },\n tick() {\n while (pendingDeliveries.length) pendingDeliveries.shift()?.();\n },\n };\n\n function schedule(delivery: () => void): void {\n if (fixture.latencyMs > 0) {\n pendingDeliveries.push(delivery);\n setTimeout(fixture.tick, fixture.latencyMs);\n } else {\n queueMicrotask(delivery);\n }\n }\n\n class FakeDataChannel extends ListenerBag implements RTCDataChannelLike {\n readyState = 'connecting';\n remote: FakeDataChannel | null = null;\n\n send(data: string): void {\n if (this.readyState !== 'open') throw new Error(`Channel is ${this.readyState}`);\n if (dropCount > 0) {\n dropCount--;\n return;\n }\n const remote = this.remote;\n if (!remote || remote.readyState !== 'open') return;\n schedule(() => remote.emit('message', { data }));\n }\n\n open(): void {\n if (this.readyState !== 'connecting') return;\n this.readyState = 'open';\n this.emit('open');\n }\n\n close(): void {\n if (this.readyState === 'closed') return;\n this.readyState = 'closed';\n this.emit('close');\n const remote = this.remote;\n if (remote && remote.readyState !== 'closed') {\n remote.readyState = 'closed';\n remote.emit('close');\n }\n }\n }\n\n class FakePeerConnection extends ListenerBag implements RTCPeerConnectionLike {\n connectionState = 'new';\n iceConnectionState = 'new';\n iceGatheringState = 'new';\n localDescription: RTCSessionDescriptionLike | null = null;\n /** Last description received through `setRemoteDescription`, for assertions. */\n remoteDescription: RTCSessionDescriptionLike | null = null;\n pendingChannel: FakeDataChannel | null = null;\n closed = false;\n\n readonly token: string;\n\n constructor(token: string) {\n super();\n this.token = token;\n }\n\n createDataChannel(): RTCDataChannelLike {\n this.pendingChannel = new FakeDataChannel();\n return this.pendingChannel;\n }\n\n createOffer(): Promise<RTCSessionDescriptionLike> {\n links.set(this.token, {\n completed: false,\n guestChannel: null,\n guestPc: null,\n hostChannel: null,\n hostPc: this,\n hostToken: this.token,\n });\n return Promise.resolve({ sdp: `fake-offer:${this.token}`, type: 'offer' });\n }\n\n createAnswer(): Promise<RTCSessionDescriptionLike> {\n const link = this.linkFor();\n return Promise.resolve({\n sdp: `fake-answer:${link?.hostToken ?? 'none'}:guest:${this.token}`,\n type: 'answer',\n });\n }\n\n setLocalDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.localDescription = description;\n if (stallNextGathering) {\n stallNextGathering = false;\n this.iceGatheringState = 'gathering';\n } else {\n this.iceGatheringState = 'complete';\n }\n this.emit('icegatheringstatechange');\n return Promise.resolve();\n }\n\n setRemoteDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.remoteDescription = description;\n remoteSdps.push(description.sdp ?? '');\n if (description.type === 'offer') {\n const hostToken = /fake-offer:(\\w+)/.exec(description.sdp ?? '')?.[1];\n const link = hostToken ? links.get(hostToken) : undefined;\n if (!link) return Promise.reject(new Error('Unknown offer'));\n link.guestPc = this;\n return Promise.resolve();\n }\n\n const match = /fake-answer:(\\w+):guest:(\\w+)/.exec(description.sdp ?? '');\n const link = match?.[1] ? links.get(match[1]) : undefined;\n if (!link || link.guestPc?.token !== match?.[2]) {\n return Promise.reject(new Error('Unknown answer'));\n }\n this.completeLink(link);\n return Promise.resolve();\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.connectionState = 'closed';\n this.iceConnectionState = 'closed';\n this.emit('iceconnectionstatechange');\n this.emit('connectionstatechange');\n this.pendingChannel?.close();\n const link = this.linkFor();\n if (link) {\n link.guestChannel?.close();\n link.hostChannel?.close();\n const remote = link.guestPc === this ? link.hostPc : link.guestPc;\n if (remote && !remote.closed) {\n remote.iceConnectionState = 'disconnected';\n remote.emit('iceconnectionstatechange');\n }\n }\n }\n\n private linkFor(): FakeLink | undefined {\n return [...links.values()].find((link) => link.hostPc === this || link.guestPc === this);\n }\n\n private completeLink(link: FakeLink): void {\n if (link.completed) return;\n link.completed = true;\n\n if (failNextIce) {\n failNextIce = false;\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc) continue;\n pc.iceConnectionState = 'failed';\n pc.emit('iceconnectionstatechange');\n }\n return;\n }\n\n const hostChannel = link.hostPc.pendingChannel ?? new FakeDataChannel();\n const guestChannel = new FakeDataChannel();\n hostChannel.remote = guestChannel;\n guestChannel.remote = hostChannel;\n link.hostChannel = hostChannel;\n link.guestChannel = guestChannel;\n link.guestPc?.emit('datachannel', { channel: guestChannel });\n\n schedule(() => {\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc || pc.closed) continue;\n pc.iceConnectionState = 'connected';\n pc.connectionState = 'connected';\n pc.emit('iceconnectionstatechange');\n pc.emit('connectionstatechange');\n }\n if (hangNextOpen) {\n hangNextOpen = false;\n return;\n }\n hostChannel.open();\n guestChannel.open();\n });\n }\n }\n\n return fixture;\n}\n"],"mappings":"AAUA,IAAM,YAAN,KAAkB,CAChB,UAA6B,IAAI,IAEjC,IAAI,EAAc,EAA0B,CAC1C,IAAI,EAAM,KAAK,UAAU,IAAI,CAAI,EAC5B,IACH,EAAM,IAAI,IACV,KAAK,UAAU,IAAI,EAAM,CAAG,GAE9B,EAAI,IAAI,CAAQ,CAClB,CAEA,OAAO,EAAc,EAA0B,CAC7C,KAAK,UAAU,IAAI,CAAI,CAAC,EAAE,OAAO,CAAQ,CAC3C,CAEA,KAAK,EAAc,EAAsB,CAAC,EAAS,CACjD,IAAK,IAAM,KAAY,KAAK,UAAU,IAAI,CAAI,GAAK,CAAC,EAAG,EAAS,CAAK,CACvE,CAGA,iBAAiB,EAAc,EAAwC,CACrE,KAAK,IAAI,EAAM,CAAoB,CACrC,CAEA,oBAAoB,EAAc,EAAwC,CACxE,KAAK,OAAO,EAAM,CAAoB,CACxC,CACF,EA8BA,SAAgB,cAAc,EAAkC,CAAC,EAAY,CAU3E,IAAM,EAAuC,CAAC,EACxC,EAAQ,IAAI,IACZ,EAAuB,CAAC,EAC1B,EAAY,EACZ,EAAY,EACZ,EAAc,GACd,EAAqB,GACrB,EAAe,GAEb,EAAmB,CACvB,aAAa,EAAO,CAClB,IAAM,EAAO,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,GACjC,GAAM,aAAa,MAAM,EACzB,GAAM,cAAc,MAAM,EACtB,IACF,EAAK,OAAO,mBAAqB,eACjC,EAAK,OAAO,KAAK,0BAA0B,EACvC,EAAK,UACP,EAAK,QAAQ,mBAAqB,eAClC,EAAK,QAAQ,KAAK,0BAA0B,GAGlD,EACA,SAAS,EAAG,CACV,GAAa,CACf,EACA,SAAU,CACR,EAAc,EAChB,EACA,UAAW,CACT,EAAe,EACjB,EACA,UAAW,EAAQ,WAAa,EAChC,IAAI,WAAY,CACd,OAAO,EAAM,IACf,EACA,IAAI,YAAa,CACf,OAAO,CACT,EACA,IAAK,CACH,yBAA4B,IAAI,mBAAmB,KAAK,EAAE,GAAW,CACvE,EACA,UAAW,CACT,EAAqB,EACvB,EACA,MAAO,CACL,KAAO,EAAkB,QAAQ,EAAkB,MAAM,CAAC,GAAG,CAC/D,CACF,EAEA,SAAS,SAAS,EAA4B,CACxC,EAAQ,UAAY,GACtB,EAAkB,KAAK,CAAQ,EAC/B,WAAW,EAAQ,KAAM,EAAQ,SAAS,GAE1C,eAAe,CAAQ,CAE3B,CAEA,MAAM,wBAAwB,WAA0C,CACtE,WAAa,aACb,OAAiC,KAEjC,KAAK,EAAoB,CACvB,GAAI,KAAK,aAAe,OAAQ,MAAU,MAAM,cAAc,KAAK,YAAY,EAC/E,GAAI,EAAY,EAAG,CACjB,IACA,MACF,CACA,IAAM,EAAS,KAAK,OACf,GAAU,EAAO,aAAe,QACrC,aAAe,EAAO,KAAK,UAAW,CAAE,MAAK,CAAC,CAAC,CACjD,CAEA,MAAa,CACP,KAAK,aAAe,eACxB,KAAK,WAAa,OAClB,KAAK,KAAK,MAAM,EAClB,CAEA,OAAc,CACZ,GAAI,KAAK,aAAe,SAAU,OAClC,KAAK,WAAa,SAClB,KAAK,KAAK,OAAO,EACjB,IAAM,EAAS,KAAK,OAChB,GAAU,EAAO,aAAe,WAClC,EAAO,WAAa,SACpB,EAAO,KAAK,OAAO,EAEvB,CACF,CAEA,MAAM,2BAA2B,WAA6C,CAC5E,gBAAkB,MAClB,mBAAqB,MACrB,kBAAoB,MACpB,iBAAqD,KAErD,kBAAsD,KACtD,eAAyC,KACzC,OAAS,GAET,MAEA,YAAY,EAAe,CACzB,MAAM,EACN,KAAK,MAAQ,CACf,CAEA,mBAAwC,CAEtC,MADA,MAAK,eAAiB,IAAI,gBACnB,KAAK,cACd,CAEA,aAAkD,CAShD,OARA,EAAM,IAAI,KAAK,MAAO,CACpB,UAAW,GACX,aAAc,KACd,QAAS,KACT,YAAa,KACb,OAAQ,KACR,UAAW,KAAK,KAClB,CAAC,EACM,QAAQ,QAAQ,CAAE,IAAK,cAAc,KAAK,QAAS,KAAM,OAAQ,CAAC,CAC3E,CAEA,cAAmD,CACjD,IAAM,EAAO,KAAK,QAAQ,EAC1B,OAAO,QAAQ,QAAQ,CACrB,IAAK,eAAe,GAAM,WAAa,OAAO,SAAS,KAAK,QAC5D,KAAM,QACR,CAAC,CACH,CAEA,oBAAoB,EAAuD,CASzE,MARA,MAAK,iBAAmB,EACpB,GACF,EAAqB,GACrB,KAAK,kBAAoB,aAEzB,KAAK,kBAAoB,WAE3B,KAAK,KAAK,yBAAyB,EAC5B,QAAQ,QAAQ,CACzB,CAEA,qBAAqB,EAAuD,CAG1E,GAFA,KAAK,kBAAoB,EACzB,EAAW,KAAK,EAAY,KAAO,EAAE,EACjC,EAAY,OAAS,QAAS,CAChC,IAAM,EAAY,mBAAmB,KAAK,EAAY,KAAO,EAAE,CAAC,GAAG,GAC7D,EAAO,EAAY,EAAM,IAAI,CAAS,EAAI,IAAA,GAGhD,OAFK,GACL,EAAK,QAAU,KACR,QAAQ,QAAQ,GAFL,QAAQ,OAAW,MAAM,eAAe,CAAC,CAG7D,CAEA,IAAM,EAAQ,gCAAgC,KAAK,EAAY,KAAO,EAAE,EAClE,EAAO,IAAQ,GAAK,EAAM,IAAI,EAAM,EAAE,EAAI,IAAA,GAKhD,MAJI,CAAC,GAAQ,EAAK,SAAS,QAAU,IAAQ,GACpC,QAAQ,OAAW,MAAM,gBAAgB,CAAC,GAEnD,KAAK,aAAa,CAAI,EACf,QAAQ,QAAQ,EACzB,CAEA,OAAc,CACZ,GAAI,KAAK,OAAQ,OACjB,KAAK,OAAS,GACd,KAAK,gBAAkB,SACvB,KAAK,mBAAqB,SAC1B,KAAK,KAAK,0BAA0B,EACpC,KAAK,KAAK,uBAAuB,EACjC,KAAK,gBAAgB,MAAM,EAC3B,IAAM,EAAO,KAAK,QAAQ,EAC1B,GAAI,EAAM,CACR,EAAK,cAAc,MAAM,EACzB,EAAK,aAAa,MAAM,EACxB,IAAM,EAAS,EAAK,UAAY,KAAO,EAAK,OAAS,EAAK,QACtD,GAAU,CAAC,EAAO,SACpB,EAAO,mBAAqB,eAC5B,EAAO,KAAK,0BAA0B,EAE1C,CACF,CAEA,SAAwC,CACtC,MAAO,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,KAAM,GAAS,EAAK,SAAW,MAAQ,EAAK,UAAY,IAAI,CACzF,CAEA,aAAqB,EAAsB,CACzC,GAAI,EAAK,UAAW,OAGpB,GAFA,EAAK,UAAY,GAEb,EAAa,CACf,EAAc,GACd,IAAK,IAAM,IAAM,CAAC,EAAK,OAAQ,EAAK,OAAO,EACpC,IACL,EAAG,mBAAqB,SACxB,EAAG,KAAK,0BAA0B,GAEpC,MACF,CAEA,IAAM,EAAc,EAAK,OAAO,gBAAkB,IAAI,gBAChD,EAAe,IAAI,gBACzB,EAAY,OAAS,EACrB,EAAa,OAAS,EACtB,EAAK,YAAc,EACnB,EAAK,aAAe,EACpB,EAAK,SAAS,KAAK,cAAe,CAAE,QAAS,CAAa,CAAC,EAE3D,aAAe,CACb,IAAK,IAAM,IAAM,CAAC,EAAK,OAAQ,EAAK,OAAO,EACpC,GAAM,GAAG,SACd,EAAG,mBAAqB,YACxB,EAAG,gBAAkB,YACrB,EAAG,KAAK,0BAA0B,EAClC,EAAG,KAAK,uBAAuB,GAEjC,GAAI,EAAc,CAChB,EAAe,GACf,MACF,CACA,EAAY,KAAK,EACjB,EAAa,KAAK,CACpB,CAAC,CACH,CACF,CAEA,OAAO,CACT"}
|
|
@@ -23,7 +23,7 @@ export interface FakeRtc {
|
|
|
23
23
|
/**
|
|
24
24
|
* In-memory WebRTC stand-in for testing mesh nodes without a browser.
|
|
25
25
|
* Peer connections created from the same fixture wire together through fake
|
|
26
|
-
* SDP tokens
|
|
26
|
+
* SDP tokens: no network, no real ICE. Pass `fixture.rtc` as the `rtc`
|
|
27
27
|
* option to `createMeshHost`/`createMeshGuest`.
|
|
28
28
|
*/
|
|
29
29
|
export declare function createFakeRtc(options?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-rtc.js","names":[],"sources":["../../src/testing/fake-rtc.ts"],"sourcesContent":["import type {\n MeshRtcEvent,\n MeshRtcFactory,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n RTCSessionDescriptionLike,\n} from '../types';\n\ntype Listener = (event: MeshRtcEvent) => void;\n\nclass ListenerBag {\n private readonly listeners = new Map<string, Set<Listener>>();\n\n add(type: string, listener: Listener): void {\n let set = this.listeners.get(type);\n if (!set) {\n set = new Set();\n this.listeners.set(type, set);\n }\n set.add(listener);\n }\n\n remove(type: string, listener: Listener): void {\n this.listeners.get(type)?.delete(listener);\n }\n\n emit(type: string, event: MeshRtcEvent = {}): void {\n for (const listener of this.listeners.get(type) ?? []) listener(event);\n }\n\n // `never` accepts every listener shape declared by the `*Like` overloads.\n addEventListener(type: string, listener: (event: never) => void): void {\n this.add(type, listener as Listener);\n }\n\n removeEventListener(type: string, listener: (event: never) => void): void {\n this.remove(type, listener as Listener);\n }\n}\n\nexport interface FakeRtc {\n /** Close both ends of the link created at `index` (0-based, pairing order). */\n closeChannel(index: number): void;\n /** Silently drop the next `n` channel sends. */\n dropNext(n: number): void;\n /** Make the next pairing fail at the ICE stage. */\n failIce(): void;\n /** Make the next completed link connect at ICE level but never open the channel. */\n hangOpen(): void;\n /** Delivery delay for messages and channel-open in milliseconds. Default: 0 (microtask). */\n latencyMs: number;\n /** Number of links initiated (one per created offer). */\n readonly linkCount: number;\n /** Every SDP handed to `setRemoteDescription`, in call order. */\n readonly remoteSdps: readonly string[];\n readonly rtc: MeshRtcFactory;\n /** Make the next `setLocalDescription` never finish ICE gathering. */\n stallIce(): void;\n /** Drain all latency-queued deliveries immediately. */\n tick(): void;\n}\n\n/**\n * In-memory WebRTC stand-in for testing mesh nodes without a browser.\n * Peer connections created from the same fixture wire together through fake\n * SDP tokens — no network, no real ICE. Pass `fixture.rtc` as the `rtc`\n * option to `createMeshHost`/`createMeshGuest`.\n */\nexport function createFakeRtc(options: { latencyMs?: number } = {}): FakeRtc {\n interface FakeLink {\n completed: boolean;\n guestChannel: FakeDataChannel | null;\n guestPc: FakePeerConnection | null;\n hostChannel: FakeDataChannel | null;\n hostPc: FakePeerConnection;\n readonly hostToken: string;\n }\n\n const pendingDeliveries: Array<() => void> = [];\n const links = new Map<string, FakeLink>();\n const remoteSdps: string[] = [];\n let pcCounter = 0;\n let dropCount = 0;\n let failNextIce = false;\n let stallNextGathering = false;\n let hangNextOpen = false;\n\n const fixture: FakeRtc = {\n closeChannel(index) {\n const link = [...links.values()][index];\n link?.hostChannel?.close();\n link?.guestChannel?.close();\n if (link) {\n link.hostPc.iceConnectionState = 'disconnected';\n link.hostPc.emit('iceconnectionstatechange');\n if (link.guestPc) {\n link.guestPc.iceConnectionState = 'disconnected';\n link.guestPc.emit('iceconnectionstatechange');\n }\n }\n },\n dropNext(n) {\n dropCount += n;\n },\n failIce() {\n failNextIce = true;\n },\n hangOpen() {\n hangNextOpen = true;\n },\n latencyMs: options.latencyMs ?? 0,\n get linkCount() {\n return links.size;\n },\n get remoteSdps() {\n return remoteSdps;\n },\n rtc: {\n createPeerConnection: () => new FakePeerConnection(`pc${++pcCounter}`),\n },\n stallIce() {\n stallNextGathering = true;\n },\n tick() {\n while (pendingDeliveries.length) pendingDeliveries.shift()?.();\n },\n };\n\n function schedule(delivery: () => void): void {\n if (fixture.latencyMs > 0) {\n pendingDeliveries.push(delivery);\n setTimeout(fixture.tick, fixture.latencyMs);\n } else {\n queueMicrotask(delivery);\n }\n }\n\n class FakeDataChannel extends ListenerBag implements RTCDataChannelLike {\n readyState = 'connecting';\n remote: FakeDataChannel | null = null;\n\n send(data: string): void {\n if (this.readyState !== 'open') throw new Error(`Channel is ${this.readyState}`);\n if (dropCount > 0) {\n dropCount--;\n return;\n }\n const remote = this.remote;\n if (!remote || remote.readyState !== 'open') return;\n schedule(() => remote.emit('message', { data }));\n }\n\n open(): void {\n if (this.readyState !== 'connecting') return;\n this.readyState = 'open';\n this.emit('open');\n }\n\n close(): void {\n if (this.readyState === 'closed') return;\n this.readyState = 'closed';\n this.emit('close');\n const remote = this.remote;\n if (remote && remote.readyState !== 'closed') {\n remote.readyState = 'closed';\n remote.emit('close');\n }\n }\n }\n\n class FakePeerConnection extends ListenerBag implements RTCPeerConnectionLike {\n connectionState = 'new';\n iceConnectionState = 'new';\n iceGatheringState = 'new';\n localDescription: RTCSessionDescriptionLike | null = null;\n /** Last description received through `setRemoteDescription`, for assertions. */\n remoteDescription: RTCSessionDescriptionLike | null = null;\n pendingChannel: FakeDataChannel | null = null;\n closed = false;\n\n readonly token: string;\n\n constructor(token: string) {\n super();\n this.token = token;\n }\n\n createDataChannel(): RTCDataChannelLike {\n this.pendingChannel = new FakeDataChannel();\n return this.pendingChannel;\n }\n\n createOffer(): Promise<RTCSessionDescriptionLike> {\n links.set(this.token, {\n completed: false,\n guestChannel: null,\n guestPc: null,\n hostChannel: null,\n hostPc: this,\n hostToken: this.token,\n });\n return Promise.resolve({ sdp: `fake-offer:${this.token}`, type: 'offer' });\n }\n\n createAnswer(): Promise<RTCSessionDescriptionLike> {\n const link = this.linkFor();\n return Promise.resolve({\n sdp: `fake-answer:${link?.hostToken ?? 'none'}:guest:${this.token}`,\n type: 'answer',\n });\n }\n\n setLocalDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.localDescription = description;\n if (stallNextGathering) {\n stallNextGathering = false;\n this.iceGatheringState = 'gathering';\n } else {\n this.iceGatheringState = 'complete';\n }\n this.emit('icegatheringstatechange');\n return Promise.resolve();\n }\n\n setRemoteDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.remoteDescription = description;\n remoteSdps.push(description.sdp ?? '');\n if (description.type === 'offer') {\n const hostToken = /fake-offer:(\\w+)/.exec(description.sdp ?? '')?.[1];\n const link = hostToken ? links.get(hostToken) : undefined;\n if (!link) return Promise.reject(new Error('Unknown offer'));\n link.guestPc = this;\n return Promise.resolve();\n }\n\n const match = /fake-answer:(\\w+):guest:(\\w+)/.exec(description.sdp ?? '');\n const link = match?.[1] ? links.get(match[1]) : undefined;\n if (!link || link.guestPc?.token !== match?.[2]) {\n return Promise.reject(new Error('Unknown answer'));\n }\n this.completeLink(link);\n return Promise.resolve();\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.connectionState = 'closed';\n this.iceConnectionState = 'closed';\n this.emit('iceconnectionstatechange');\n this.emit('connectionstatechange');\n this.pendingChannel?.close();\n const link = this.linkFor();\n if (link) {\n link.guestChannel?.close();\n link.hostChannel?.close();\n const remote = link.guestPc === this ? link.hostPc : link.guestPc;\n if (remote && !remote.closed) {\n remote.iceConnectionState = 'disconnected';\n remote.emit('iceconnectionstatechange');\n }\n }\n }\n\n private linkFor(): FakeLink | undefined {\n return [...links.values()].find((link) => link.hostPc === this || link.guestPc === this);\n }\n\n private completeLink(link: FakeLink): void {\n if (link.completed) return;\n link.completed = true;\n\n if (failNextIce) {\n failNextIce = false;\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc) continue;\n pc.iceConnectionState = 'failed';\n pc.emit('iceconnectionstatechange');\n }\n return;\n }\n\n const hostChannel = link.hostPc.pendingChannel ?? new FakeDataChannel();\n const guestChannel = new FakeDataChannel();\n hostChannel.remote = guestChannel;\n guestChannel.remote = hostChannel;\n link.hostChannel = hostChannel;\n link.guestChannel = guestChannel;\n link.guestPc?.emit('datachannel', { channel: guestChannel });\n\n schedule(() => {\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc || pc.closed) continue;\n pc.iceConnectionState = 'connected';\n pc.connectionState = 'connected';\n pc.emit('iceconnectionstatechange');\n pc.emit('connectionstatechange');\n }\n if (hangNextOpen) {\n hangNextOpen = false;\n return;\n }\n hostChannel.open();\n guestChannel.open();\n });\n }\n }\n\n return fixture;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fake-rtc.js","names":[],"sources":["../../src/testing/fake-rtc.ts"],"sourcesContent":["import type {\n MeshRtcEvent,\n MeshRtcFactory,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n RTCSessionDescriptionLike,\n} from '../types';\n\ntype Listener = (event: MeshRtcEvent) => void;\n\nclass ListenerBag {\n private readonly listeners = new Map<string, Set<Listener>>();\n\n add(type: string, listener: Listener): void {\n let set = this.listeners.get(type);\n if (!set) {\n set = new Set();\n this.listeners.set(type, set);\n }\n set.add(listener);\n }\n\n remove(type: string, listener: Listener): void {\n this.listeners.get(type)?.delete(listener);\n }\n\n emit(type: string, event: MeshRtcEvent = {}): void {\n for (const listener of this.listeners.get(type) ?? []) listener(event);\n }\n\n // `never` accepts every listener shape declared by the `*Like` overloads.\n addEventListener(type: string, listener: (event: never) => void): void {\n this.add(type, listener as Listener);\n }\n\n removeEventListener(type: string, listener: (event: never) => void): void {\n this.remove(type, listener as Listener);\n }\n}\n\nexport interface FakeRtc {\n /** Close both ends of the link created at `index` (0-based, pairing order). */\n closeChannel(index: number): void;\n /** Silently drop the next `n` channel sends. */\n dropNext(n: number): void;\n /** Make the next pairing fail at the ICE stage. */\n failIce(): void;\n /** Make the next completed link connect at ICE level but never open the channel. */\n hangOpen(): void;\n /** Delivery delay for messages and channel-open in milliseconds. Default: 0 (microtask). */\n latencyMs: number;\n /** Number of links initiated (one per created offer). */\n readonly linkCount: number;\n /** Every SDP handed to `setRemoteDescription`, in call order. */\n readonly remoteSdps: readonly string[];\n readonly rtc: MeshRtcFactory;\n /** Make the next `setLocalDescription` never finish ICE gathering. */\n stallIce(): void;\n /** Drain all latency-queued deliveries immediately. */\n tick(): void;\n}\n\n/**\n * In-memory WebRTC stand-in for testing mesh nodes without a browser.\n * Peer connections created from the same fixture wire together through fake\n * SDP tokens: no network, no real ICE. Pass `fixture.rtc` as the `rtc`\n * option to `createMeshHost`/`createMeshGuest`.\n */\nexport function createFakeRtc(options: { latencyMs?: number } = {}): FakeRtc {\n interface FakeLink {\n completed: boolean;\n guestChannel: FakeDataChannel | null;\n guestPc: FakePeerConnection | null;\n hostChannel: FakeDataChannel | null;\n hostPc: FakePeerConnection;\n readonly hostToken: string;\n }\n\n const pendingDeliveries: Array<() => void> = [];\n const links = new Map<string, FakeLink>();\n const remoteSdps: string[] = [];\n let pcCounter = 0;\n let dropCount = 0;\n let failNextIce = false;\n let stallNextGathering = false;\n let hangNextOpen = false;\n\n const fixture: FakeRtc = {\n closeChannel(index) {\n const link = [...links.values()][index];\n link?.hostChannel?.close();\n link?.guestChannel?.close();\n if (link) {\n link.hostPc.iceConnectionState = 'disconnected';\n link.hostPc.emit('iceconnectionstatechange');\n if (link.guestPc) {\n link.guestPc.iceConnectionState = 'disconnected';\n link.guestPc.emit('iceconnectionstatechange');\n }\n }\n },\n dropNext(n) {\n dropCount += n;\n },\n failIce() {\n failNextIce = true;\n },\n hangOpen() {\n hangNextOpen = true;\n },\n latencyMs: options.latencyMs ?? 0,\n get linkCount() {\n return links.size;\n },\n get remoteSdps() {\n return remoteSdps;\n },\n rtc: {\n createPeerConnection: () => new FakePeerConnection(`pc${++pcCounter}`),\n },\n stallIce() {\n stallNextGathering = true;\n },\n tick() {\n while (pendingDeliveries.length) pendingDeliveries.shift()?.();\n },\n };\n\n function schedule(delivery: () => void): void {\n if (fixture.latencyMs > 0) {\n pendingDeliveries.push(delivery);\n setTimeout(fixture.tick, fixture.latencyMs);\n } else {\n queueMicrotask(delivery);\n }\n }\n\n class FakeDataChannel extends ListenerBag implements RTCDataChannelLike {\n readyState = 'connecting';\n remote: FakeDataChannel | null = null;\n\n send(data: string): void {\n if (this.readyState !== 'open') throw new Error(`Channel is ${this.readyState}`);\n if (dropCount > 0) {\n dropCount--;\n return;\n }\n const remote = this.remote;\n if (!remote || remote.readyState !== 'open') return;\n schedule(() => remote.emit('message', { data }));\n }\n\n open(): void {\n if (this.readyState !== 'connecting') return;\n this.readyState = 'open';\n this.emit('open');\n }\n\n close(): void {\n if (this.readyState === 'closed') return;\n this.readyState = 'closed';\n this.emit('close');\n const remote = this.remote;\n if (remote && remote.readyState !== 'closed') {\n remote.readyState = 'closed';\n remote.emit('close');\n }\n }\n }\n\n class FakePeerConnection extends ListenerBag implements RTCPeerConnectionLike {\n connectionState = 'new';\n iceConnectionState = 'new';\n iceGatheringState = 'new';\n localDescription: RTCSessionDescriptionLike | null = null;\n /** Last description received through `setRemoteDescription`, for assertions. */\n remoteDescription: RTCSessionDescriptionLike | null = null;\n pendingChannel: FakeDataChannel | null = null;\n closed = false;\n\n readonly token: string;\n\n constructor(token: string) {\n super();\n this.token = token;\n }\n\n createDataChannel(): RTCDataChannelLike {\n this.pendingChannel = new FakeDataChannel();\n return this.pendingChannel;\n }\n\n createOffer(): Promise<RTCSessionDescriptionLike> {\n links.set(this.token, {\n completed: false,\n guestChannel: null,\n guestPc: null,\n hostChannel: null,\n hostPc: this,\n hostToken: this.token,\n });\n return Promise.resolve({ sdp: `fake-offer:${this.token}`, type: 'offer' });\n }\n\n createAnswer(): Promise<RTCSessionDescriptionLike> {\n const link = this.linkFor();\n return Promise.resolve({\n sdp: `fake-answer:${link?.hostToken ?? 'none'}:guest:${this.token}`,\n type: 'answer',\n });\n }\n\n setLocalDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.localDescription = description;\n if (stallNextGathering) {\n stallNextGathering = false;\n this.iceGatheringState = 'gathering';\n } else {\n this.iceGatheringState = 'complete';\n }\n this.emit('icegatheringstatechange');\n return Promise.resolve();\n }\n\n setRemoteDescription(description: RTCSessionDescriptionLike): Promise<void> {\n this.remoteDescription = description;\n remoteSdps.push(description.sdp ?? '');\n if (description.type === 'offer') {\n const hostToken = /fake-offer:(\\w+)/.exec(description.sdp ?? '')?.[1];\n const link = hostToken ? links.get(hostToken) : undefined;\n if (!link) return Promise.reject(new Error('Unknown offer'));\n link.guestPc = this;\n return Promise.resolve();\n }\n\n const match = /fake-answer:(\\w+):guest:(\\w+)/.exec(description.sdp ?? '');\n const link = match?.[1] ? links.get(match[1]) : undefined;\n if (!link || link.guestPc?.token !== match?.[2]) {\n return Promise.reject(new Error('Unknown answer'));\n }\n this.completeLink(link);\n return Promise.resolve();\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.connectionState = 'closed';\n this.iceConnectionState = 'closed';\n this.emit('iceconnectionstatechange');\n this.emit('connectionstatechange');\n this.pendingChannel?.close();\n const link = this.linkFor();\n if (link) {\n link.guestChannel?.close();\n link.hostChannel?.close();\n const remote = link.guestPc === this ? link.hostPc : link.guestPc;\n if (remote && !remote.closed) {\n remote.iceConnectionState = 'disconnected';\n remote.emit('iceconnectionstatechange');\n }\n }\n }\n\n private linkFor(): FakeLink | undefined {\n return [...links.values()].find((link) => link.hostPc === this || link.guestPc === this);\n }\n\n private completeLink(link: FakeLink): void {\n if (link.completed) return;\n link.completed = true;\n\n if (failNextIce) {\n failNextIce = false;\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc) continue;\n pc.iceConnectionState = 'failed';\n pc.emit('iceconnectionstatechange');\n }\n return;\n }\n\n const hostChannel = link.hostPc.pendingChannel ?? new FakeDataChannel();\n const guestChannel = new FakeDataChannel();\n hostChannel.remote = guestChannel;\n guestChannel.remote = hostChannel;\n link.hostChannel = hostChannel;\n link.guestChannel = guestChannel;\n link.guestPc?.emit('datachannel', { channel: guestChannel });\n\n schedule(() => {\n for (const pc of [link.hostPc, link.guestPc]) {\n if (!pc || pc.closed) continue;\n pc.iceConnectionState = 'connected';\n pc.connectionState = 'connected';\n pc.emit('iceconnectionstatechange');\n pc.emit('connectionstatechange');\n }\n if (hangNextOpen) {\n hangNextOpen = false;\n return;\n }\n hostChannel.open();\n guestChannel.open();\n });\n }\n }\n\n return fixture;\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface MeshInvitation {
|
|
|
72
72
|
readonly secret: string;
|
|
73
73
|
/** Random 128-bit session identifier, base64url-encoded. */
|
|
74
74
|
readonly sessionId: string;
|
|
75
|
-
/** Schema version
|
|
75
|
+
/** Schema version: currently always 1. */
|
|
76
76
|
readonly v: 1;
|
|
77
77
|
}
|
|
78
78
|
/** Guest's answer returned to the host out-of-band. */
|
|
@@ -126,7 +126,7 @@ export interface MeshOptions {
|
|
|
126
126
|
readonly maxMessageBytes?: number;
|
|
127
127
|
/** Randomness source for ids and secrets. Default: `crypto.getRandomValues`. */
|
|
128
128
|
readonly random?: RandomSource;
|
|
129
|
-
/** WebRTC factory
|
|
129
|
+
/** WebRTC factory: the injection point for tests and non-browser runtimes. */
|
|
130
130
|
readonly rtc?: MeshRtcFactory;
|
|
131
131
|
/** Disposes the node when aborted. */
|
|
132
132
|
readonly signal?: AbortSignal;
|
|
@@ -167,7 +167,7 @@ export interface MeshInbound<T> {
|
|
|
167
167
|
export type Unsubscribe = () => void;
|
|
168
168
|
/**
|
|
169
169
|
* Runtime events emitted by {@link MeshNode.tap}.
|
|
170
|
-
* Subscribe via `node.tap(handler)
|
|
170
|
+
* Subscribe via `node.tap(handler)`: handler errors are swallowed.
|
|
171
171
|
*/
|
|
172
172
|
export type MeshEvent = {
|
|
173
173
|
readonly type: 'status-change';
|
|
@@ -247,7 +247,7 @@ export interface MeshHost<P extends MeshProtocol> extends MeshNode {
|
|
|
247
247
|
/**
|
|
248
248
|
* Create a single-use invitation for one prospective guest. Deliver the
|
|
249
249
|
* encoded invitation out-of-band (copy/paste, `navigator.share`, QR).
|
|
250
|
-
* May be called repeatedly
|
|
250
|
+
* May be called repeatedly: one invitation per guest. `meta.name` is
|
|
251
251
|
* shown to the guest as this host's peer name.
|
|
252
252
|
*/
|
|
253
253
|
createInvitation(meta?: {
|