node-datachannel icon indicating copy to clipboard operation
node-datachannel copied to clipboard

Intermittent data loss

Open achingbrain opened this issue 3 months ago • 1 comments

If a datachannel sends data and then immediately closes the connection:

while (...) {
  dc.send(buf)
}

dc.close()

If it sends a lot of small messages then the remote does not always receive all of the data.

I've opened https://github.com/murat-dogan/node-datachannel/pull/374 which has a failing test showing the behaviour.

There's a codepen here which allows running the same code in a browser, it works in Chrome and Firefox Nightly so this appears to be a bug in node-datachannel or libdatachannel.

achingbrain avatar Sep 17 '25 16:09 achingbrain

Here's a reproduction without using the polyfill API, if it's useful:

import { nodeDataChannel } from 'node-datachannel'

// Log Level
// nodeDataChannel.initLogger("Debug")

// increase `chunks` until `Peer2` no longer receives all of the messages
const chunks = 1024 * 10
// the number of successfully sent messages
let bytesSent = 0
// the number of successfully received messages
let receivedBytes = 0
// how many bytes to send in each message
const chunkSize = 1024
// how many bytes to send
const bytesToSend = chunks * chunkSize

const receivedAllBytes = Promise.withResolvers()

const peer1 = new nodeDataChannel.PeerConnection('Peer1', { iceServers: [] })
const peer2 = new nodeDataChannel.PeerConnection('Peer2', { iceServers: [] })

peer1.onLocalDescription((sdp, type) => {
  peer2.setRemoteDescription(sdp, type)
})
peer1.onLocalCandidate((candidate, mid) => {
  peer2.addRemoteCandidate(candidate, mid)
})

peer2.onLocalDescription((sdp, type) => {
  peer1.setRemoteDescription(sdp, type)
})
peer2.onLocalCandidate((candidate, mid) => {
  peer1.addRemoteCandidate(candidate, mid)
})

const start = Date.now()

// necessary to stop dc from being garbage collected
let peer2Dc

peer2.onDataChannel((dc) => {
  peer2Dc = dc

  console.log('Peer2 Got DataChannel', dc.getLabel())

  dc.onMessage((buf) => {
    receivedBytes += buf.byteLength

    if (receivedBytes === bytesToSend) {
      receivedAllBytes.resolve()
    }
  })
  dc.onClosed(() => {
    if (receivedBytes !== bytesSent) {
      receivedAllBytes.reject(new Error(`Peer2 channel closed after only receiving ${receivedBytes}/${bytesToSend} bytes`))
    }

    peer2Dc = null
  })
})

const dc = peer1.createDataChannel('test channel')

dc.onOpen(async () => {
  for (let i = 0; i < chunks; i++) {
    dc.sendMessageBinary(Uint8Array.from(new Array(chunkSize).fill(0)))
    bytesSent++
  }

  console.info('Peer1 sent all bytes, closing channel')
  dc.close()
})

receivedAllBytes.promise
  .then(() => {
    console.log(`Peer2 received all ${bytesToSend} bytes after ${Date.now() - start} ms`)
  })
  .finally(() => {
    peer1.close()
    peer2.close()
    nodeDataChannel.cleanup()
  })

achingbrain avatar Sep 18 '25 06:09 achingbrain