@vielzeug/mesh 26.10.15 → 26.10.16
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/dist/_base45.cjs.map +1 -1
- package/dist/_base45.js +37 -1
- package/dist/_base45.js.map +1 -1
- package/dist/_defaults.cjs.map +1 -1
- package/dist/_defaults.js +5 -1
- package/dist/_defaults.js.map +1 -1
- package/dist/_node.cjs.map +1 -1
- package/dist/_node.js +166 -1
- package/dist/_node.js.map +1 -1
- package/dist/_proof.cjs.map +1 -1
- package/dist/_proof.js +16 -1
- package/dist/_proof.js.map +1 -1
- package/dist/_random.cjs.map +1 -1
- package/dist/_random.js +14 -1
- package/dist/_random.js.map +1 -1
- package/dist/_rtc.cjs.map +1 -1
- package/dist/_rtc.js +41 -3
- package/dist/_rtc.js.map +1 -1
- package/dist/_sdp.cjs.map +1 -1
- package/dist/_sdp.js +54 -2
- package/dist/_sdp.js.map +1 -1
- package/dist/_wire.cjs.map +1 -1
- package/dist/_wire.js +20 -1
- package/dist/_wire.js.map +1 -1
- package/dist/codec.cjs.map +1 -1
- package/dist/codec.js +34 -1
- package/dist/codec.js.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +18 -1
- package/dist/errors.js.map +1 -1
- package/dist/guest.cjs.map +1 -1
- package/dist/guest.js +153 -1
- package/dist/guest.js.map +1 -1
- package/dist/host.cjs.map +1 -1
- package/dist/host.js +202 -1
- package/dist/host.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/mesh.cjs.map +1 -1
- package/dist/mesh.iife.js.map +1 -1
- package/dist/mesh.js +756 -4
- package/dist/mesh.js.map +1 -1
- package/dist/qr-codec.cjs.map +1 -1
- package/dist/qr-codec.js +52 -1
- package/dist/qr-codec.js.map +1 -1
- package/dist/testing/fake-rtc.cjs +1 -1
- package/dist/testing/fake-rtc.cjs.map +1 -1
- package/dist/testing/fake-rtc.js +153 -1
- package/dist/testing/fake-rtc.js.map +1 -1
- package/dist/testing.js +1 -1
- package/package.json +5 -5
package/dist/guest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guest.js","names":[],"sources":["../src/guest.ts"],"sourcesContent":["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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"guest.js","names":["rtc","options","nativeRtcFactory","clock","maxMessageBytes","channelOpenTimeoutMs","iceGatheringTimeoutMs","core","createNodeCore","messenger","createMessenger","hostPeer","openTimer","MeshTimeoutError","peer","error","dc","event","MeshConnectionError","pc","state","MeshPairingError","invitation","createPeerRecord","normalizeSdp","cause","answerSdp","waitIceGathering","MeshDisposedError","peerId","randomId","proof","createProof","meta","type","handler","payload","tapOptions"],"sources":["../src/guest.ts"],"sourcesContent":["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"],"mappings":"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"}
|
package/dist/host.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.cjs","names":[],"sources":["../src/host.ts"],"sourcesContent":["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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"host.cjs","names":["nativeRtcFactory","createNodeCore","createMessenger","MeshPairingError","createProof","createPeerRecord","waitChannelOpen","normalizeSdp","MeshDisposedError","randomId","bytesToBase64Url","randomBytes","waitIceGathering","MeshConnectionError","require_errors","require__random","require__node","require__proof","require__rtc","_vielzeug_arsenal","rtc","options","clock","maxMessageBytes","invitationTtlMs","channelOpenTimeoutMs","iceGatheringTimeoutMs","channelInit","core","messenger","peers","pending","sawPeer","sawFailure","next","peer","invitation","sessionId","status","failed","pc","dc","event","state","answer","expected","opened","cause","error","broadcastOptions","type","payload","secret","expiresAt","sdp","expiryTimer","meta","peerId","reason","handler","tapOptions"],"sources":["../src/host.ts"],"sourcesContent":["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"],"mappings":"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"}
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,203 @@
|
|
|
1
|
-
import{MeshConnectionError as e,MeshDisposedError as t,MeshPairingError as n
|
|
1
|
+
import { MeshConnectionError as e, MeshDisposedError as t, MeshPairingError as n } from "./errors.js";
|
|
2
|
+
import "./_defaults.js";
|
|
3
|
+
import { randomBytes as r, randomId as i } from "./_random.js";
|
|
4
|
+
import { createMessenger as a, createNodeCore as o, createPeerRecord as s } from "./_node.js";
|
|
5
|
+
import { createProof as c } from "./_proof.js";
|
|
6
|
+
import { nativeRtcFactory as l, normalizeSdp as u, waitChannelOpen as d, waitIceGathering as f } from "./_rtc.js";
|
|
7
|
+
import { bytesToBase64Url as p } from "@vielzeug/arsenal";
|
|
8
|
+
//#region src/host.ts
|
|
9
|
+
function createMeshHost(m = {}) {
|
|
10
|
+
let h = m.rtc ?? l, g = m.clock ?? (() => Date.now()), _ = m.maxMessageBytes ?? 65536, v = m.invitationTtlMs ?? 3e5, y = m.channelOpenTimeoutMs ?? 6e4, b = m.iceGatheringTimeoutMs ?? 5e3, x = {
|
|
11
|
+
label: "mesh",
|
|
12
|
+
ordered: !0,
|
|
13
|
+
...m.channel
|
|
14
|
+
}, S = o(m.signal), C = a({
|
|
15
|
+
clock: g,
|
|
16
|
+
emitTap: S.emitTap,
|
|
17
|
+
maxMessageBytes: _,
|
|
18
|
+
random: m.random
|
|
19
|
+
}), w = /* @__PURE__ */ new Map(), T = /* @__PURE__ */ new Map(), E = !1, D = !1;
|
|
20
|
+
function recomputeStatus() {
|
|
21
|
+
let e = T.size > 0 ? "pairing" : E ? "disconnected" : D ? "failed" : "idle";
|
|
22
|
+
for (let t of w.values()) {
|
|
23
|
+
if (t.status === "connected") {
|
|
24
|
+
e = "connected";
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
t.status === "connecting" && (e = "connecting");
|
|
28
|
+
}
|
|
29
|
+
S.setStatus(e);
|
|
30
|
+
}
|
|
31
|
+
function expireInvitation(e) {
|
|
32
|
+
let t = T.get(e);
|
|
33
|
+
t && (T.delete(e), clearTimeout(t.expiryTimer), t.pc.close(), S.emitTap({
|
|
34
|
+
sessionId: e,
|
|
35
|
+
type: "invitation-expired"
|
|
36
|
+
}), recomputeStatus());
|
|
37
|
+
}
|
|
38
|
+
function setPeerStatus(e, t) {
|
|
39
|
+
e.status !== t && (e.status = t, S.emitTap({
|
|
40
|
+
peerId: e.id,
|
|
41
|
+
status: t,
|
|
42
|
+
type: "peer-status-change"
|
|
43
|
+
}), recomputeStatus());
|
|
44
|
+
}
|
|
45
|
+
function leavePeer(e, t, n = !1) {
|
|
46
|
+
w.has(e.id) && (w.delete(e.id), e.status = n ? "failed" : "disconnected", e.pc?.close(), e.dc?.close(), S.emitTap({
|
|
47
|
+
peerId: e.id,
|
|
48
|
+
status: e.status,
|
|
49
|
+
type: "peer-status-change"
|
|
50
|
+
}), S.emitTap({
|
|
51
|
+
peer: e.publicPeer,
|
|
52
|
+
reason: t,
|
|
53
|
+
type: "peer-left"
|
|
54
|
+
}), n || (E = !0), D ||= n, recomputeStatus());
|
|
55
|
+
}
|
|
56
|
+
function wirePeerTransport(e, t, n) {
|
|
57
|
+
e.pc = t, e.dc = n, n.addEventListener("message", (t) => {
|
|
58
|
+
S.disposed || C.handleInbound(e, t.data);
|
|
59
|
+
}), n.addEventListener("close", () => {
|
|
60
|
+
S.disposed || leavePeer(e, "channel closed");
|
|
61
|
+
}), n.addEventListener("error", () => {
|
|
62
|
+
S.disposed || leavePeer(e, "channel error", !0);
|
|
63
|
+
}), t.addEventListener("iceconnectionstatechange", () => {
|
|
64
|
+
if (S.disposed) return;
|
|
65
|
+
let n = t.iceConnectionState;
|
|
66
|
+
S.emitTap({
|
|
67
|
+
peerId: e.id,
|
|
68
|
+
state: n,
|
|
69
|
+
type: "ice-state"
|
|
70
|
+
}), n === "failed" || n === "closed" ? leavePeer(e, `ice ${n}`, n === "failed") : n === "disconnected" && e.status === "connected" && setPeerStatus(e, "disconnected");
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function markPeerConnected(e) {
|
|
74
|
+
E = !0, w.set(e.id, e), setPeerStatus(e, "connected"), S.emitTap({
|
|
75
|
+
peer: e,
|
|
76
|
+
type: "peer-joined"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
async acceptAnswer(e) {
|
|
81
|
+
if (S.ensureLive(), e?.v !== 1) throw new n("Unsupported answer version");
|
|
82
|
+
if (!e.peer?.id) throw new n("Answer is missing a peer id");
|
|
83
|
+
let r = T.get(e.sessionId);
|
|
84
|
+
if (!r) throw new n("Unknown or expired session");
|
|
85
|
+
if (r.answered) throw new n("Invitation already answered");
|
|
86
|
+
if (w.has(e.peer.id)) throw new n(`Peer "${e.peer.id}" is already connected`);
|
|
87
|
+
if (g() >= r.expiresAt) throw expireInvitation(e.sessionId), new n("Invitation expired");
|
|
88
|
+
let i = await c(r.secret, e.sdp);
|
|
89
|
+
if (e.proof !== i) throw new n("Proof mismatch");
|
|
90
|
+
if (!(await m.approvePeer?.(e.peer) ?? !0)) throw S.emitTap({
|
|
91
|
+
peerId: e.peer.id,
|
|
92
|
+
type: "peer-rejected"
|
|
93
|
+
}), new n(`Peer "${e.peer.id}" rejected by host`);
|
|
94
|
+
if (S.emitTap({
|
|
95
|
+
peerId: e.peer.id,
|
|
96
|
+
type: "peer-approved"
|
|
97
|
+
}), w.has(e.peer.id)) throw new n(`Peer "${e.peer.id}" is already connected`);
|
|
98
|
+
r.answered = !0, T.delete(e.sessionId), clearTimeout(r.expiryTimer);
|
|
99
|
+
let a = s(e.peer.id, e.peer.name, "guest"), o = d(r.dc, r.pc, y, a.id, S.disposalCtrl.signal);
|
|
100
|
+
wirePeerTransport(a, r.pc, r.dc), w.set(a.id, a), recomputeStatus();
|
|
101
|
+
try {
|
|
102
|
+
await r.pc.setRemoteDescription({
|
|
103
|
+
sdp: u(e.sdp),
|
|
104
|
+
type: "answer"
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
throw leavePeer(a, "invalid answer sdp", !0), new n("Invalid answer SDP", { cause: e });
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await o;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
let n = !(e instanceof t);
|
|
113
|
+
throw leavePeer(a, e instanceof Error ? e.message : "open failed", n), e;
|
|
114
|
+
}
|
|
115
|
+
return markPeerConnected(a), a.publicPeer;
|
|
116
|
+
},
|
|
117
|
+
broadcast(e, t, n) {
|
|
118
|
+
S.ensureLive();
|
|
119
|
+
for (let r of w.values()) r.status !== "connected" || n?.except?.includes(r.id) || C.sendTo(r, e, t);
|
|
120
|
+
},
|
|
121
|
+
async createInvitation(n) {
|
|
122
|
+
S.ensureLive();
|
|
123
|
+
let a = h.createPeerConnection({ iceServers: [...m.iceServers ?? []] }), o = a.createDataChannel(x.label, {
|
|
124
|
+
maxRetransmits: x.maxRetransmits,
|
|
125
|
+
ordered: x.ordered
|
|
126
|
+
}), s = i(m.random), c = p(r(32, m.random)), l = g() + v;
|
|
127
|
+
try {
|
|
128
|
+
await a.setLocalDescription(await a.createOffer()), await f(a, b);
|
|
129
|
+
} catch (t) {
|
|
130
|
+
throw a.close(), new e("Failed to create offer", null, { cause: t });
|
|
131
|
+
}
|
|
132
|
+
if (S.disposed) throw a.close(), new t();
|
|
133
|
+
let u = a.localDescription?.sdp;
|
|
134
|
+
if (!u) throw a.close(), new e("Local description has no SDP");
|
|
135
|
+
let d = setTimeout(() => expireInvitation(s), v);
|
|
136
|
+
return T.set(s, {
|
|
137
|
+
answered: !1,
|
|
138
|
+
dc: o,
|
|
139
|
+
expiresAt: l,
|
|
140
|
+
expiryTimer: d,
|
|
141
|
+
pc: a,
|
|
142
|
+
secret: c,
|
|
143
|
+
sessionId: s
|
|
144
|
+
}), S.emitTap({
|
|
145
|
+
sessionId: s,
|
|
146
|
+
type: "invitation-created"
|
|
147
|
+
}), recomputeStatus(), {
|
|
148
|
+
expiresAt: l,
|
|
149
|
+
sdp: u,
|
|
150
|
+
secret: c,
|
|
151
|
+
sessionId: s,
|
|
152
|
+
v: 1,
|
|
153
|
+
...n?.name === void 0 ? {} : { hostName: n.name }
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
get disposalSignal() {
|
|
157
|
+
return S.disposalCtrl.signal;
|
|
158
|
+
},
|
|
159
|
+
dispose() {
|
|
160
|
+
if (!S.disposed) {
|
|
161
|
+
S.markDisposed();
|
|
162
|
+
for (let e of T.values()) clearTimeout(e.expiryTimer), e.pc.close();
|
|
163
|
+
T.clear();
|
|
164
|
+
for (let e of w.values()) e.dc?.close(), e.pc?.close();
|
|
165
|
+
w.clear(), C.clear();
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
get disposed() {
|
|
169
|
+
return S.disposed;
|
|
170
|
+
},
|
|
171
|
+
kick(t, n) {
|
|
172
|
+
S.ensureLive();
|
|
173
|
+
let r = w.get(t);
|
|
174
|
+
if (!r) throw new e(`Unknown peer "${t}"`, t);
|
|
175
|
+
leavePeer(r, n ?? "kicked");
|
|
176
|
+
},
|
|
177
|
+
on(e, t) {
|
|
178
|
+
return S.ensureLive(), C.on(e, t);
|
|
179
|
+
},
|
|
180
|
+
get peers() {
|
|
181
|
+
return w;
|
|
182
|
+
},
|
|
183
|
+
send(t, n, r) {
|
|
184
|
+
S.ensureLive();
|
|
185
|
+
let i = w.get(t);
|
|
186
|
+
if (!i) throw new e(`Unknown peer "${t}"`, t);
|
|
187
|
+
C.sendTo(i, n, r);
|
|
188
|
+
},
|
|
189
|
+
get status() {
|
|
190
|
+
return S.status;
|
|
191
|
+
},
|
|
192
|
+
tap(e, t) {
|
|
193
|
+
return S.tap(e, t);
|
|
194
|
+
},
|
|
195
|
+
[Symbol.dispose]() {
|
|
196
|
+
this.dispose();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
export { createMeshHost };
|
|
202
|
+
|
|
2
203
|
//# sourceMappingURL=host.js.map
|
package/dist/host.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.js","names":[],"sources":["../src/host.ts"],"sourcesContent":["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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"host.js","names":["rtc","options","nativeRtcFactory","clock","maxMessageBytes","invitationTtlMs","channelOpenTimeoutMs","iceGatheringTimeoutMs","channelInit","core","createNodeCore","messenger","createMessenger","peers","pending","sawPeer","sawFailure","next","peer","invitation","sessionId","status","failed","pc","dc","event","state","answer","MeshPairingError","expected","createProof","createPeerRecord","opened","waitChannelOpen","normalizeSdp","cause","error","MeshDisposedError","broadcastOptions","type","payload","randomId","secret","bytesToBase64Url","randomBytes","expiresAt","waitIceGathering","MeshConnectionError","sdp","expiryTimer","meta","peerId","reason","handler","tapOptions"],"sources":["../src/host.ts"],"sourcesContent":["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"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
import{MeshConnectionError as e,MeshDisposedError as t,MeshError as n,MeshPairingError as r,MeshPayloadError as i,MeshTimeoutError as a,MeshUnsupportedError as o
|
|
1
|
+
import { MeshConnectionError as e, MeshDisposedError as t, MeshError as n, MeshPairingError as r, MeshPayloadError as i, MeshTimeoutError as a, MeshUnsupportedError as o } from "./errors.js";
|
|
2
|
+
import { meshCodec as s } from "./codec.js";
|
|
3
|
+
import { createMeshGuest as c } from "./guest.js";
|
|
4
|
+
import { createMeshHost as l } from "./host.js";
|
|
5
|
+
import { meshQrCodec as u } from "./qr-codec.js";
|
|
6
|
+
export { e as MeshConnectionError, t as MeshDisposedError, n as MeshError, r as MeshPairingError, i as MeshPayloadError, a as MeshTimeoutError, o as MeshUnsupportedError, c as createMeshGuest, l as createMeshHost, s as meshCodec, u as meshQrCodec };
|