Files
skills/docs/networking/drive/drive-webrtc/src/client/offer-answer/offer-answer.md
2026-05-29 08:20:56 +01:00

3.9 KiB

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

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

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<RTCSessionDescriptionInit>

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<RTCSessionDescriptionInit>

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<void>

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.