Skip to content

Conversation

@mertushka
Copy link
Contributor

As identified here: #326 (comment) and mertushka/haxball.js#64 (comment)

This fixes a race condition where <RTCDataChannel>.readyState could remain stuck in 'open' even after the underlying native DataChannel was closed. It also improves typings a bit.

@mertushka
Copy link
Contributor Author

Test Case

import { RTCPeerConnection } from "node-datachannel/polyfill";

const peer1 = new RTCPeerConnection();
const peer2 = new RTCPeerConnection();

peer2.addEventListener("datachannel", (e) => {
  const channel = e.channel;
  channel.binaryType = "arraybuffer";
  channel.addEventListener("message", (evt) => {
    // echo back
    if (channel.readyState === "open") channel.send(evt.data);
  });
});

const channel = peer1.createDataChannel("race");
await connectPeers(peer1, peer2);
await waitForOpen(channel);

let tick = 0;
let sent = 0;
let interval;

channel.binaryType = "arraybuffer";

// Start sending every tick
interval = setInterval(() => {
  console.log("readyState: ", channel.readyState);
  if (channel.readyState === "open") {
    try {
      channel.send(new Uint8Array(128));
      sent++;
    } catch (err) {
      console.error(`Tick ${tick}: Send failed despite readyState === "open"`);
      console.error("Error:", err.message);
      clearInterval(interval);
      cleanup();
    }
  } else {
    console.warn(`Tick ${tick}: Not open, readyState is`, channel.readyState);
    clearInterval(interval);
    cleanup();
  }

  tick++;

  // Simulate sudden closure of peer2 after a few ticks
  if (tick === 200) {
    console.warn(">> Closing peer2 abruptly");
    peer2.close();
  }
}, 0);

function cleanup() {
  console.log(`Sent ${sent} messages before failure`);
  peer1.close();
}

async function connectPeers(peer1, peer2) {
  const offer = await peer1.createOffer();
  await peer2.setRemoteDescription(offer);

  const answer = await peer2.createAnswer();
  await peer1.setRemoteDescription(answer);

  peer1.addEventListener("icecandidate", (e) => {
    peer2.addIceCandidate(e.candidate);
  });

  peer2.addEventListener("icecandidate", (e) => {
    peer1.addIceCandidate(e.candidate);
  });

  await Promise.all([waitForConnection(peer1), waitForConnection(peer2)]);
}

function waitForConnection(peer) {
  return new Promise((resolve, reject) => {
    peer.addEventListener("connectionstatechange", () => {
      if (peer.connectionState === "connected") resolve();
      if (peer.connectionState === "failed") reject(new Error("ICE failed"));
    });
  });
}

function waitForOpen(dc) {
  return new Promise((resolve, reject) => {
    if (dc.readyState === "open") return resolve();
    dc.addEventListener("open", () => resolve());
    dc.addEventListener("error", () => reject(new Error("DC failed")));
  });
}

Before Fix

readyState: open
>> Closing peer2 abruptly
readyState: open
❌ Error: libdatachannel error while sending data channel message: DataChannel is closed

After Fix

readyState: open
>> Closing peer2 abruptly
readyState: closed
✅ Tick 200: Not open, readyState is closed

@mertushka
Copy link
Contributor Author

@murat-dogan This is ready to merge after you review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant