Fix file transfer, signaling reliability, and DC callback races
- wireFileRecv: replace OnMessage/OnClose channel approach with pion Detach API so SCTP buffers data regardless of callback timing — fixes 0-byte files caused by pion dispatching data goroutines before OnMessage was registered - yaw DC: same Detach API treatment for the control channel - connectTo: use DetachDataChannels SettingEngine, replace stale PCs (Connecting/Failed/Disconnected state) instead of returning early; isCurrent guard in OnConnectionStateChange prevents stale-PC close from firing OnDisconnected - offerByOrder: gate maybeOffer on peer ID order so only one side creates the yaw DC and SDP offer — fixes signaling glare causing triple connected (verified) logs - pendingRecvs: promote single pendingRecv slot to map[string]*recvState so concurrent file offers don't overwrite each other - peer-left: stop calling resetPeer() so active file DCs aren't closed before data arrives - signaling ping: wrap conn.Ping with 10s timeout context so a dead TCP connection is detected within 30s rather than hanging forever - signaling read: 3-minute read deadline forces reconnect if anchor silently evicts the peer from the room (idle timeout) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -400,7 +400,8 @@ export class PeerConn {
|
||||
this.events.fileOffer?.({ peer: this.peerId, xid: m['xid'] as string, name: m['name'] as string, size: m['size'] as number })
|
||||
} else if (m['type'] === 'file-accept') {
|
||||
const xid = m['xid'] as string
|
||||
if (this._pushQueue.has(xid)) void this._stream(xid)
|
||||
const offset = typeof m['offset'] === 'string' ? parseInt(m['offset'] as string, 10) || 0 : 0
|
||||
if (this._pushQueue.has(xid)) void this._stream(xid, offset)
|
||||
} else if (m['type'] === 'file-cancel') {
|
||||
const xid = m['xid'] as string
|
||||
if (this._recv[xid]) {
|
||||
@@ -427,23 +428,22 @@ export class PeerConn {
|
||||
this._dc({ type: 'file-offer', name: file.name, size: file.size, xid })
|
||||
}
|
||||
|
||||
private async _stream(xid: string) {
|
||||
private async _stream(xid: string, resumeOffset = 0) {
|
||||
const file = this._pushQueue.get(xid)
|
||||
if (!file) return
|
||||
this._pushQueue.delete(xid)
|
||||
const dc = this.pc.createDataChannel(`f:${xid}`)
|
||||
dc.binaryType = 'arraybuffer'
|
||||
await new Promise<void>(res => { dc.onopen = () => res() })
|
||||
const buf = await file.arrayBuffer()
|
||||
let offset = 0
|
||||
while (offset < buf.byteLength) {
|
||||
// Slice from resume offset so we only transfer the remaining bytes.
|
||||
const slice = resumeOffset > 0 ? file.slice(resumeOffset) : file
|
||||
const buf = await slice.arrayBuffer()
|
||||
let localOffset = 0
|
||||
while (localOffset < buf.byteLength) {
|
||||
while (dc.bufferedAmount > 1024 * 1024) await new Promise(r => setTimeout(r, 10))
|
||||
dc.send(buf.slice(offset, offset + CHUNK))
|
||||
offset += CHUNK
|
||||
this.events.fileSendProgress?.({ peer: this.peerId, xid, name: file.name, sent: offset, total: buf.byteLength })
|
||||
// Yield to the event loop every chunk so the browser can paint
|
||||
// progress — without this, small files finish in one synchronous
|
||||
// tick and the UI never shows intermediate state.
|
||||
dc.send(buf.slice(localOffset, localOffset + CHUNK))
|
||||
localOffset += CHUNK
|
||||
this.events.fileSendProgress?.({ peer: this.peerId, xid, name: file.name, sent: resumeOffset + localOffset, total: file.size })
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
}
|
||||
dc.close()
|
||||
|
||||
Reference in New Issue
Block a user