{"version":3,"file":"peer-mesh.cjs","names":[],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n    acceptCandidate,\n    adoptTransceivers,\n    attachLocalTracks,\n    createPeerConnection,\n    drainCandidates,\n    slotOf,\n    type PeerLink,\n} from \"./peer-link\";\nimport { createLinkStatsSampler, type LinkStatsSampler } from \"./link-stats\";\nimport type {\n    MeshMessage,\n    MeshPeer,\n    MeshQuality,\n    MeshStatsOptions,\n    PeerMeshOptions,\n} from \"./mesh-types\";\n\n/** A running mesh. */\nexport interface PeerMesh {\n    /**\n     * Open a link to a peer.\n     *\n     * `offerer` decides which side sends the offer, and exactly one side of each\n     * link must say `true`. Drive it from the arrival order your server already\n     * imposes — the peer that joined **later** offers, the one already in the\n     * room answers. That avoids glare by construction, without the rollback\n     * dance of perfect negotiation and without depending on a browser\n     * implementing argument-less `setLocalDescription`.\n     */\n    addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n    /** Close one link and give its share of the uplink back to the others. */\n    removePeer: (peerId: string) => void;\n    /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n    accept: (message: MeshMessage & { from: string }) => Promise<void>;\n    /**\n     * Publish or retract one slot across every live link.\n     *\n     * `null` retracts: the sender keeps its negotiated transceiver and stops\n     * producing, which is what mutes a microphone without disturbing the other\n     * slots or the connection. Nothing renegotiates — the m-lines were allocated\n     * before any track existed, which is what makes a mesh survive toggling: a\n     * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n     * somebody unmutes.\n     */\n    setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n    /** Apply encoder limits, divided by the current size of the room. */\n    applyQuality: (quality: MeshQuality) => Promise<void>;\n    /** Replace the ICE servers used for links opened from now on. */\n    setIceServers: (servers: RTCIceServer[]) => void;\n    /** Close every link and report `closed`. */\n    stop: () => void;\n    /** The peers as the mesh currently sees them. */\n    readonly peers: MeshPeer[];\n    /**\n     * The connection carrying one peer's link, or `null`.\n     *\n     * The escape hatch for everything this mesh does not model. `getStats()`,\n     * `getSenders()`, an `RTCDataChannel`, an encoding tweak the `quality`\n     * shape has no field for — all of it lives on the connection, and a mesh\n     * that keeps it private turns adopting the mesh into losing a feature.\n     *\n     * {@link MeshPeer.connection} is the *state*, which is what a view needs;\n     * this is the object, which is what a measurement needs. For the common\n     * case of measuring, {@link PeerMeshOptions.stats} spares you the loop —\n     * including the one-sampler-per-connection rule that a hand-rolled version\n     * gets wrong.\n     *\n     * @param peerId - The id the peer was added under.\n     * @returns The connection, or `null` when there is no link for that id.\n     */\n    getConnection: (peerId: string) => RTCPeerConnection | null;\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n *     slots: [\n *         { name: \"mic\", kind: \"audio\" },\n *         { name: \"cam\", kind: \"video\" },\n *     ],\n *     send: (message) => socket.send(message),\n *     onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n    const { slots, send } = options;\n    const links = new Map<string, PeerLink>();\n    const peers = new Map<string, MeshPeer>();\n    const localTracks = new Map<string, MediaStreamTrack | null>();\n    let iceServers = options.iceServers ?? [];\n    const describe =\n        options.setLocalDescription ??\n        ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n            connection.setLocalDescription(description));\n    let quality: MeshQuality = options.quality ?? {};\n    let stopped = false;\n    const samplers = new Map<string, LinkStatsSampler>();\n    let statsTimer: ReturnType<typeof setInterval> | null = null;\n\n    const emptyStreams = (): Record<string, MediaStream | null> =>\n        Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n    const emitPeers = (): void => {\n        options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n    };\n\n    /**\n     * Sample every connected link once.\n     *\n     * Only `connected` links are asked. A link still gathering candidates has\n     * no traffic to measure, and asking anyway spends a `getStats()` round trip\n     * to learn that — per link, on every tick.\n     *\n     * `settings` arrives as an argument rather than being read from `options`\n     * here: the timer is only ever armed past the check that they exist, so\n     * reading them again would add a branch that nothing can reach.\n     *\n     * @param settings - The measuring options, already known to be present.\n     */\n    const sampleLinks = async (settings: MeshStatsOptions): Promise<void> => {\n        if (stopped) return;\n        let changed = false;\n\n        for (const link of [...links.values()]) {\n            if (link.pc.connectionState !== \"connected\") continue;\n            let sampler = samplers.get(link.peerId);\n            if (sampler === undefined) {\n                sampler = createLinkStatsSampler(\n                    settings.kind === undefined ? {} : { kind: settings.kind },\n                );\n                samplers.set(link.peerId, sampler);\n            }\n            const stats = await sampler.sample(link.pc);\n            const peer = peers.get(link.peerId);\n            /*\n             * Belt and braces, and the belt is the state check above: a peer\n             * removed from inside `onStats` has its connection closed first, so\n             * the next iteration skips it there rather than here. This catches\n             * the entry disappearing any other way across the await.\n             */\n            if (peer === undefined) continue;\n            peer.stats = stats;\n            changed = true;\n            settings.onStats?.(link.peerId, stats);\n        }\n\n        if (changed) emitPeers();\n    };\n\n    /**\n     * Keep the timer alive exactly while there is something to measure.\n     *\n     * One timer for the whole mesh, not one per peer — and none at all in an\n     * empty room, which is where a call sits for as long as the first person is\n     * early.\n     */\n    const syncStatsTimer = (): void => {\n        const settings = options.stats;\n        if (settings === undefined) return;\n        const wanted = !stopped && links.size > 0;\n        if (wanted && statsTimer === null) {\n            statsTimer = setInterval(() => void sampleLinks(settings), settings.intervalMs ?? 2000);\n            return;\n        }\n        if (!wanted && statsTimer !== null) {\n            clearInterval(statsTimer);\n            statsTimer = null;\n        }\n    };\n\n    /**\n     * Collapse the per-link states into the one badge a call shows.\n     *\n     * No links at all is `connected`, not a failure: it means nobody else is in\n     * the room yet, which is a legitimate state a call sits in for as long as the\n     * first person is early.\n     */\n    const refreshState = (): void => {\n        if (stopped) return;\n        const states = [...links.values()].map((link) => link.pc.connectionState);\n        if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n        else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n        else options.onState?.(\"connected\", \"alone\");\n    };\n\n    const applyQuality = async (next: MeshQuality): Promise<void> => {\n        quality = next;\n        const effective = scaleForRoom(next, links.size);\n        const degradation = resolveDegradation(next, effective);\n        for (const link of links.values()) {\n            await applyQualityToLink(link.transceivers, slots, effective, degradation);\n        }\n    };\n\n    const makeOffer = async (link: PeerLink): Promise<void> => {\n        if (link.makingOffer) return;\n        link.makingOffer = true;\n        try {\n            const offer = await link.pc.createOffer();\n            if (!offer.sdp) {\n                options.onNotice?.(\"empty_local_offer_sdp\");\n                return;\n            }\n            await describe(link.pc, offer);\n            send({\n                type: \"offer\",\n                to: link.peerId,\n                sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n            });\n        } catch {\n            options.onNotice?.(\"offer_failed\");\n        } finally {\n            link.makingOffer = false;\n        }\n    };\n\n    /**\n     * Wrap an inbound track in the stream the caller renders.\n     *\n     * The event's own stream is preferred: it is the one the sending side\n     * grouped the track into, so a caller that reads `stream.id` sees what the\n     * peer published rather than a wrapper invented here. Browsers only fill it\n     * when the offer named a stream, hence the fallback — and the guard after\n     * that is for a runtime with no `MediaStream` at all, where routing still\n     * has to not throw.\n     */\n    const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n        const provided = event.streams?.[0];\n        if (provided) return provided;\n        return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n    };\n\n    const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n        const slot = slotOf(event.transceiver, link, slots);\n        const peer = peers.get(link.peerId);\n        if (!slot || !peer) return;\n\n        peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n        event.track.onended = (): void => {\n            const current = peers.get(link.peerId);\n            if (!current) return;\n            current.streams = { ...current.streams, [slot.name]: null };\n            emitPeers();\n        };\n        emitPeers();\n    };\n\n    const removePeer = (peerId: string): void => {\n        const link = links.get(peerId);\n        if (link) {\n            link.pc.close();\n            links.delete(peerId);\n        }\n        peers.delete(peerId);\n        samplers.delete(peerId);\n        void applyQuality(quality);\n        syncStatsTimer();\n        emitPeers();\n        refreshState();\n    };\n\n    const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n        if (links.has(peerId)) return;\n\n        const pc = createPeerConnection(iceServers);\n        const link: PeerLink = {\n            peerId,\n            pc,\n            transceivers: [],\n            pendingCandidates: [],\n            remoteReady: false,\n            makingOffer: false,\n            isOfferer: offerer,\n        };\n        links.set(peerId, link);\n        peers.set(peerId, {\n            peerId,\n            connection: pc.connectionState,\n            streams: peers.get(peerId)?.streams ?? emptyStreams(),\n        });\n\n        if (offerer) {\n            link.transceivers = slots.map((slot) =>\n                pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n            );\n            await attachLocalTracks(link, slots, localTracks);\n        }\n\n        pc.onicecandidate = (event): void => {\n            send({\n                type: \"ice\",\n                to: peerId,\n                candidate: event.candidate?.candidate ?? null,\n                sdpMid: event.candidate?.sdpMid ?? null,\n                sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n            });\n        };\n\n        pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n        pc.onconnectionstatechange = (): void => {\n            const peer = peers.get(peerId);\n            if (peer) peer.connection = pc.connectionState;\n            if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n                removePeer(peerId);\n                return;\n            }\n            emitPeers();\n            refreshState();\n        };\n\n        pc.onnegotiationneeded = (): void => {\n            if (link.isOfferer) void makeOffer(link);\n        };\n\n        syncStatsTimer();\n        emitPeers();\n        refreshState();\n        if (offerer) await makeOffer(link);\n    };\n\n    const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n        if (!links.has(from)) await addPeer(from, { offerer: false });\n        const link = links.get(from);\n        if (!link) return;\n\n        await link.pc.setRemoteDescription({ type: \"offer\", sdp });\n        link.remoteReady = true;\n        await drainCandidates(link);\n\n        if (!link.isOfferer) {\n            adoptTransceivers(link);\n            await attachLocalTracks(link, slots, localTracks);\n        }\n\n        const answer = await link.pc.createAnswer();\n        if (!answer.sdp) {\n            options.onNotice?.(\"empty_local_answer_sdp\");\n            return;\n        }\n        await describe(link.pc, answer);\n        send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n        await applyQuality(quality);\n    };\n\n    const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n        if (message.type === \"offer\") {\n            await acceptOffer(message.from, message.sdp);\n            return;\n        }\n\n        const link = links.get(message.from);\n        if (!link) return;\n\n        if (message.type === \"answer\") {\n            await link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n            link.remoteReady = true;\n            await drainCandidates(link);\n            await applyQuality(quality);\n            return;\n        }\n\n        if (message.candidate === null) return;\n        await acceptCandidate(link, {\n            candidate: message.candidate,\n            sdpMid: message.sdpMid ?? undefined,\n            sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n        });\n    };\n\n    const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n        localTracks.set(slot, track);\n        const index = slots.findIndex((entry) => entry.name === slot);\n        if (index < 0) return;\n\n        for (const link of links.values()) {\n            const transceiver = link.transceivers[index];\n            if (!transceiver) continue;\n            try {\n                await transceiver.sender.replaceTrack(track);\n            } catch {\n                /* the link is tearing down; its state handler will remove it */\n            }\n        }\n        await applyQuality(quality);\n    };\n\n    const stop = (): void => {\n        for (const link of links.values()) link.pc.close();\n        links.clear();\n        peers.clear();\n        samplers.clear();\n        stopped = true;\n        syncStatsTimer();\n        emitPeers();\n        options.onState?.(\"closed\");\n    };\n\n    options.onState?.(\"connecting\", \"signaling\");\n\n    return {\n        addPeer,\n        removePeer,\n        accept,\n        setLocalTrack,\n        applyQuality,\n        setIceServers: (servers): void => {\n            iceServers = servers;\n        },\n        stop,\n        getConnection: (peerId: string): RTCPeerConnection | null => links.get(peerId)?.pc ?? null,\n        get peers(): MeshPeer[] {\n            return [...peers.values()].map((peer) => ({ ...peer }));\n        },\n    };\n}\n"],"mappings":"iGA4GA,SAAgB,EAAe,EAAoC,CAC/D,GAAM,CAAE,QAAO,QAAS,EAClB,EAAQ,IAAI,IACZ,EAAQ,IAAI,IACZ,EAAc,IAAI,IACpB,EAAa,EAAQ,YAAc,CAAC,EAClC,EACF,EAAQ,uBACN,EAA+B,IAC7B,EAAW,oBAAoB,CAAW,GAC9C,EAAuB,EAAQ,SAAW,CAAC,EAC3C,EAAU,GACR,EAAW,IAAI,IACjB,EAAoD,KAElD,MACF,OAAO,YAAY,EAAM,IAAK,GAAS,CAAC,EAAK,KAAM,IAAI,CAAC,CAAC,EAEvD,MAAwB,CAC1B,EAAQ,UAAU,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,IAAK,IAAU,CAAE,GAAG,CAAK,EAAE,CAAC,CACtE,EAeM,EAAc,KAAO,IAA8C,CACrE,GAAI,EAAS,OACb,IAAI,EAAU,GAEd,IAAK,IAAM,IAAQ,CAAC,GAAG,EAAM,OAAO,CAAC,EAAG,CACpC,GAAI,EAAK,GAAG,kBAAoB,YAAa,SAC7C,IAAI,EAAU,EAAS,IAAI,EAAK,MAAM,EAClC,IAAY,IAAA,KACZ,EAAU,EAAA,uBACN,EAAS,OAAS,IAAA,GAAY,CAAC,EAAI,CAAE,KAAM,EAAS,IAAK,CAC7D,EACA,EAAS,IAAI,EAAK,OAAQ,CAAO,GAErC,IAAM,EAAQ,MAAM,EAAQ,OAAO,EAAK,EAAE,EACpC,EAAO,EAAM,IAAI,EAAK,MAAM,EAO9B,IAAS,IAAA,KACb,EAAK,MAAQ,EACb,EAAU,GACV,EAAS,UAAU,EAAK,OAAQ,CAAK,EACzC,CAEI,GAAS,EAAU,CAC3B,EASM,MAA6B,CAC/B,IAAM,EAAW,EAAQ,MACzB,GAAI,IAAa,IAAA,GAAW,OAC5B,IAAM,EAAS,CAAC,GAAW,EAAM,KAAO,EACxC,GAAI,GAAU,IAAe,KAAM,CAC/B,EAAa,gBAAkB,KAAK,EAAY,CAAQ,EAAG,EAAS,YAAc,GAAI,EACtF,MACJ,CACI,CAAC,GAAU,IAAe,OAC1B,cAAc,CAAU,EACxB,EAAa,KAErB,EASM,MAA2B,CAC7B,GAAI,EAAS,OACb,IAAM,EAAS,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,IAAK,GAAS,EAAK,GAAG,eAAe,EACpE,EAAO,KAAM,GAAU,IAAU,WAAW,EAAG,EAAQ,UAAU,WAAW,EACvE,EAAO,OAAS,EAAG,EAAQ,UAAU,aAAc,aAAa,EACpE,EAAQ,UAAU,YAAa,OAAO,CAC/C,EAEM,EAAe,KAAO,IAAqC,CAC7D,EAAU,EACV,IAAM,EAAY,EAAA,aAAa,EAAM,EAAM,IAAI,EACzC,EAAc,EAAA,mBAAmB,EAAM,CAAS,EACtD,IAAK,IAAM,KAAQ,EAAM,OAAO,EAC5B,MAAM,EAAA,mBAAmB,EAAK,aAAc,EAAO,EAAW,CAAW,CAEjF,EAEM,EAAY,KAAO,IAAkC,CACnD,MAAK,YACT,GAAK,YAAc,GACnB,GAAI,CACA,IAAM,EAAQ,MAAM,EAAK,GAAG,YAAY,EACxC,GAAI,CAAC,EAAM,IAAK,CACZ,EAAQ,WAAW,uBAAuB,EAC1C,MACJ,CACA,MAAM,EAAS,EAAK,GAAI,CAAK,EAC7B,EAAK,CACD,KAAM,QACN,GAAI,EAAK,OACT,IAAK,EAAK,GAAG,kBAAkB,KAAO,EAAM,GAChD,CAAC,CACL,MAAQ,CACJ,EAAQ,WAAW,cAAc,CACrC,QAAU,CACN,EAAK,YAAc,EACvB,CAjBmB,CAkBvB,EAYM,EAAa,GACE,EAAM,UAAU,KAE1B,OAAO,YAAgB,IAAc,KAAO,IAAI,YAAY,CAAC,EAAM,KAAK,CAAC,GAG9E,GAAqB,EAAgB,IAA+B,CACtE,IAAM,EAAO,EAAA,OAAO,EAAM,YAAa,EAAM,CAAK,EAC5C,EAAO,EAAM,IAAI,EAAK,MAAM,EAC7B,GAAS,IAEd,EAAK,QAAU,CAAE,GAAG,EAAK,SAAU,EAAK,MAAO,EAAU,CAAK,CAAE,EAChE,EAAM,MAAM,YAAsB,CAC9B,IAAM,EAAU,EAAM,IAAI,EAAK,MAAM,EAChC,IACL,EAAQ,QAAU,CAAE,GAAG,EAAQ,SAAU,EAAK,MAAO,IAAK,EAC1D,EAAU,EACd,EACA,EAAU,EACd,EAEM,EAAc,GAAyB,CACzC,IAAM,EAAO,EAAM,IAAI,CAAM,EACzB,IACA,EAAK,GAAG,MAAM,EACd,EAAM,OAAO,CAAM,GAEvB,EAAM,OAAO,CAAM,EACnB,EAAS,OAAO,CAAM,EACtB,EAAkB,CAAO,EACzB,EAAe,EACf,EAAU,EACV,EAAa,CACjB,EAEM,EAAU,MAAO,EAAgB,CAAE,aAAmD,CACxF,GAAI,EAAM,IAAI,CAAM,EAAG,OAEvB,IAAM,EAAK,EAAA,qBAAqB,CAAU,EACpC,EAAiB,CACnB,SACA,KACA,aAAc,CAAC,EACf,kBAAmB,CAAC,EACpB,YAAa,GACb,YAAa,GACb,UAAW,CACf,EACA,EAAM,IAAI,EAAQ,CAAI,EACtB,EAAM,IAAI,EAAQ,CACd,SACA,WAAY,EAAG,gBACf,QAAS,EAAM,IAAI,CAAM,CAAC,EAAE,SAAW,EAAa,CACxD,CAAC,EAEG,IACA,EAAK,aAAe,EAAM,IAAK,GAC3B,EAAG,eAAe,EAAK,KAAM,CAAE,UAAW,UAAW,CAAC,CAC1D,EACA,MAAM,EAAA,kBAAkB,EAAM,EAAO,CAAW,GAGpD,EAAG,eAAkB,GAAgB,CACjC,EAAK,CACD,KAAM,MACN,GAAI,EACJ,UAAW,EAAM,WAAW,WAAa,KACzC,OAAQ,EAAM,WAAW,QAAU,KACnC,cAAe,EAAM,WAAW,eAAiB,IACrD,CAAC,CACL,EAEA,EAAG,QAAW,GAAgB,EAAkB,EAAM,CAAK,EAE3D,EAAG,4BAAsC,CACrC,IAAM,EAAO,EAAM,IAAI,CAAM,EAE7B,GADI,IAAM,EAAK,WAAa,EAAG,iBAC3B,EAAG,kBAAoB,UAAY,EAAG,kBAAoB,SAAU,CACpE,EAAW,CAAM,EACjB,MACJ,CACA,EAAU,EACV,EAAa,CACjB,EAEA,EAAG,wBAAkC,CAC7B,EAAK,WAAW,EAAe,CAAI,CAC3C,EAEA,EAAe,EACf,EAAU,EACV,EAAa,EACT,GAAS,MAAM,EAAU,CAAI,CACrC,EAEM,EAAc,MAAO,EAAc,IAA+B,CAC/D,EAAM,IAAI,CAAI,GAAG,MAAM,EAAQ,EAAM,CAAE,QAAS,EAAM,CAAC,EAC5D,IAAM,EAAO,EAAM,IAAI,CAAI,EAC3B,GAAI,CAAC,EAAM,OAEX,MAAM,EAAK,GAAG,qBAAqB,CAAE,KAAM,QAAS,KAAI,CAAC,EACzD,EAAK,YAAc,GACnB,MAAM,EAAA,gBAAgB,CAAI,EAErB,EAAK,YACN,EAAA,kBAAkB,CAAI,EACtB,MAAM,EAAA,kBAAkB,EAAM,EAAO,CAAW,GAGpD,IAAM,EAAS,MAAM,EAAK,GAAG,aAAa,EAC1C,GAAI,CAAC,EAAO,IAAK,CACb,EAAQ,WAAW,wBAAwB,EAC3C,MACJ,CACA,MAAM,EAAS,EAAK,GAAI,CAAM,EAC9B,EAAK,CAAE,KAAM,SAAU,GAAI,EAAM,IAAK,EAAK,GAAG,kBAAkB,KAAO,EAAO,GAAI,CAAC,EACnF,MAAM,EAAa,CAAO,CAC9B,EAyDA,OAFA,EAAQ,UAAU,aAAc,WAAW,EAEpC,CACH,UACA,aACA,YA1DkB,IAA2D,CAC7E,GAAI,EAAQ,OAAS,QAAS,CAC1B,MAAM,EAAY,EAAQ,KAAM,EAAQ,GAAG,EAC3C,MACJ,CAEA,IAAM,EAAO,EAAM,IAAI,EAAQ,IAAI,EAC9B,KAEL,IAAI,EAAQ,OAAS,SAAU,CAC3B,MAAM,EAAK,GAAG,qBAAqB,CAAE,KAAM,SAAU,IAAK,EAAQ,GAAI,CAAC,EACvE,EAAK,YAAc,GACnB,MAAM,EAAA,gBAAgB,CAAI,EAC1B,MAAM,EAAa,CAAO,EAC1B,MACJ,CAEI,EAAQ,YAAc,MAC1B,MAAM,EAAA,gBAAgB,EAAM,CACxB,UAAW,EAAQ,UACnB,OAAQ,EAAQ,QAAU,IAAA,GAC1B,cAAe,EAAQ,eAAiB,IAAA,EAC5C,CAAC,CAPD,CAQJ,EAoCI,oBAlCyB,EAAc,IAAkD,CACzF,EAAY,IAAI,EAAM,CAAK,EAC3B,IAAM,EAAQ,EAAM,UAAW,GAAU,EAAM,OAAS,CAAI,EACxD,OAAQ,GAEZ,KAAK,IAAM,KAAQ,EAAM,OAAO,EAAG,CAC/B,IAAM,EAAc,EAAK,aAAa,GACjC,KACL,GAAI,CACA,MAAM,EAAY,OAAO,aAAa,CAAK,CAC/C,MAAQ,CAER,CACJ,CACA,MAAM,EAAa,CAAO,CAD1B,CAEJ,EAoBI,eACA,cAAgB,GAAkB,CAC9B,EAAa,CACjB,EACA,SAtBqB,CACrB,IAAK,IAAM,KAAQ,EAAM,OAAO,EAAG,EAAK,GAAG,MAAM,EACjD,EAAM,MAAM,EACZ,EAAM,MAAM,EACZ,EAAS,MAAM,EACf,EAAU,GACV,EAAe,EACf,EAAU,EACV,EAAQ,UAAU,QAAQ,CAC9B,EAcI,cAAgB,GAA6C,EAAM,IAAI,CAAM,CAAC,EAAE,IAAM,KACtF,IAAI,OAAoB,CACpB,MAAO,CAAC,GAAG,EAAM,OAAO,CAAC,CAAC,CAAC,IAAK,IAAU,CAAE,GAAG,CAAK,EAAE,CAC1D,CACJ,CACJ"}