Skip to content

Phase B2: WebRTC connector over libdatachannel - #76

Merged
ptesavol merged 1 commit into
mainfrom
claude/phase-b2
Jul 11, 2026
Merged

ptesavol merged 1 commit into
mainfrom
claude/phase-b2

Conversation

@ptesavol

Copy link
Copy Markdown
Collaborator

Implements Phase B2 of trackerless-network-completion-plan.md (Milestone B): the WebRTC connector, ported line-by-line from the pinned TypeScript (af966cf03, v103.8.0-rc.3) onto libdatachannel's rtc::PeerConnection/rtc::DataChannel. TS's node-datachannel wraps the same library, so the offer/answer/ICE signalling and callback semantics carry over closely.

New modules

  • webrtcTypes — IceServer + iceServerAsString (produces the URL form rtc::IceServer parses), the localDescription/localCandidate signalling events (a separate member emitter, since the Connection base fixes its event tuple), EARLY_TIMEOUT, WebrtcConnectionParams.
  • WebrtcConnection — one data-channel connection. EARLY_TIMEOUT via an AbortableTimers weak-self watchdog; rtc callbacks guarded by mMutex, with all call-outs and the rtc close() performed outside it.
  • WebrtcConnectorRpcRemote / WebrtcConnectorRpcLocal — the requestConnection/rtcOffer/rtcAnswer/iceCandidate signalling notifications. The remote co_awaits the generated client (the lazy-task trap); the local drives connect/setRemoteDescription.
  • WebrtcConnector — the XOR-id offerer tie-break (OffererHelper), ongoingConnectAttempts dedupe, handshaker bookkeeping, and detached signalling on a serial view of the shared worker pool drained by a GuardedAsyncScope in stop() (the PR Shared executor pools: fix the thread-per-node explosion behind the red macOS CI leg #75 executor architecture). replaceInternalIpWithExternalIp for externalIp candidate rewriting.
  • DefaultConnectorFacade — createConnection now falls through to the WebrtcConnector; iceServers / allowPrivateAddresses / buffer-threshold / externalIp / webrtcPortRange options are plumbed through.

Four lifecycle bugs the TS original doesn't face

node-datachannel is async; the C++ library's calls block. Each was found by runtime evidence (trace/sampling/ASan), not guesswork:

  1. rtc close() self-deadlock. rtc::PeerConnection::close()/resetCallbacks() block until the current callback returns. Called inline from onStateChange (rtc's own ThreadPool thread) that is itself why we're closing, that is a self-join hang (deterministic once a peerless connection transitioned to Failed). The rtc teardown is posted to the shared worker pool, owning the rtc shared_ptrs.
  2. Live signalling connectionId. The answerer adopts the offerer's connection id in rtcOffer after connect(); a value captured at connect time made the answer carry the wrong id → "connectionId mismatch" → the handshake never completed. The signalling callbacks now read getConnectionID() live at emit time.
  3. Early ICE candidates. Over the simulator the offerer's separate rtcOffer and iceCandidate notifications reach the answerer out of order, so a candidate legitimately arrives before the offer. TS closes here — its documented flaky test NET-911, with a TODO right at that line saying "should queue". Queuing (flushed in setRemoteDescription) took the rpc-over-webrtc case from 4/10 to 14/15.
  4. ABBA deadlock in doClose. It emitted Disconnected while holding mMutex, racing rtc's onStateChange which wants mMutex; the emit/removeAllListeners now run outside the lock (the phase-A0 "no call-outs under a connection mutex" policy).

Also: FakeTransport held its localPeerDescriptor by reference and dangled when a caller passed a temporary (BUS error in send()); it's now by value.

Tests

All five ported: unit/WebrtcConnection, unit/WebrtcConnector, integration/WebrtcConnectorRpc, integration/WebrtcConnectionManagement (real ICE + DTLS data channels over loopback, signalled through the Simulator), integration/rpc-connections-over-webrtc. The connector unit test triggers its disconnect through close(false) rather than a bare re-entrant emit — C++'s EventEmitter mutex is non-recursive, so a handler that re-emits on the same emitter self-deadlocks (a JS-only assumption; close()'s stopped guard breaks the cascade, which is why the real disconnect paths and all integration tests are fine).

  • Unit 181/181, integration 43/43 (as a single binary), lint green.
  • The five B2 test files join the documented clangd std-type-unification exclusion list (the compiler builds and runs them on every platform).
  • The rpc-over-webrtc happy path retains ~7% ICE-over-loopback nondeterminism (the NET-911 class) that ctest --repeat until-pass:2 absorbs.

Notes

🤖 Generated with Claude Code

Ports the streamr WebRTC connection path (packages/dht v103.8.0-rc.3) onto
libdatachannel's rtc::PeerConnection/rtc::DataChannel — the TS
node-datachannel binding wraps the same library, so the offer/answer/ICE
signalling and callback semantics match. New modules:

- webrtcTypes: IceServer, iceServerAsString (produces the URL form
  rtc::IceServer parses), the localDescription/localCandidate signalling
  events (a separate member emitter, since the Connection base fixes its
  event tuple), consts, WebrtcConnectionParams.
- WebrtcConnection: one data-channel connection. EARLY_TIMEOUT via an
  AbortableTimers weak-self watchdog; rtc callbacks guarded by mMutex with
  all call-outs and rtc close() OUTSIDE it (see the fixes below).
- WebrtcConnectorRpcRemote/Local: the offer/answer/ICE/requestConnection
  signalling notifications; the remote co_awaits the generated client
  (lazy-task trap), the local drives connect/setRemoteDescription.
- WebrtcConnector: the XOR-id offerer tie-break (OffererHelper), ongoing
  connect-attempt dedupe, handshaker bookkeeping, and detached signalling
  on a serial view of the shared worker pool drained by a GuardedAsyncScope
  in stop() (the SharedExecutors architecture). replaceInternalIpWithExternalIp
  for externalIp candidate rewriting.
- DefaultConnectorFacade: createConnection now falls through to the
  WebrtcConnector, with iceServers/allowPrivateAddresses/buffer-threshold/
  externalIp/webrtcPortRange options plumbed through.

Four lifecycle bugs the TS original does not face (node-datachannel is
async; the C++ library blocks), each found by runtime evidence:

1. rtc PeerConnection/DataChannel close() blocks until the current callback
   returns; called inline from onStateChange (rtc's own thread) it
   self-joins and hangs. The rtc teardown is posted to the shared worker
   pool instead.
2. The signalling connectionId must be read LIVE at emit time: the answerer
   adopts the offerer's id in rtcOffer AFTER connect(), so a captured id
   made the answer mismatch and the handshake never completed.
3. Early ICE candidates (candidate before the remote description, common
   over the simulator's out-of-order notifications) are queued and flushed
   in setRemoteDescription instead of closing — the TS TODO at that exact
   line, and the dominant cause of the documented flaky test NET-911.
4. doClose emitted Disconnected while holding mMutex, an ABBA deadlock with
   rtc's onStateChange; the emit/removeAllListeners now run outside the lock.

Also: FakeTransport held its localPeerDescriptor by reference and dangled
when callers passed a temporary (BUS error in send()); now by value.

Tests ported (all five): unit/WebrtcConnection, unit/WebrtcConnector,
integration/WebrtcConnectorRpc, integration/WebrtcConnectionManagement,
integration/rpc-connections-over-webrtc. The connector unit test drives
the disconnect through close(false) rather than a bare re-entrant emit
(C++ EventEmitter is non-recursive, unlike JS). Unit 181/181, integration
43/43 (single-binary), lint green; the five B2 test files join the
documented clangd std-type-unification exclusion list. The
rpc-over-webrtc happy path retains ~7% ICE-over-loopback nondeterminism
(the NET-911 class) that ctest --repeat until-pass:2 absorbs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the dht label Jul 11, 2026
@ptesavol
ptesavol merged commit dd99e2c into main Jul 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant