Repository navigation
Phase B2: WebRTC connector over libdatachannel - #76
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'srtc::PeerConnection/rtc::DataChannel. TS'snode-datachannelwraps the same library, so the offer/answer/ICE signalling and callback semantics carry over closely.New modules
webrtcTypes—IceServer+iceServerAsString(produces the URL formrtc::IceServerparses), thelocalDescription/localCandidatesignalling events (a separate member emitter, since theConnectionbase fixes its event tuple),EARLY_TIMEOUT,WebrtcConnectionParams.WebrtcConnection— one data-channel connection.EARLY_TIMEOUTvia anAbortableTimersweak-self watchdog; rtc callbacks guarded bymMutex, with all call-outs and the rtcclose()performed outside it.WebrtcConnectorRpcRemote/WebrtcConnectorRpcLocal— therequestConnection/rtcOffer/rtcAnswer/iceCandidatesignalling notifications. The remoteco_awaits the generated client (the lazy-task trap); the local drivesconnect/setRemoteDescription.WebrtcConnector— the XOR-id offerer tie-break (OffererHelper),ongoingConnectAttemptsdedupe, handshaker bookkeeping, and detached signalling on a serial view of the shared worker pool drained by aGuardedAsyncScopeinstop()(the PR Shared executor pools: fix the thread-per-node explosion behind the red macOS CI leg #75 executor architecture).replaceInternalIpWithExternalIpforexternalIpcandidate rewriting.DefaultConnectorFacade—createConnectionnow falls through to theWebrtcConnector;iceServers/allowPrivateAddresses/ buffer-threshold /externalIp/webrtcPortRangeoptions 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:
close()self-deadlock.rtc::PeerConnection::close()/resetCallbacks()block until the current callback returns. Called inline fromonStateChange(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 rtcshared_ptrs.rtcOfferafterconnect(); a value captured at connect time made the answer carry the wrong id → "connectionId mismatch" → the handshake never completed. The signalling callbacks now readgetConnectionID()live at emit time.rtcOfferandiceCandidatenotifications 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 insetRemoteDescription) took the rpc-over-webrtc case from 4/10 to 14/15.doClose. It emittedDisconnectedwhile holdingmMutex, racing rtc'sonStateChangewhich wantsmMutex; the emit/removeAllListenersnow run outside the lock (the phase-A0 "no call-outs under a connection mutex" policy).Also:
FakeTransportheld itslocalPeerDescriptorby reference and dangled when a caller passed a temporary (BUS error insend()); 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 throughclose(false)rather than a bare re-entrantemit— C++'sEventEmittermutex is non-recursive, so a handler that re-emits on the same emitter self-deadlocks (a JS-only assumption;close()'sstoppedguard breaks the cascade, which is why the real disconnect paths and all integration tests are fine).ctest --repeat until-pass:2absorbs.Notes
main.🤖 Generated with Claude Code