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:
@@ -41,7 +41,7 @@ function App() {
|
||||
const [scanning, setScanning] = useState(false)
|
||||
const [myId, setMyId] = useState<string | null>(null)
|
||||
const [addIdInput, setAddIdInput] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [copied, setCopied] = useState<'snip' | 'id' | null>(null)
|
||||
const sessionRef = useRef<FlitSession | null>(null)
|
||||
const verifiedRef = useRef(false)
|
||||
|
||||
@@ -218,13 +218,14 @@ function App() {
|
||||
setMode('known')
|
||||
}
|
||||
|
||||
function copyMyId() {
|
||||
function copyMyId(mode: 'snip' | 'id') {
|
||||
if (!myId) return
|
||||
// Build a snip the other side can paste into "Add peer"
|
||||
const snip = 'flit-peer:' + btoa(JSON.stringify({ id: myId })).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
navigator.clipboard.writeText(snip).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
const text = mode === 'snip'
|
||||
? 'flit-peer:' + btoa(JSON.stringify({ id: myId })).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
: myId
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(mode)
|
||||
setTimeout(() => setCopied(null), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -308,8 +309,11 @@ function App() {
|
||||
<div className="my-id-row">
|
||||
<span className="my-id-label">your ID</span>
|
||||
<span className="peer-id">{myId.slice(0, 16)}…</span>
|
||||
<button className="btn btn-sm" onClick={copyMyId}>
|
||||
{copied ? 'Copied ✓' : 'Copy snip'}
|
||||
<button className="btn btn-sm" onClick={() => copyMyId('id')}>
|
||||
{copied === 'id' ? 'Copied ✓' : 'Copy ID'}
|
||||
</button>
|
||||
<button className="btn btn-sm" onClick={() => copyMyId('snip')}>
|
||||
{copied === 'snip' ? 'Copied ✓' : 'Copy snip'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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