# Offer / Answer Primitives _(no submodules)_ ## Purpose The offer/answer module exposes three pure async wrappers around the spec-standard WebRTC handshake calls. `createOffer` covers the outbound half (local offer + `setLocalDescription`), `handleOffer` covers the inbound half (`setRemoteDescription` + answer + `setLocalDescription`), and `handleAnswer` finalises an outbound handshake by applying the remote answer. Each helper takes an `RTCPeerConnection` plus an optional `AbortSignal` and never owns peer state itself — that lives in `WebRTCClient`. Splitting these out keeps `WebRTCClient` thin and testable. The handshake logic is a deterministic three- or four-step pipeline that maps beautifully to property tests against a structural `FakeRTCPeerConnection`; the orchestrator can then focus on lifecycle, eventing, and connection caching without having to re-stub the same `pc.createOffer().then(...)` chain in every code path. ## Features - Spec-faithful call ordering: `createOffer → setLocalDescription`, `setRemoteDescription → createAnswer → setLocalDescription`. - Every failure surfaces as `DriveWebRtcError(PEER_HANDSHAKE_FAILED)` with the underlying error preserved via `cause`. - `AbortSignal` is checked **before** every async step, so a pre-aborted signal rejects immediately and a signal aborted mid-handshake rejects at the next step boundary. - Pure functions over an injected `RTCPeerConnection` — no module-level state, no globals, no hidden allocations. ## Usage: Basic Example ```ts import { createOffer, handleAnswer } from "@suaveplan/drive-webrtc"; const pc = new RTCPeerConnection(); const offer = await createOffer(pc); sendToPeer({ type: "offer", payload: offer }); const answer = await receiveAnswerFromPeer(); await handleAnswer(pc, answer); ``` ## Usage: Advanced Example ```ts import { createOffer, handleOffer } from "@suaveplan/drive-webrtc"; const pc = new RTCPeerConnection({ iceServers }); // Inbound caller — respond to a remote offer with an abort budget. const ac = new AbortController(); setTimeout(() => ac.abort(), 5_000); try { const answer = await handleOffer(pc, remoteOffer, ac.signal); sendToPeer({ type: "answer", payload: answer }); } catch (err) { if (err instanceof DriveWebRtcError && err.code === DRIVE_WEBRTC_CODES.PEER_HANDSHAKE_FAILED) { // Handshake failed — log + tear down the peer connection. } throw err; } // Outbound caller — create an offer and survive a transient // setLocalDescription error. try { const offer = await createOffer(pc); sendToPeer({ type: "offer", payload: offer }); } catch (err) { pc.close(); throw err; } ``` ## API Reference ### `createOffer(pc, signal?): Promise` Creates an SDP offer on `pc`, applies it as local description, returns the offer. Throws `DriveWebRtcError(PEER_HANDSHAKE_FAILED)` on abort or any step failure (original error preserved via `cause`). ### `handleOffer(pc, offer, signal?): Promise` Applies `offer` as remote description, generates an answer, applies it as local description, returns the answer. Same error semantics as `createOffer`. ### `handleAnswer(pc, answer, signal?): Promise` Applies `answer` as remote description on a `pc` that has already sent an offer. Same error semantics as `createOffer`. ## Implementation Notes - Abort checks happen between every awaited step so an abort that lands during `createOffer` does not silently proceed into `setLocalDescription`. - The wrapper never mutates `pc` outside of the documented calls and never registers event listeners — handler wiring is the orchestrator's responsibility (see `client.ts`). - The structural typing of `RTCPeerConnection` means these helpers work unchanged against the project's `FakeRTCPeerConnection` test double; the no-mocks rule is satisfied by injection at the call site, not by monkey-patching globals.