{"version":3,"file":"peer-link.cjs","names":[],"sources":["../../src/webrtc/peer-link.ts"],"sourcesContent":["import type { MeshSlot } from \"./mesh-types\";\n\n/** One remote peer's connection and everything the mesh tracks about it. */\nexport interface PeerLink {\n    peerId: string;\n    pc: RTCPeerConnection;\n    /**\n     * Transceivers in slot order.\n     *\n     * Held explicitly rather than re-read from `getTransceivers()` so slot\n     * lookup stays O(1) and cannot drift if a browser reorders.\n     */\n    transceivers: RTCRtpTransceiver[];\n    /** Candidates that arrived before the remote description they belong to. */\n    pendingCandidates: RTCIceCandidateInit[];\n    /** Whether `setRemoteDescription` has landed, which gates the queue. */\n    remoteReady: boolean;\n    /** Whether an offer is in flight, so a re-entrant call does not stack one. */\n    makingOffer: boolean;\n    /** Whether this side offers. Exactly one side of a link does. */\n    isOfferer: boolean;\n}\n\n/** Open a connection with the policies a mesh needs. */\nexport function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {\n    return new RTCPeerConnection({\n        iceServers,\n        bundlePolicy: \"max-bundle\",\n        rtcpMuxPolicy: \"require\",\n    });\n}\n\n/**\n * Resolve which slot a transceiver carries.\n *\n * The offerer allocates the m-lines in slot order, so a transceiver's `mid` is\n * its ordinal and both ends read the same one. That makes `mid` the slot's wire\n * identity, and the **only** identifier correct on both sides of a link.\n *\n * Position in the link's own array is a fallback for a `mid` that is not a plain\n * ordinal, and never the primary: a browser answering an offer appends the\n * negotiated transceivers *after* any it already held, so positional lookup\n * silently resolves to the wrong slot — the camera arriving in the screen tile —\n * or to none at all, dropping the track.\n *\n * @param transceiver - The transceiver a track arrived on.\n * @param link - The link it belongs to.\n * @param slots - The mesh's slot list.\n * @returns The slot, or `null` when neither route names one.\n */\nexport function slotOf(\n    transceiver: RTCRtpTransceiver,\n    link: PeerLink,\n    slots: readonly MeshSlot[],\n): MeshSlot | null {\n    const ordinal = Number(transceiver.mid);\n    if (transceiver.mid !== null && Number.isInteger(ordinal) && slots[ordinal]) {\n        return slots[ordinal];\n    }\n    const index = link.transceivers.indexOf(transceiver);\n    return index >= 0 ? (slots[index] ?? null) : null;\n}\n\n/**\n * Feed a link every candidate that arrived before its remote description.\n *\n * `addIceCandidate` throws while `remoteDescription` is `null`, and candidates\n * routinely beat the offer or answer they belong to. Without the queue the\n * connection loses them and sits in `checking` until it times out — a failure\n * with no error anywhere.\n *\n * @param link - The link whose queue is drained.\n */\nexport async function drainCandidates(link: PeerLink): Promise<void> {\n    const queued = link.pendingCandidates.splice(0, link.pendingCandidates.length);\n    for (const candidate of queued) {\n        try {\n            await link.pc.addIceCandidate(candidate);\n        } catch {\n            /* a candidate the browser rejects is not fatal to the connection */\n        }\n    }\n}\n\n/**\n * Hand a candidate to the connection, or park it until the description lands.\n *\n * @param link - The link the candidate belongs to.\n * @param init - The candidate, already in `RTCIceCandidateInit` shape.\n */\nexport async function acceptCandidate(link: PeerLink, init: RTCIceCandidateInit): Promise<void> {\n    if (!link.remoteReady) {\n        link.pendingCandidates.push(init);\n        return;\n    }\n    try {\n        await link.pc.addIceCandidate(init);\n    } catch {\n        /* a candidate the browser rejects is not fatal to the connection */\n    }\n}\n\n/**\n * Publish every held local track onto a link's slots.\n *\n * Called once the transceivers exist — up front for the offerer, after the offer\n * lands for the answerer — so a microphone that was already live when the peer\n * joined starts flowing without waiting for a toggle.\n *\n * @param link - The link to publish onto.\n * @param slots - The mesh's slot list.\n * @param tracks - The current local track per slot name.\n */\nexport async function attachLocalTracks(\n    link: PeerLink,\n    slots: readonly MeshSlot[],\n    tracks: Map<string, MediaStreamTrack | null>,\n): Promise<void> {\n    for (const [index, slot] of slots.entries()) {\n        const transceiver = link.transceivers[index];\n        const track = tracks.get(slot.name) ?? null;\n        if (!transceiver || !track) continue;\n        try {\n            await transceiver.sender.replaceTrack(track);\n        } catch {\n            /* the browser refused the track; the slot simply stays silent */\n        }\n    }\n}\n\n/**\n * Adopt the transceivers a remote offer created, in `mid` order.\n *\n * The answering side allocates **nothing** up front, and that is the whole\n * subtlety: applying a remote offer creates one transceiver per m-line, in\n * m-line order, and browsers do not reuse transceivers the answerer made\n * beforehand. Pre-allocating leaves dead, never-negotiated transceivers sitting\n * in front of the live ones, which is how slot lookup ends up pointing at the\n * wrong media or at nothing.\n *\n * Each adopted transceiver is switched to `sendrecv` so this side may publish on\n * it too; without that a peer that answered could hear but never be heard.\n *\n * @param link - The link that just applied a remote offer.\n */\nexport function adoptTransceivers(link: PeerLink): void {\n    link.transceivers = link.pc\n        .getTransceivers()\n        .filter((transceiver) => transceiver.mid !== null)\n        .sort((a, b) => Number(a.mid) - Number(b.mid));\n    for (const transceiver of link.transceivers) {\n        if (transceiver.direction !== \"sendrecv\") transceiver.direction = \"sendrecv\";\n    }\n}\n"],"mappings":"AAwBA,SAAgB,EAAqB,EAA+C,CAChF,OAAO,IAAI,kBAAkB,CACzB,aACA,aAAc,aACd,cAAe,SACnB,CAAC,CACL,CAoBA,SAAgB,EACZ,EACA,EACA,EACe,CACf,IAAM,EAAU,OAAO,EAAY,GAAG,EACtC,GAAI,EAAY,MAAQ,MAAQ,OAAO,UAAU,CAAO,GAAK,EAAM,GAC/D,OAAO,EAAM,GAEjB,IAAM,EAAQ,EAAK,aAAa,QAAQ,CAAW,EACnD,OAAO,GAAS,EAAK,EAAM,IAAU,KAAQ,IACjD,CAYA,eAAsB,EAAgB,EAA+B,CACjE,IAAM,EAAS,EAAK,kBAAkB,OAAO,EAAG,EAAK,kBAAkB,MAAM,EAC7E,IAAK,IAAM,KAAa,EACpB,GAAI,CACA,MAAM,EAAK,GAAG,gBAAgB,CAAS,CAC3C,MAAQ,CAER,CAER,CAQA,eAAsB,EAAgB,EAAgB,EAA0C,CAC5F,GAAI,CAAC,EAAK,YAAa,CACnB,EAAK,kBAAkB,KAAK,CAAI,EAChC,MACJ,CACA,GAAI,CACA,MAAM,EAAK,GAAG,gBAAgB,CAAI,CACtC,MAAQ,CAER,CACJ,CAaA,eAAsB,EAClB,EACA,EACA,EACa,CACb,IAAK,GAAM,CAAC,EAAO,KAAS,EAAM,QAAQ,EAAG,CACzC,IAAM,EAAc,EAAK,aAAa,GAChC,EAAQ,EAAO,IAAI,EAAK,IAAI,GAAK,KACnC,GAAC,GAAgB,EACrB,GAAI,CACA,MAAM,EAAY,OAAO,aAAa,CAAK,CAC/C,MAAQ,CAER,CACJ,CACJ,CAiBA,SAAgB,EAAkB,EAAsB,CACpD,EAAK,aAAe,EAAK,GACpB,gBAAgB,CAAC,CACjB,OAAQ,GAAgB,EAAY,MAAQ,IAAI,CAAC,CACjD,MAAM,EAAG,IAAM,OAAO,EAAE,GAAG,EAAI,OAAO,EAAE,GAAG,CAAC,EACjD,IAAK,IAAM,KAAe,EAAK,aACvB,EAAY,YAAc,aAAY,EAAY,UAAY,WAE1E"}