perkoon 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import{EventEmitter as Te}from"node:events";import Se from"node:crypto";import ae from"node-datachannel";import{EventEmitter as ce}from"node:events";var{PeerConnection:le}=ae,z=class extends ce{constructor(){super(),this.pc=null,this.dataChannel=null,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let i={iceServers:e.map(s=>(Array.isArray(s.urls)?s.urls:[s.urls]).map(n=>s.username&&s.credential?`${n}:${s.username}:${s.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new le(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{this.dataChannel=this._wrapChannel(s),this.emit("data-channel",this.dataChannel)})}async createOffer(){return new Promise(t=>{this.pc.onLocalDescription((i,s)=>{t({sdp:i,type:s})});let e=this.pc.createDataChannel("transfer",{ordered:!1,maxRetransmits:0});this.dataChannel=this._wrapChannel(e)})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((i,s)=>{e({sdp:i,type:s})}),this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()})}handleAnswer(t){this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()}addIceCandidate(t){t.candidate&&(this.remoteDescriptionSet?this.pc.addRemoteCandidate(t.candidate,t.mid||"0"):this.pendingCandidates.push(t))}_flushPendingCandidates(){for(let t of this.pendingCandidates)this.pc.addRemoteCandidate(t.candidate,t.mid||"0");this.pendingCandidates=[]}getDataChannel(){return this.dataChannel}async waitForDataChannel(t=3e4){return this.dataChannel?.readyState==="open"?this.dataChannel:new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Data channel open timeout"))},t),r=n=>{n.readyState==="open"?(clearTimeout(s),e(n)):n.addEventListener("open",()=>{clearTimeout(s),e(n)})};this.dataChannel?r(this.dataChannel):this.once("data-channel",n=>{r(n)})})}close(){if(this.dataChannel){try{this.dataChannel.close()}catch{}this.dataChannel=null}if(this.pc){try{this.pc.close()}catch{}this.pc=null}}_wrapChannel(t){let e=new Map,i="connecting";try{t.isOpen&&t.isOpen()&&(i="open")}catch{}let s={get readyState(){return i},get bufferedAmount(){try{return t.bufferedAmount()||0}catch{return 0}},send(n){n instanceof ArrayBuffer?t.sendMessageBinary(Buffer.from(n)):n instanceof Uint8Array?t.sendMessageBinary(Buffer.from(n.buffer,n.byteOffset,n.byteLength)):Buffer.isBuffer(n)?t.sendMessageBinary(n):t.sendMessage(String(n))},close(){i="closed";try{t.close()}catch{}},addEventListener(n,o){e.has(n)||e.set(n,new Set),e.get(n).add(o)},removeEventListener(n,o){let a=e.get(n);a&&a.delete(o)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let o;Buffer.isBuffer(n)?o=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):o=n,r("message",{data:o})});function r(n,o){let a=e.get(n);if(a)for(let c of a)try{c(o)}catch{}}return s}};import{Socket as fe}from"phoenix";import he from"ws";import{EventEmitter as ue}from"node:events";var M=class extends ue{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,o)=>{this.socket=new fe(r,{params:{},transport:he,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(a=>{this.connected||o(new Error(`Signaling connection failed: ${a?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",a=>{this.emit(a.type,a)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",a=>{this.connected=!0,this.peers=a.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",a=>{o(new Error(`Channel join failed: ${a.reason||"unknown"}`))}).receive("timeout",()=>{o(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}disconnect(){if(this._stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connected=!1}_startHeartbeat(){this._stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.connected&&this.channel&&this.channel.push("heartbeat",{})},1e4)}_stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}};import{open as de}from"node:fs/promises";import pe from"node:path";import{lookup as me}from"mime-types";async function K(l){let t=await de(l,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${l}`);return{name:pe.basename(l),size:e.size,type:me(l)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:o}=await t.read(n,0,r,i);return o===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+o)},async close(){await t.close()}}}import{open as ge,mkdir as ye}from"node:fs/promises";import _e from"node:path";function V(l,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await ye(l,{recursive:!0});let n=we(s.fileName),o=_e.join(l,n);if(!t.overwrite)try{let{access:c}=await import("node:fs/promises");throw await c(o),new Error(`File already exists: ${o} (use --overwrite to replace)`)}catch(c){if(c.code!=="ENOENT")throw c}let a=await ge(o,"w");s.size>0&&await a.truncate(s.size),e.set(i,{fh:a,filePath:o,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let o=s*n.pieceSize,a=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(a,0,a.byteLength,o),{success:!0}}catch(c){return{success:!1,error:c.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}function we(l){return l.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function Z(){let l=new Map;return{async startStreamingReceive(t,e,i){l.set(t,{pieceSize:e.pieceSize,totalPieces:Math.ceil(e.size/e.pieceSize),size:e.size,nextPiece:0,buffer:new Map,bytesWritten:0})},async writeChunk(t,e,i){let s=l.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let o=s.nextPiece*s.pieceSize,a=s.size-o,c=a<n.byteLength?n.subarray(0,a):n;process.stdout.write(c),s.bytesWritten+=c.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){l.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){l.delete(t)}}}var $=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=o=>{r&&clearTimeout(r),i(o)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var Pe={pieceSize:84*1024,maxConcurrentPieces:8,pieceTimeout:3e4,idleTimeout:6e4,maxPieceRetries:3,maxRetryQueueSize:1e4,maxInFlightMultiplier:10,highWaterMark:16*1024*1024,lowWaterMark:4*1024*1024,maxPiecesPerSecond:5e3,rateLimitWindowMs:1e3,channelRotationThreshold:512*1024*1024,channelRotationEnabled:!1,parallelChannels:1,parallelChannelsEnabled:!1,parallelChannelsThreshold:100*1024*1024,pacingEnabled:!0,pacingThreshold:0,pacingIntervalMs:10,targetBufferedAmount:8*1024*1024,minBufferedAmount:4*1024*1024,maxBufferedAmount:16*1024*1024,maxPiecesPerTick:32,burstPiecesPerTick:64,maxPendingReads:512,maxInFlightPieces:4e3},D=class extends ${constructor(t={}){super(),this.config={...Pe,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let o=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=o,this._checkProgressMilestones(t,i,o),this._emitProgress(t,i,o),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let o=i.inFlightPieces.get(n);if(!o)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let a=o.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=a,r+=a;let c=o.channelIndex>=0?o.channelIndex:0;i.channelBytes[c]!==void 0&&(i.channelBytes[c]+=a)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let a of i)if(typeof a!="number"||a<0||s.manifest&&a>=s.manifest.totalPieces)return}let r=Date.now(),n=0,o=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let a=[];for(let[c,h]of s.inFlightPieces)c<=e&&a.push([c,h]);for(let[c,h]of a){s.inFlightPieces.delete(c);let f=h.pieceSize||this._getPieceSize(s.manifest,c);s.bytesTransferred+=f,n+=f,o++,s.pieceTimeouts.delete(c)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let a of i){let c=s.inFlightPieces.get(a);if(c){s.inFlightPieces.delete(a);let h=c.pieceSize||this._getPieceSize(s.manifest,a);s.bytesTransferred+=h,n+=h,o++,s.piecesAckedCount++,s.pieceTimeouts.delete(a)}}if(o>0){s.lastActivityTime=r;let a=s.piecesAckedCount/s.manifest.totalPieces*100;s.lastProgress=a,this._checkProgressMilestones(t,s,a),this._emitProgress(t,s,a),s.piecesAckedCount===s.manifest.totalPieces&&this._completeSend(t)}}requeuePiece(t,e){let i=this.transfers.get(t);!i||i.type!=="send"||i.state==="completed"||i.state==="completing"||(i.inFlightPieces.delete(e),i.retryQueue.includes(e)||i.retryQueue.push(e),!i.paused&&!i.usePacing&&this._sendNextPieces(t))}markSendComplete(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.state==="completed"||e.state==="completing"||this._completeSend(t)}_checkChannelRotation(t,e,i){if(!this.config.channelRotationEnabled||e.pendingRotations.has(i))return;let s=e.channelBytes[i]||0,r=e.channelThresholds?.[i]||this.config.channelRotationThreshold;s>=r&&(e.pendingRotations.add(i),this.emit("channel-rotation-needed",{transferId:t,channelIndex:i,bytesTransferred:e.bytesTransferred,channelBytes:s,threshold:r,rotationCount:e.channelRotationCounts[i]||0}))}rotateSingleChannel(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||e>=s.channels.length)return;let r=s.channels[e],n=(s.channelRotationCounts[e]||0)+1;s.channels[e]=i,e===0&&(s.channel=i),s.channelBytes[e]=0,s.channelRotationCounts[e]=n,s.pendingRotations.delete(e),i.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!s.paused&&!s.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,i){let s=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(s,50)}};s(),setTimeout(()=>{if(t.readyState==="open")try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}},5e3)}rotateChannel(t,e){this.rotateChannels(t,[e])}initReceive(t,e,i={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s={id:t,type:"receive",manifest:e,state:"initialized",nextPieceToRequest:0,piecesReceivedCount:0,receivedWaterLevel:-1,receivedOutOfOrder:new Set,maxOutOfOrderSize:1e4,piecesRequested:new Set,bytesTransferred:0,streamingHandler:i.streamingHandler||null,backpressureCheckerId:null,fileBuffer:null,pieceBuffers:new Map,paused:!1,senderPaused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return s.streamingHandler||this._initReceiveBuffer(s,e),this.transfers.set(t,s),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(s)}}startReceive(t,e){let i=this.transfers.get(t);if(!i||i.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);i.state="transferring",i.startTime=i.startTime||Date.now(),i.requestPiece=e,this._startIdleTimeout(t),this._startBackpressureChecker(t),this._requestNextPieces(t)}_startBackpressureChecker(t){let e=this.transfers.get(t);!e||e.backpressureCheckerId||(e.backpressureCheckerId=setInterval(()=>{let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="cancelled"){this._stopBackpressureChecker(t);return}if(i.streamingHandler?.getBackpressureInfo){let s=i.streamingHandler.getBackpressureInfo();!s.isUnderPressure&&i.senderPaused&&(i.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:s.overflowSize}))}},200))}_stopBackpressureChecker(t){let e=this.transfers.get(t);!e||!e.backpressureCheckerId||(clearInterval(e.backpressureCheckerId),e.backpressureCheckerId=null)}async handlePieceData(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="receive")return{success:!1,error:"Transfer not found"};if(s.state==="completing"||s.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:s.lastProgress||0};let r=i.byteLength;if(s.piecesReceivedCount<100){if(e<0||e>=s.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(i instanceof ArrayBuffer||i instanceof Uint8Array||ArrayBuffer.isView(i)))return{success:!1,error:"Invalid piece data type"};if(r===0&&!(e===0&&s.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(s.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"}}if(s.streamingHandler?.getBackpressureInfo){let n=s.streamingHandler.getBackpressureInfo();n.isUnderPressure&&!s.senderPaused?(s.senderPaused=!0,this.emit("send-backpressure",{transferId:t,pause:!0,bufferSize:n.overflowSize})):!n.isUnderPressure&&s.senderPaused&&(s.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:n.overflowSize}))}try{if(e===s.receivedWaterLevel+1){for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;s.lastActivityTime=Date.now()}else if(e>s.receivedWaterLevel+1){if(s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}if(s.streamingHandler?s.streamingHandler.writeChunk(t,e,i).then(o=>{o&&!o.success&&!o.duplicate&&(o.error!=="OVERFLOW_TIMEOUT"&&console.warn("[TransferEngine] Write issue for piece",e,":",o.error),o.error==="SW_DOWNLOAD_DEAD"&&o.retryable===!1&&(console.error("[TransferEngine] Fatal write error - aborting transfer:",t),this.emit("transfer-failed",{transferId:t,error:"Download stream cancelled. Please try again.",fatal:!0}),this.cancel(t)))}).catch(o=>{console.error("[TransferEngine] Write error for piece",e,":",o.message)}):this._storePieceInMemory(s,e,i),s.receivedOutOfOrder.size>=100&&s.requestPiece){let o=Date.now(),a=s.lastGapCheckTime||0;if(o-a>5e3){s.lastGapCheckTime=o;let c=s.receivedWaterLevel+1,h=Math.min(c+10,e);for(let f=c;f<h;f++)!s.receivedOutOfOrder.has(f)&&!s.piecesRequested.has(f)&&(s.piecesRequested.add(f),s.requestPiece(f))}}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.pieceTimeouts.delete(e),s.pieceRetries.delete(e),s.bytesTransferred+=r;let n=s.piecesReceivedCount/s.manifest.totalPieces*100;return s.lastProgress=n,this._checkProgressMilestones(t,s,n),this._emitProgress(t,s,n),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(!s.paused&&s.piecesReceivedCount%10===0&&this._requestNextPieces(t),{success:!0})}catch(n){return{success:!1,error:n.message}}}async completeReceive(t){let e=this.transfers.get(t);if(!e||e.type!=="receive")return{success:!1,error:"Transfer not found"};if(e.piecesReceivedCount!==e.manifest.totalPieces)return{success:!1,error:`Incomplete: ${e.piecesReceivedCount}/${e.manifest.totalPieces}`};e.state="completing",this._stopIdleTimeout(t),this._stopBackpressureChecker(t);try{if(e.streamingHandler)return await e.streamingHandler.completeStream(t),e.state="completed",{success:!0,streaming:!0,manifest:e.manifest};{let i=this._assembleFile(e);return e.state="completed",{success:!0,blob:i,manifest:e.manifest,fileName:e.manifest.fileName}}}catch(i){return{success:!1,error:i.message}}}pause(t){let e=this.transfers.get(t);e&&(e.paused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t),this.emit("paused",{transferId:t}))}resume(t){let e=this.transfers.get(t);!e||!e.paused||(e.paused=!1,this._startIdleTimeout(t),e.type==="send"?e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t):this._requestNextPieces(t),this.emit("resumed",{transferId:t}))}pauseForBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused||(e.backpressurePaused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t))}resumeFromBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused&&(e.backpressurePaused=!1,!e.paused&&(this._startIdleTimeout(t),e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t)))}cancel(t){let e=this.transfers.get(t);e&&(this._cleanupTransfer(t),this.emit("cancelled",{transferId:t,type:e.type}))}cleanup(t){this._cleanupTransfer(t)}getStats(t){let e=this.transfers.get(t);if(!e)return null;let i=e.startTime?Date.now()-e.startTime:0,s=i>0?e.bytesTransferred/i*1e3:0;return{transferId:t,type:e.type,state:e.state,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),piecesCompleted:e.type==="send"?e.piecesAckedCount:e.piecesReceivedCount,totalPieces:e.manifest.totalPieces,speed:s,elapsed:i,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.retryQueue.length>=this.config.maxRetryQueueSize){this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=i)return;let s=e.channel,r=e.channels.length===1;if(r){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return}else{let f=!1;for(let m of e.channels)if(m.readyState==="open"&&m.bufferedAmount<=this.config.highWaterMark){f=!0;break}if(!f)return}let o=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(o<=0)return;let a=[];for(;e.retryQueue.length>0&&a.length<o;){let f=e.retryQueue.shift();e.inFlightPieces.has(f)||a.push(f)}for(;e.nextPieceToSend<e.manifest.totalPieces&&a.length<o;){let f=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(f)||a.push(f)}if(a.length===0)return;let c=Date.now();for(let f of a)e.inFlightPieces.set(f,{sendTime:c,pieceSize:this._getPieceSize(e.manifest,f)});let h=r?4:4*e.channels.length;try{for(let f=0;f<a.length;f+=h){let y=a.slice(f,f+h).map(async g=>{let u=await this._readPiece(e.file,e.manifest,g);return{pieceIndex:g,data:u}}),p=await Promise.all(y);if(r)for(let{pieceIndex:g,data:u}of p){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,g,u)),this._startPieceTimeout(t,g)}else for(let{pieceIndex:g,data:u}of p){let F=null,T=-1;for(let k=0;k<e.channels.length;k++){let _=(e.channelIndex+k)%e.channels.length,C=e.channels[_];if(C.readyState==="open"&&C.bufferedAmount<=this.config.highWaterMark){F=C,T=_,e.channelIndex=(_+1)%e.channels.length;break}}if(!F)return;F.send(this._encodePieceMessage(t,g,u)),e.inFlightPieces.set(g,{sendTime:Date.now(),channelIndex:T,pieceSize:this._getPieceSize(e.manifest,g)}),this._startPieceTimeout(t,g)}}}catch(f){for(let m of a)e.inFlightPieces.delete(m),e.retryQueue.push(m);this.emit("error",{transferId:t,error:`Failed to send pieces: ${f.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,o=this.config.maxInFlightPieces*.99,a=this.config.maxPendingReads*.95,c=r<2*1024*1024,h=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=o||i>=n&&c?"RECEIVER_SLOW":e.pendingReads>=a?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:h})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let f=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?f=32:r>this.config.minBufferedAmount?f=this.config.maxPiecesPerTick:f=this.config.burstPiecesPerTick;let m=this.config.maxPendingReads-e.pendingReads;if(m<=0)return;f=Math.min(f,m);let y=this.config.maxInFlightPieces-i;f=Math.min(f,y);let p=[];for(;e.retryQueue.length>0&&p.length<f;){let u=e.retryQueue.shift();e.inFlightPieces.has(u)||p.push(u)}for(;e.nextPieceToSend<e.manifest.totalPieces&&p.length<f;){let u=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(u)||p.push(u)}if(p.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=p.length;let g=Date.now();for(let u of p)e.inFlightPieces.set(u,{sendTime:g,pieceSize:this._getPieceSize(e.manifest,u)});this._readAndSendPieces(t,e,s,p)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(o=>this._readPiece(e.file,e.manifest,o).then(a=>({pieceIndex:o,data:a})).catch(a=>({pieceIndex:o,error:a}))),n=await Promise.all(r);for(let o of n){if(o.error){e.inFlightPieces.delete(o.pieceIndex),e.retryQueue.push(o.pieceIndex);continue}let{pieceIndex:a,data:c}=o,h=e.channel;if(!h||h.readyState!=="open"){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}if(h.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}try{h.send(this._encodePieceMessage(t,a,c)),this._startPieceTimeout(t,a)}catch{e.inFlightPieces.delete(a),e.retryQueue.push(a)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let a=0;a<e.totalPieces;a++){let c=s.get(a);if(!c)throw new Error(`Missing piece ${a}`);n.push(c)}let o=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),o}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),o=new Uint8Array(4+n.byteLength);return o.set(new Uint8Array(n),4),o}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,o=e.lastSpeedTime||e.startTime||s,a=s-o;if(a>=200){let m=e.bytesTransferred-n;r=a>0?m/a*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=c*r+(1-c)*e.smoothedSpeed);let h=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let f=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:f,speed:h})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){if(!t||!(t instanceof ArrayBuffer)||t.byteLength<4)return{pieceIndex:-1,data:null,error:"invalid_message"};try{let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}catch{return{pieceIndex:-1,data:null,error:"decode_failed"}}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let o=s-e.lastMilestoneCheck.time,a=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(o>0&&o<r&&a>0)n=a/o*1e3/1e6;else if(o>=r){let c=e.startTime?s-e.startTime:0;n=c>0?e.bytesTransferred/c*1e3/1e6:0}}else{let o=e.startTime?s-e.startTime:0;n=o>0?e.bytesTransferred/o*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,a=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,c=n<r&&!a,h=[];for(let[y,p]of e.pieceTimeouts)if(i-p>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(y<=e.receivedWaterLevel||e.receivedOutOfOrder.has(y))){e.pieceTimeouts.delete(y);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&y<=e.ackedWaterLevel){e.pieceTimeouts.delete(y);continue}h.push(y)}if(h.length===0||c)return;let m=h.slice(0,50);for(let y of m)this._handlePieceTimeout(t,y)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),o=Date.now()-n;o>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:o}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var ve="https://perkoon.com",O=class extends Te{constructor(t=ve,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new D,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await K(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=await this._createSession(e.name,e.password);this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new M(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r}),this.transport=new z,this.transport.initialize(i.peer_id,i.ice_servers);let n=await this.transport.createOffer();this.signaling.sendOffer(r,n),this.transport.on("ice-candidate",({candidate:m,mid:y})=>{this.signaling.sendIceCandidate(r,{candidate:m,mid:y})}),this.signaling.on("ice_candidate",m=>{this.transport.addIceCandidate(m.candidate)});let o=await this._waitForSignal("answer");this.transport.handleAnswer(o.answer);let a=await this.transport.waitForDataChannel();this.emit("connected");let c=Se.randomUUID();this.engine.initSend(c,this.fileSource,a);let h=this.engine.transfers.get(c).manifest;this._setupControlHandler(a,c),a.send(JSON.stringify({type:"manifest",transfer_id:c,manifest:h})),this._setupProgressEvents(c),await this.engine.startSend(c);let f=await this._waitForCompletion(c);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:f.speed,duration:f.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new M(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver"),n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new z,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:p,mid:g})=>{this.signaling.sendIceCandidate(n,{candidate:p,mid:g})}),this.signaling.on("ice_candidate",p=>{this.transport.addIceCandidate(p.candidate)});let o=await this._waitForSignal("offer"),a=await this.transport.handleOffer(o.offer);this.signaling.sendAnswer(n,a);let c=await this.transport.waitForDataChannel();this.emit("connected");let h=i.stdout?Z():V(e,{overwrite:i.overwrite}),f=[],y=await new Promise((p,g)=>{let u=null;c.addEventListener("message",async F=>{let T=F.data;if(typeof T=="string"){try{let k=JSON.parse(T);if(k.type==="manifest"){u=k.transfer_id;let _=k.manifest;this.emit("receiving-file",{name:_.fileName,size:_.fileSize}),await h.startStreamingReceive(u,{fileName:_.fileName,size:_.fileSize,fileSize:_.fileSize,type:_.fileType,pieceSize:_.pieceSize},{}),this.engine.initReceive(u,_,{streamingHandler:h}),this._setupProgressEvents(u),this.engine.startReceive(u,b=>{this._safeSend(c,JSON.stringify({type:"request",transfer_id:u,piece_index:b}))});let C=-1;this._ackInterval=setInterval(()=>{let b=this.engine.transfers.get(u);if(!b||b.state==="completed"||b.state==="completing"){clearInterval(this._ackInterval);return}let L=b.receivedWaterLevel;if(L>C){C=L;let q=b.receivedOutOfOrder.size>0?Array.from(b.receivedOutOfOrder):void 0;this._safeSend(c,JSON.stringify({type:"water_level_ack",transfer_id:u,water_level:L,out_of_order:q}))}},100)}}catch{}return}if(u&&(T instanceof ArrayBuffer||Buffer.isBuffer(T))){let k=T instanceof ArrayBuffer?T:T.buffer.slice(T.byteOffset,T.byteOffset+T.byteLength),{pieceIndex:_,data:C}=this.engine.decodePieceMessage(k);if(_>=0&&C&&(await this.engine.handlePieceData(u,_,C))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let L=this.engine.transfers.get(u);L&&this._safeSend(c,JSON.stringify({type:"water_level_ack",transfer_id:u,water_level:L.receivedWaterLevel}));let q=h.getFilePath(u);f.push(q),await this.engine.completeReceive(u),this._safeSend(c,JSON.stringify({type:"complete",transfer_id:u}));let G=this.engine.transfers.get(u),J=Date.now()-(G?.startTime||Date.now()),X=G?.manifest;p({files:f,speed:X?X.fileSize/(J/1e3):0,duration:J})}}}),setTimeout(()=>{g(new Error("Transfer timed out (10 minutes)"))},6e5)});return this.signaling.disconnect(),this.transport.close(),y}async _createSession(t,e){let i=`${this.serverUrl}/api/v1/sessions`,s={};t&&(s.name=t),e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${n.error||r.statusText}`)}return r.json()}async _joinSession(t,e){let i=`${this.serverUrl}/api/v1/sessions/${t}/join`,s={role:"receiver"};e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw r.status===404?new Error(`Session ${t} not found. Check the code and try again.`):r.status===410?new Error(`Session ${t} has expired.`):r.status===403?new Error(n.error||"Access denied"):new Error(`Failed to join session: ${n.error||r.statusText}`)}return r.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),s=r=>{(r.type==="ready"||r.type==="offer")&&(clearTimeout(i),this.signaling.removeListener("ready",s),this.signaling.removeListener("offer",s),t(r.from))};this.signaling.on("ready",s),this.signaling.on("offer",s)})}_waitForSignal(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error(`Timed out waiting for ${t} signal`))},3e4);this.signaling.once(t,r=>{clearTimeout(s),e(r)})})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let o=r>0?Math.round(n/r*100):0,a=Date.now()-(i.startTime||Date.now()),c=n*s.pieceSize,h=a>0?c/(a/1e3):0,f=h>0?Math.round((s.fileSize-c)/h):0;this.emit("progress",{transferId:t,percent:Math.min(o,100),speed:h,eta:f,bytesTransferred:c,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"request":this.engine.requeuePiece(e,s.piece_index);break;case"complete":{let r=this.engine.transfers.get(e);r&&r.state!=="completed"&&(r.piecesAckedCount=r.manifest.totalPieces,r.bytesTransferred=r.manifest.fileSize,r.inFlightPieces.clear(),r.state="completed")}this.emit("transfer-complete",{transferId:e});break;case"backpressure":s.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Transfer timed out (10 minutes)"))},6e5),r=()=>{let o=this.engine.transfers.get(t);if(!o){clearTimeout(s),i(new Error("Transfer context lost"));return}if(o.state==="completed"){clearTimeout(s);let a=Date.now()-o.startTime;e({speed:o.manifest.fileSize/(a/1e3),duration:a});return}if(o.state==="cancelled"||o.state==="failed"){clearTimeout(s),i(new Error("Transfer failed"));return}};this.on("transfer-complete",({transferId:o})=>{o===t&&r()});let n=setInterval(()=>{r();let o=this.engine.transfers.get(t);(!o||o.state==="completed"||o.state==="cancelled")&&clearInterval(n)},500)})}_safeSend(t,e){try{t.readyState==="open"&&t.send(e)}catch{}}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};import ke from"node:path";import{access as Ce,stat as be}from"node:fs/promises";var Y="0.1.2",se="https://perkoon.com",ie=0,R=1,ee=2,H=3,Ae=4,Ee=5,A=process.argv.slice(2),te=A[0],P={};for(let l=1;l<A.length;l++)if(A[l].startsWith("--")){let t=A[l].slice(2);A[l+1]&&!A[l+1].startsWith("--")?P[t]=A[++l]:P[t]=!0}else P._positional||(P._positional=A[l]);var E=se,B=P.json===!0,N=P.quiet===!0,Re=P.overwrite===!0,j=P.output||"./received",x=typeof P.password=="string"?P.password:void 0,Q=typeof P.timeout=="string"?parseInt(P.timeout,10):NaN,I=!isNaN(Q)&&Q>0?Q:300;B&&j==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
3
- `),process.exit(R));function d(l){!N&&!B&&process.stderr.write(l+`
4
- `)}function w(l,t={}){B&&process.stdout.write(JSON.stringify({event:l,...t})+`
5
- `)}function S(l){d(` \u2713 ${l}`)}function W(l){return l>=1024*1024*1024?`${(l/(1024*1024*1024)).toFixed(1)} GB`:l>=1024*1024?`${(l/(1024*1024)).toFixed(1)} MB`:l>=1024?`${(l/1024).toFixed(0)} KB`:`${l} B`}function U(l){return W(l)+"/s"}function ne(l){return l<=0?"...":l<60?`${l}s`:`${Math.floor(l/60)}m ${l%60}s`}function re(l,t=30){let e=Math.round(t*l/100);return"\u2588".repeat(e)+"\u2591".repeat(t-e)}function v(l,t){d(`
6
- Error: ${l}`),t&&d(`
7
- ${t}`),d("")}function oe(l){let t=l.message||"";return t.includes("No peer connected")||t.includes("timed out")?Ee:t.includes("Password required")||t.includes("Invalid password")||t.includes("Access denied")?Ae:(t.includes("Failed to create session")||t.includes("Failed to join")||t.includes("not found")||t.includes("expired"),H)}async function Be(){let l=P._positional;l||(v("No file specified.","Usage: perkoon send <file>"),process.exit(R));let t=ke.resolve(l);try{await Ce(t),(await be(t)).isFile()||(v(`Not a regular file: ${t}`),process.exit(ee))}catch{v(`File not found: ${t}`),process.exit(ee)}let e=new O(E,{timeout:I*1e3});e.on("file-ready",({name:r,size:n})=>{d(""),S(`${r} (${W(n)})`),w("file_ready",{name:r,size:n})}),e.on("session-created",({sessionCode:r})=>{S(`Code: ${r}`),x&&S("Password protected"),d("");let n=x?` --password ${x}`:"";d(` Receiver command: perkoon receive ${r}${n}`),d(` Or open in browser: ${E}/${r}`),d(""),w("session_created",{session_code:r,share_url:`${E}/${r}`})}),e.on("waiting-for-receiver",()=>{d(" Waiting for receiver..."),w("waiting_for_receiver")}),e.on("receiver-connected",()=>{S("Receiver connected"),w("receiver_connected")}),e.on("connected",()=>{S("Direct connection established"),d(""),w("webrtc_connected")});let i="";e.on("progress",({percent:r,speed:n,eta:o,bytesTransferred:a})=>{if(B)w("progress",{percent:r,speed:Math.round(n),eta:o,bytes_transferred:a});else if(!N){let c=` ${re(r)} ${String(r).padStart(3)}% ${U(n).padStart(10)} ETA ${ne(o)}`;c!==i&&(process.stderr.write(`\r${c}`),i=c)}});let s=()=>{d(`
2
+ import{EventEmitter as ve}from"node:events";import ke from"node:crypto";import le from"node-datachannel";import{EventEmitter as fe}from"node:events";var{PeerConnection:ue}=le,z=class extends fe{constructor(){super(),this.pc=null,this.dataChannel=null,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let i={iceServers:e.map(s=>(Array.isArray(s.urls)?s.urls:[s.urls]).map(n=>s.username&&s.credential?`${n}:${s.username}:${s.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new ue(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{this.dataChannel=this._wrapChannel(s),this.emit("data-channel",this.dataChannel)})}async createOffer(){return new Promise(t=>{this.pc.onLocalDescription((i,s)=>{t({sdp:i,type:s})});let e=this.pc.createDataChannel("transfer",{ordered:!1,maxRetransmits:0});this.dataChannel=this._wrapChannel(e)})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((i,s)=>{e({sdp:i,type:s})}),this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()})}handleAnswer(t){this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()}addIceCandidate(t){t.candidate&&(this.remoteDescriptionSet?this.pc.addRemoteCandidate(t.candidate,t.mid||"0"):this.pendingCandidates.push(t))}_flushPendingCandidates(){for(let t of this.pendingCandidates)this.pc.addRemoteCandidate(t.candidate,t.mid||"0");this.pendingCandidates=[]}getDataChannel(){return this.dataChannel}async waitForDataChannel(t=3e4){return this.dataChannel?.readyState==="open"?this.dataChannel:new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Data channel open timeout"))},t),r=n=>{n.readyState==="open"?(clearTimeout(s),e(n)):n.addEventListener("open",()=>{clearTimeout(s),e(n)})};this.dataChannel?r(this.dataChannel):this.once("data-channel",n=>{r(n)})})}close(){if(this.dataChannel){try{this.dataChannel.close()}catch{}this.dataChannel=null}if(this.pc){try{this.pc.close()}catch{}this.pc=null}}_wrapChannel(t){let e=new Map,i="connecting";try{t.isOpen&&t.isOpen()&&(i="open")}catch{}let s={get readyState(){return i},get bufferedAmount(){try{return t.bufferedAmount()||0}catch{return 0}},send(n){n instanceof ArrayBuffer?t.sendMessageBinary(Buffer.from(n)):n instanceof Uint8Array?t.sendMessageBinary(Buffer.from(n.buffer,n.byteOffset,n.byteLength)):Buffer.isBuffer(n)?t.sendMessageBinary(n):t.sendMessage(String(n))},close(){i="closed";try{t.close()}catch{}},addEventListener(n,o){e.has(n)||e.set(n,new Set),e.get(n).add(o)},removeEventListener(n,o){let a=e.get(n);a&&a.delete(o)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let o;Buffer.isBuffer(n)?o=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):o=n,r("message",{data:o})});function r(n,o){let a=e.get(n);if(a)for(let l of a)try{l(o)}catch{}}return s}};import{Socket as he}from"phoenix";import de from"ws";import{EventEmitter as pe}from"node:events";var M=class extends pe{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,o)=>{this.socket=new he(r,{params:{},transport:de,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(a=>{this.connected||o(new Error(`Signaling connection failed: ${a?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",a=>{this.emit(a.type,a)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",a=>{this.connected=!0,this.peers=a.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",a=>{o(new Error(`Channel join failed: ${a.reason||"unknown"}`))}).receive("timeout",()=>{o(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}disconnect(){if(this._stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connected=!1}_startHeartbeat(){this._stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.connected&&this.channel&&this.channel.push("heartbeat",{})},1e4)}_stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}};import{open as me}from"node:fs/promises";import ge from"node:path";import{lookup as ye}from"mime-types";async function K(c){let t=await me(c,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${c}`);return{name:ge.basename(c),size:e.size,type:ye(c)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:o}=await t.read(n,0,r,i);return o===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+o)},async close(){await t.close()}}}import{open as _e,mkdir as we,access as V}from"node:fs/promises";import Z from"node:path";function Y(c,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await we(c,{recursive:!0});let n=Te(s.fileName),o=Z.join(c,n);t.overwrite||(o=await Pe(o));let a=await _e(o,"w");s.size>0&&await a.truncate(s.size),e.set(i,{fh:a,filePath:o,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let o=s*n.pieceSize,a=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(a,0,a.byteLength,o),{success:!0}}catch(l){return{success:!1,error:l.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}async function Pe(c){try{await V(c)}catch{return c}let t=Z.extname(c),e=c.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await V(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function Te(c){return c.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function ee(){let c=new Map;return{async startStreamingReceive(t,e,i){c.set(t,{pieceSize:e.pieceSize,totalPieces:Math.ceil(e.size/e.pieceSize),size:e.size,nextPiece:0,buffer:new Map,bytesWritten:0})},async writeChunk(t,e,i){let s=c.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let o=s.nextPiece*s.pieceSize,a=s.size-o,l=a<n.byteLength?n.subarray(0,a):n;process.stdout.write(l),s.bytesWritten+=l.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){c.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){c.delete(t)}}}var N=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=o=>{r&&clearTimeout(r),i(o)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var Se={pieceSize:84*1024,maxConcurrentPieces:8,pieceTimeout:3e4,idleTimeout:6e4,maxPieceRetries:3,maxRetryQueueSize:1e4,maxInFlightMultiplier:10,highWaterMark:16*1024*1024,lowWaterMark:4*1024*1024,maxPiecesPerSecond:5e3,rateLimitWindowMs:1e3,channelRotationThreshold:512*1024*1024,channelRotationEnabled:!1,parallelChannels:1,parallelChannelsEnabled:!1,parallelChannelsThreshold:100*1024*1024,pacingEnabled:!0,pacingThreshold:0,pacingIntervalMs:10,targetBufferedAmount:8*1024*1024,minBufferedAmount:4*1024*1024,maxBufferedAmount:16*1024*1024,maxPiecesPerTick:32,burstPiecesPerTick:64,maxPendingReads:512,maxInFlightPieces:4e3},D=class extends N{constructor(t={}){super(),this.config={...Se,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let o=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=o,this._checkProgressMilestones(t,i,o),this._emitProgress(t,i,o),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let o=i.inFlightPieces.get(n);if(!o)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let a=o.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=a,r+=a;let l=o.channelIndex>=0?o.channelIndex:0;i.channelBytes[l]!==void 0&&(i.channelBytes[l]+=a)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let a of i)if(typeof a!="number"||a<0||s.manifest&&a>=s.manifest.totalPieces)return}let r=Date.now(),n=0,o=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let a=[];for(let[l,u]of s.inFlightPieces)l<=e&&a.push([l,u]);for(let[l,u]of a){s.inFlightPieces.delete(l);let f=u.pieceSize||this._getPieceSize(s.manifest,l);s.bytesTransferred+=f,n+=f,o++,s.pieceTimeouts.delete(l)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let a of i){let l=s.inFlightPieces.get(a);if(l){s.inFlightPieces.delete(a);let u=l.pieceSize||this._getPieceSize(s.manifest,a);s.bytesTransferred+=u,n+=u,o++,s.piecesAckedCount++,s.pieceTimeouts.delete(a)}}if(o>0){s.lastActivityTime=r;let a=s.piecesAckedCount/s.manifest.totalPieces*100;s.lastProgress=a,this._checkProgressMilestones(t,s,a),this._emitProgress(t,s,a),s.piecesAckedCount===s.manifest.totalPieces&&this._completeSend(t)}}requeuePiece(t,e){let i=this.transfers.get(t);!i||i.type!=="send"||i.state==="completed"||i.state==="completing"||(i.inFlightPieces.delete(e),i.retryQueue.includes(e)||i.retryQueue.push(e),!i.paused&&!i.usePacing&&this._sendNextPieces(t))}markSendComplete(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.state==="completed"||e.state==="completing"||this._completeSend(t)}_checkChannelRotation(t,e,i){if(!this.config.channelRotationEnabled||e.pendingRotations.has(i))return;let s=e.channelBytes[i]||0,r=e.channelThresholds?.[i]||this.config.channelRotationThreshold;s>=r&&(e.pendingRotations.add(i),this.emit("channel-rotation-needed",{transferId:t,channelIndex:i,bytesTransferred:e.bytesTransferred,channelBytes:s,threshold:r,rotationCount:e.channelRotationCounts[i]||0}))}rotateSingleChannel(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||e>=s.channels.length)return;let r=s.channels[e],n=(s.channelRotationCounts[e]||0)+1;s.channels[e]=i,e===0&&(s.channel=i),s.channelBytes[e]=0,s.channelRotationCounts[e]=n,s.pendingRotations.delete(e),i.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!s.paused&&!s.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,i){let s=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(s,50)}};s(),setTimeout(()=>{if(t.readyState==="open")try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}},5e3)}rotateChannel(t,e){this.rotateChannels(t,[e])}initReceive(t,e,i={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s={id:t,type:"receive",manifest:e,state:"initialized",nextPieceToRequest:0,piecesReceivedCount:0,receivedWaterLevel:-1,receivedOutOfOrder:new Set,maxOutOfOrderSize:1e4,piecesRequested:new Set,bytesTransferred:0,streamingHandler:i.streamingHandler||null,backpressureCheckerId:null,fileBuffer:null,pieceBuffers:new Map,paused:!1,senderPaused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return s.streamingHandler||this._initReceiveBuffer(s,e),this.transfers.set(t,s),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(s)}}startReceive(t,e){let i=this.transfers.get(t);if(!i||i.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);i.state="transferring",i.startTime=i.startTime||Date.now(),i.requestPiece=e,this._startIdleTimeout(t),this._startBackpressureChecker(t),this._requestNextPieces(t)}_startBackpressureChecker(t){let e=this.transfers.get(t);!e||e.backpressureCheckerId||(e.backpressureCheckerId=setInterval(()=>{let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="cancelled"){this._stopBackpressureChecker(t);return}if(i.streamingHandler?.getBackpressureInfo){let s=i.streamingHandler.getBackpressureInfo();!s.isUnderPressure&&i.senderPaused&&(i.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:s.overflowSize}))}},200))}_stopBackpressureChecker(t){let e=this.transfers.get(t);!e||!e.backpressureCheckerId||(clearInterval(e.backpressureCheckerId),e.backpressureCheckerId=null)}async handlePieceData(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="receive")return{success:!1,error:"Transfer not found"};if(s.state==="completing"||s.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:s.lastProgress||0};let r=i.byteLength;if(s.piecesReceivedCount<100){if(e<0||e>=s.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(i instanceof ArrayBuffer||i instanceof Uint8Array||ArrayBuffer.isView(i)))return{success:!1,error:"Invalid piece data type"};if(r===0&&!(e===0&&s.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(s.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"}}if(s.streamingHandler?.getBackpressureInfo){let n=s.streamingHandler.getBackpressureInfo();n.isUnderPressure&&!s.senderPaused?(s.senderPaused=!0,this.emit("send-backpressure",{transferId:t,pause:!0,bufferSize:n.overflowSize})):!n.isUnderPressure&&s.senderPaused&&(s.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:n.overflowSize}))}try{if(e===s.receivedWaterLevel+1){for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;s.lastActivityTime=Date.now()}else if(e>s.receivedWaterLevel+1){if(s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}if(s.streamingHandler?s.streamingHandler.writeChunk(t,e,i).then(o=>{o&&!o.success&&!o.duplicate&&(o.error!=="OVERFLOW_TIMEOUT"&&console.warn("[TransferEngine] Write issue for piece",e,":",o.error),o.error==="SW_DOWNLOAD_DEAD"&&o.retryable===!1&&(console.error("[TransferEngine] Fatal write error - aborting transfer:",t),this.emit("transfer-failed",{transferId:t,error:"Download stream cancelled. Please try again.",fatal:!0}),this.cancel(t)))}).catch(o=>{console.error("[TransferEngine] Write error for piece",e,":",o.message)}):this._storePieceInMemory(s,e,i),s.receivedOutOfOrder.size>=100&&s.requestPiece){let o=Date.now(),a=s.lastGapCheckTime||0;if(o-a>5e3){s.lastGapCheckTime=o;let l=s.receivedWaterLevel+1,u=Math.min(l+10,e);for(let f=l;f<u;f++)!s.receivedOutOfOrder.has(f)&&!s.piecesRequested.has(f)&&(s.piecesRequested.add(f),s.requestPiece(f))}}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.pieceTimeouts.delete(e),s.pieceRetries.delete(e),s.bytesTransferred+=r;let n=s.piecesReceivedCount/s.manifest.totalPieces*100;return s.lastProgress=n,this._checkProgressMilestones(t,s,n),this._emitProgress(t,s,n),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(!s.paused&&s.piecesReceivedCount%10===0&&this._requestNextPieces(t),{success:!0})}catch(n){return{success:!1,error:n.message}}}async completeReceive(t){let e=this.transfers.get(t);if(!e||e.type!=="receive")return{success:!1,error:"Transfer not found"};if(e.piecesReceivedCount!==e.manifest.totalPieces)return{success:!1,error:`Incomplete: ${e.piecesReceivedCount}/${e.manifest.totalPieces}`};e.state="completing",this._stopIdleTimeout(t),this._stopBackpressureChecker(t);try{if(e.streamingHandler)return await e.streamingHandler.completeStream(t),e.state="completed",{success:!0,streaming:!0,manifest:e.manifest};{let i=this._assembleFile(e);return e.state="completed",{success:!0,blob:i,manifest:e.manifest,fileName:e.manifest.fileName}}}catch(i){return{success:!1,error:i.message}}}pause(t){let e=this.transfers.get(t);e&&(e.paused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t),this.emit("paused",{transferId:t}))}resume(t){let e=this.transfers.get(t);!e||!e.paused||(e.paused=!1,this._startIdleTimeout(t),e.type==="send"?e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t):this._requestNextPieces(t),this.emit("resumed",{transferId:t}))}pauseForBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused||(e.backpressurePaused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t))}resumeFromBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused&&(e.backpressurePaused=!1,!e.paused&&(this._startIdleTimeout(t),e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t)))}cancel(t){let e=this.transfers.get(t);e&&(this._cleanupTransfer(t),this.emit("cancelled",{transferId:t,type:e.type}))}cleanup(t){this._cleanupTransfer(t)}getStats(t){let e=this.transfers.get(t);if(!e)return null;let i=e.startTime?Date.now()-e.startTime:0,s=i>0?e.bytesTransferred/i*1e3:0;return{transferId:t,type:e.type,state:e.state,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),piecesCompleted:e.type==="send"?e.piecesAckedCount:e.piecesReceivedCount,totalPieces:e.manifest.totalPieces,speed:s,elapsed:i,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.retryQueue.length>=this.config.maxRetryQueueSize){this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=i)return;let s=e.channel,r=e.channels.length===1;if(r){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return}else{let f=!1;for(let m of e.channels)if(m.readyState==="open"&&m.bufferedAmount<=this.config.highWaterMark){f=!0;break}if(!f)return}let o=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(o<=0)return;let a=[];for(;e.retryQueue.length>0&&a.length<o;){let f=e.retryQueue.shift();e.inFlightPieces.has(f)||a.push(f)}for(;e.nextPieceToSend<e.manifest.totalPieces&&a.length<o;){let f=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(f)||a.push(f)}if(a.length===0)return;let l=Date.now();for(let f of a)e.inFlightPieces.set(f,{sendTime:l,pieceSize:this._getPieceSize(e.manifest,f)});let u=r?4:4*e.channels.length;try{for(let f=0;f<a.length;f+=u){let y=a.slice(f,f+u).map(async g=>{let h=await this._readPiece(e.file,e.manifest,g);return{pieceIndex:g,data:h}}),p=await Promise.all(y);if(r)for(let{pieceIndex:g,data:h}of p){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,g,h)),this._startPieceTimeout(t,g)}else for(let{pieceIndex:g,data:h}of p){let F=null,T=-1;for(let S=0;S<e.channels.length;S++){let _=(e.channelIndex+S)%e.channels.length,C=e.channels[_];if(C.readyState==="open"&&C.bufferedAmount<=this.config.highWaterMark){F=C,T=_,e.channelIndex=(_+1)%e.channels.length;break}}if(!F)return;F.send(this._encodePieceMessage(t,g,h)),e.inFlightPieces.set(g,{sendTime:Date.now(),channelIndex:T,pieceSize:this._getPieceSize(e.manifest,g)}),this._startPieceTimeout(t,g)}}}catch(f){for(let m of a)e.inFlightPieces.delete(m),e.retryQueue.push(m);this.emit("error",{transferId:t,error:`Failed to send pieces: ${f.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,o=this.config.maxInFlightPieces*.99,a=this.config.maxPendingReads*.95,l=r<2*1024*1024,u=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=o||i>=n&&l?"RECEIVER_SLOW":e.pendingReads>=a?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:u})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let f=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?f=32:r>this.config.minBufferedAmount?f=this.config.maxPiecesPerTick:f=this.config.burstPiecesPerTick;let m=this.config.maxPendingReads-e.pendingReads;if(m<=0)return;f=Math.min(f,m);let y=this.config.maxInFlightPieces-i;f=Math.min(f,y);let p=[];for(;e.retryQueue.length>0&&p.length<f;){let h=e.retryQueue.shift();e.inFlightPieces.has(h)||p.push(h)}for(;e.nextPieceToSend<e.manifest.totalPieces&&p.length<f;){let h=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(h)||p.push(h)}if(p.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=p.length;let g=Date.now();for(let h of p)e.inFlightPieces.set(h,{sendTime:g,pieceSize:this._getPieceSize(e.manifest,h)});this._readAndSendPieces(t,e,s,p)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(o=>this._readPiece(e.file,e.manifest,o).then(a=>({pieceIndex:o,data:a})).catch(a=>({pieceIndex:o,error:a}))),n=await Promise.all(r);for(let o of n){if(o.error){e.inFlightPieces.delete(o.pieceIndex),e.retryQueue.push(o.pieceIndex);continue}let{pieceIndex:a,data:l}=o,u=e.channel;if(!u||u.readyState!=="open"){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}if(u.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}try{u.send(this._encodePieceMessage(t,a,l)),this._startPieceTimeout(t,a)}catch{e.inFlightPieces.delete(a),e.retryQueue.push(a)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let a=0;a<e.totalPieces;a++){let l=s.get(a);if(!l)throw new Error(`Missing piece ${a}`);n.push(l)}let o=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),o}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),o=new Uint8Array(4+n.byteLength);return o.set(new Uint8Array(n),4),o}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,o=e.lastSpeedTime||e.startTime||s,a=s-o;if(a>=200){let m=e.bytesTransferred-n;r=a>0?m/a*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let l=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=l*r+(1-l)*e.smoothedSpeed);let u=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let f=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:f,speed:u})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){if(!t||!(t instanceof ArrayBuffer)||t.byteLength<4)return{pieceIndex:-1,data:null,error:"invalid_message"};try{let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}catch{return{pieceIndex:-1,data:null,error:"decode_failed"}}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let o=s-e.lastMilestoneCheck.time,a=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(o>0&&o<r&&a>0)n=a/o*1e3/1e6;else if(o>=r){let l=e.startTime?s-e.startTime:0;n=l>0?e.bytesTransferred/l*1e3/1e6:0}}else{let o=e.startTime?s-e.startTime:0;n=o>0?e.bytesTransferred/o*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,a=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,l=n<r&&!a,u=[];for(let[y,p]of e.pieceTimeouts)if(i-p>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(y<=e.receivedWaterLevel||e.receivedOutOfOrder.has(y))){e.pieceTimeouts.delete(y);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&y<=e.ackedWaterLevel){e.pieceTimeouts.delete(y);continue}u.push(y)}if(u.length===0||l)return;let m=u.slice(0,50);for(let y of m)this._handlePieceTimeout(t,y)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),o=Date.now()-n;o>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:o}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var Ce="https://perkoon.com",$=class extends ve{constructor(t=Ce,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new D,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await K(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=await this._createSession(e.name,e.password);this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new M(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r}),this.transport=new z,this.transport.initialize(i.peer_id,i.ice_servers);let n=await this.transport.createOffer();this.signaling.sendOffer(r,n),this.transport.on("ice-candidate",({candidate:m,mid:y})=>{this.signaling.sendIceCandidate(r,{candidate:m,mid:y})}),this.signaling.on("ice_candidate",m=>{this.transport.addIceCandidate(m.candidate)});let o=await this._waitForSignal("answer");this.transport.handleAnswer(o.answer);let a=await this.transport.waitForDataChannel();this.emit("connected");let l=ke.randomUUID();this.engine.initSend(l,this.fileSource,a);let u=this.engine.transfers.get(l).manifest;this._setupControlHandler(a,l),a.send(JSON.stringify({type:"manifest",transfer_id:l,manifest:u})),this._setupProgressEvents(l),await this.engine.startSend(l);let f=await this._waitForCompletion(l);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:f.speed,duration:f.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new M(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver"),n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new z,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:p,mid:g})=>{this.signaling.sendIceCandidate(n,{candidate:p,mid:g})}),this.signaling.on("ice_candidate",p=>{this.transport.addIceCandidate(p.candidate)});let o=await this._waitForSignal("offer"),a=await this.transport.handleOffer(o.offer);this.signaling.sendAnswer(n,a);let l=await this.transport.waitForDataChannel();this.emit("connected");let u=i.stdout?ee():Y(e,{overwrite:i.overwrite}),f=[],y=await new Promise((p,g)=>{let h=null;l.addEventListener("message",async F=>{let T=F.data;if(typeof T=="string"){try{let S=JSON.parse(T);if(S.type==="manifest"){h=S.transfer_id;let _=S.manifest;this.emit("receiving-file",{name:_.fileName,size:_.fileSize}),await u.startStreamingReceive(h,{fileName:_.fileName,size:_.fileSize,fileSize:_.fileSize,type:_.fileType,pieceSize:_.pieceSize},{}),this.engine.initReceive(h,_,{streamingHandler:u}),this._setupProgressEvents(h),this.engine.startReceive(h,b=>{this._safeSend(l,JSON.stringify({type:"request",transfer_id:h,piece_index:b}))});let C=-1;this._ackInterval=setInterval(()=>{let b=this.engine.transfers.get(h);if(!b||b.state==="completed"||b.state==="completing"){clearInterval(this._ackInterval);return}let L=b.receivedWaterLevel;if(L>C){C=L;let q=b.receivedOutOfOrder.size>0?Array.from(b.receivedOutOfOrder):void 0;this._safeSend(l,JSON.stringify({type:"water_level_ack",transfer_id:h,water_level:L,out_of_order:q}))}},100)}}catch(S){S.message&&!S.message.includes("Unexpected token")&&g(S)}return}if(h&&(T instanceof ArrayBuffer||Buffer.isBuffer(T))){let S=T instanceof ArrayBuffer?T:T.buffer.slice(T.byteOffset,T.byteOffset+T.byteLength),{pieceIndex:_,data:C}=this.engine.decodePieceMessage(S);if(_>=0&&C&&(await this.engine.handlePieceData(h,_,C))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let L=this.engine.transfers.get(h);L&&this._safeSend(l,JSON.stringify({type:"water_level_ack",transfer_id:h,water_level:L.receivedWaterLevel}));let q=u.getFilePath(h);f.push(q),await this.engine.completeReceive(h),this._safeSend(l,JSON.stringify({type:"complete",transfer_id:h}));let G=this.engine.transfers.get(h),J=Date.now()-(G?.startTime||Date.now()),X=G?.manifest;p({files:f,speed:X?X.fileSize/(J/1e3):0,duration:J})}}}),setTimeout(()=>{g(new Error("Transfer timed out (10 minutes)"))},6e5)});return this.signaling.disconnect(),this.transport.close(),y}async _createSession(t,e){let i=`${this.serverUrl}/api/v1/sessions`,s={};t&&(s.name=t),e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${n.error||r.statusText}`)}return r.json()}async _joinSession(t,e){let i=`${this.serverUrl}/api/v1/sessions/${t}/join`,s={role:"receiver"};e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw r.status===404?new Error(`Session ${t} not found. Check the code and try again.`):r.status===410?new Error(`Session ${t} has expired.`):r.status===403?new Error(n.error||"Access denied"):new Error(`Failed to join session: ${n.error||r.statusText}`)}return r.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),s=r=>{(r.type==="ready"||r.type==="offer")&&(clearTimeout(i),this.signaling.removeListener("ready",s),this.signaling.removeListener("offer",s),t(r.from))};this.signaling.on("ready",s),this.signaling.on("offer",s)})}_waitForSignal(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error(`Timed out waiting for ${t} signal`))},3e4);this.signaling.once(t,r=>{clearTimeout(s),e(r)})})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let o=r>0?Math.round(n/r*100):0,a=Date.now()-(i.startTime||Date.now()),l=n*s.pieceSize,u=a>0?l/(a/1e3):0,f=u>0?Math.round((s.fileSize-l)/u):0;this.emit("progress",{transferId:t,percent:Math.min(o,100),speed:u,eta:f,bytesTransferred:l,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"request":this.engine.requeuePiece(e,s.piece_index);break;case"complete":{let r=this.engine.transfers.get(e);r&&r.state!=="completed"&&(r.piecesAckedCount=r.manifest.totalPieces,r.bytesTransferred=r.manifest.fileSize,r.inFlightPieces.clear(),r.state="completed")}this.emit("transfer-complete",{transferId:e});break;case"backpressure":s.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Transfer timed out (10 minutes)"))},6e5),r=()=>{let o=this.engine.transfers.get(t);if(!o){clearTimeout(s),i(new Error("Transfer context lost"));return}if(o.state==="completed"){clearTimeout(s);let a=Date.now()-o.startTime;e({speed:o.manifest.fileSize/(a/1e3),duration:a});return}if(o.state==="cancelled"||o.state==="failed"){clearTimeout(s),i(new Error("Transfer failed"));return}};this.on("transfer-complete",({transferId:o})=>{o===t&&r()});let n=setInterval(()=>{r();let o=this.engine.transfers.get(t);(!o||o.state==="completed"||o.state==="cancelled")&&clearInterval(n)},500)})}_safeSend(t,e){try{t.readyState==="open"&&t.send(e)}catch{}}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};import be from"node:path";import{access as Ae,stat as Ee}from"node:fs/promises";var te="0.1.3",ne="https://perkoon.com",re=0,R=1,se=2,H=3,Re=4,Be=5,A=process.argv.slice(2),ie=A[0],P={};for(let c=1;c<A.length;c++)if(A[c].startsWith("--")){let t=A[c].slice(2);A[c+1]&&!A[c+1].startsWith("--")?P[t]=A[++c]:P[t]=!0}else P._positional||(P._positional=A[c]);var E=ne,B=P.json===!0,O=P.quiet===!0,Le=P.overwrite===!0,j=P.output||"./received",x=typeof P.password=="string"?P.password:void 0,Q=typeof P.timeout=="string"?parseInt(P.timeout,10):NaN,I=!isNaN(Q)&&Q>0?Q:300;B&&j==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
3
+ `),process.exit(R));function d(c){!O&&!B&&process.stderr.write(c+`
4
+ `)}function w(c,t={}){B&&process.stdout.write(JSON.stringify({event:c,...t})+`
5
+ `)}function v(c){d(` \u2713 ${c}`)}function W(c){return c>=1024*1024*1024?`${(c/(1024*1024*1024)).toFixed(1)} GB`:c>=1024*1024?`${(c/(1024*1024)).toFixed(1)} MB`:c>=1024?`${(c/1024).toFixed(0)} KB`:`${c} B`}function U(c){return W(c)+"/s"}function oe(c){return c<=0?"...":c<60?`${c}s`:`${Math.floor(c/60)}m ${c%60}s`}function ae(c,t=30){let e=Math.round(t*c/100);return"\u2588".repeat(e)+"\u2591".repeat(t-e)}function k(c,t){d(`
6
+ Error: ${c}`),t&&d(`
7
+ ${t}`),d("")}function ce(c){let t=c.message||"";return t.includes("No peer connected")||t.includes("timed out")?Be:t.includes("Password required")||t.includes("Invalid password")||t.includes("Access denied")?Re:(t.includes("Failed to create session")||t.includes("Failed to join")||t.includes("not found")||t.includes("expired"),H)}async function Fe(){let c=P._positional;c||(k("No file specified.","Usage: perkoon send <file>"),process.exit(R));let t=be.resolve(c);try{await Ae(t),(await Ee(t)).isFile()||(k(`Not a regular file: ${t}`),process.exit(se))}catch{k(`File not found: ${t}`),process.exit(se)}let e=new $(E,{timeout:I*1e3});e.on("file-ready",({name:r,size:n})=>{d(""),v(`${r} (${W(n)})`),w("file_ready",{name:r,size:n})}),e.on("session-created",({sessionCode:r})=>{v(`Code: ${r}`),x&&v("Password protected"),d("");let n=x?` --password ${x}`:"";d(` Receiver command: perkoon receive ${r}${n}`),d(` Or open in browser: ${E}/${r}`),d(""),w("session_created",{session_code:r,share_url:`${E}/${r}`})}),e.on("waiting-for-receiver",()=>{d(" Waiting for receiver..."),w("waiting_for_receiver")}),e.on("receiver-connected",()=>{v("Receiver connected"),w("receiver_connected")}),e.on("connected",()=>{v("Direct connection established"),d(""),w("webrtc_connected")});let i="";e.on("progress",({percent:r,speed:n,eta:o,bytesTransferred:a})=>{if(B)w("progress",{percent:r,speed:Math.round(n),eta:o,bytes_transferred:a});else if(!O){let l=` ${ae(r)} ${String(r).padStart(3)}% ${U(n).padStart(10)} ETA ${oe(o)}`;l!==i&&(process.stderr.write(`\r${l}`),i=l)}});let s=()=>{d(`
8
8
 
9
- Cancelled.`),e.destroy(),process.exit(R)};process.on("SIGINT",s),process.on("SIGTERM",s);try{let r=await e.send(t,{password:x});if(!N&&!B){process.stderr.write(`
10
- `);let n=W(r.speed*(r.duration/1e3));S(`Complete: ${n} in ${(r.duration/1e3).toFixed(1)}s (${U(r.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}w("transfer_complete",{session_code:r.sessionCode,duration_ms:r.duration,speed:Math.round(r.speed)}),process.exit(ie)}catch(r){let n=oe(r);r.message.includes("No peer connected")?v(`No receiver joined after ${I}s.`,`Make sure they entered the right code.
11
- Or share the link: ${E}/${e.sessionCode||""}`):r.message.includes("Failed to create session")?v("Could not reach perkoon.com","Check your internet connection and try again."):v(r.message),w("error",{message:r.message,exit_code:n}),e.destroy(),process.exit(n)}}async function Le(){let l=P._positional;l||(v("No session code specified.","Usage: perkoon receive <code>"),process.exit(R)),/^[A-Za-z0-9]{12}$/i.test(l)||(v(`Invalid code: ${l}`,"Codes are 12 alphanumeric characters, like K7MX4QPR9W2N."),process.exit(R));let t=j==="-",e=l.toUpperCase(),i=new O(E,{timeout:I*1e3});i.on("session-joined",()=>{d(""),S(`Joined session ${e}`),w("session_joined",{session_code:e})}),i.on("sender-found",()=>{S("Sender found"),w("sender_found")}),i.on("connected",()=>{S("Direct connection established"),w("webrtc_connected")}),i.on("receiving-file",({name:n,size:o})=>{S(`Receiving: ${n} (${W(o)})`),d(""),w("receiving_file",{name:n,size:o})});let s="";i.on("progress",({percent:n,speed:o,eta:a,bytesTransferred:c})=>{if(B)w("progress",{percent:n,speed:Math.round(o),eta:a,bytes_transferred:c});else if(!N){let h=` ${re(n)} ${String(n).padStart(3)}% ${U(o).padStart(10)} ETA ${ne(a)}`;h!==s&&(process.stderr.write(`\r${h}`),s=h)}});let r=()=>{d(`
9
+ Cancelled.`),e.destroy(),process.exit(R)};process.on("SIGINT",s),process.on("SIGTERM",s);try{let r=await e.send(t,{password:x});if(!O&&!B){process.stderr.write(`
10
+ `);let n=W(r.speed*(r.duration/1e3));v(`Complete: ${n} in ${(r.duration/1e3).toFixed(1)}s (${U(r.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}w("transfer_complete",{session_code:r.sessionCode,duration_ms:r.duration,speed:Math.round(r.speed)}),process.exit(re)}catch(r){let n=ce(r);r.message.includes("No peer connected")?k(`No receiver joined after ${I}s.`,`Make sure they entered the right code.
11
+ Or share the link: ${E}/${e.sessionCode||""}`):r.message.includes("Failed to create session")?k("Could not reach perkoon.com","Check your internet connection and try again."):k(r.message),w("error",{message:r.message,exit_code:n}),e.destroy(),process.exit(n)}}async function ze(){let c=P._positional;c||(k("No session code specified.","Usage: perkoon receive <code>"),process.exit(R)),/^[A-Za-z0-9]{12}$/i.test(c)||(k(`Invalid code: ${c}`,"Codes are 12 alphanumeric characters, like K7MX4QPR9W2N."),process.exit(R));let t=j==="-",e=c.toUpperCase(),i=new $(E,{timeout:I*1e3});i.on("session-joined",()=>{d(""),v(`Joined session ${e}`),w("session_joined",{session_code:e})}),i.on("sender-found",()=>{v("Sender found"),w("sender_found")}),i.on("connected",()=>{v("Direct connection established"),w("webrtc_connected")}),i.on("receiving-file",({name:n,size:o})=>{v(`Receiving: ${n} (${W(o)})`),d(""),w("receiving_file",{name:n,size:o})});let s="";i.on("progress",({percent:n,speed:o,eta:a,bytesTransferred:l})=>{if(B)w("progress",{percent:n,speed:Math.round(o),eta:a,bytes_transferred:l});else if(!O){let u=` ${ae(n)} ${String(n).padStart(3)}% ${U(o).padStart(10)} ETA ${oe(a)}`;u!==s&&(process.stderr.write(`\r${u}`),s=u)}});let r=()=>{d(`
12
12
 
13
- Cancelled.`),i.destroy(),process.exit(R)};process.on("SIGINT",r),process.on("SIGTERM",r);try{let n={overwrite:Re,password:x};t&&(n.stdout=!0);let o=await i.receive(e,t?null:j,n);if(!N&&!B){if(process.stderr.write(`
14
- `),!t)for(let a of o.files)S(`Saved: ${a}`);S(`Complete: ${(o.duration/1e3).toFixed(1)}s (${U(o.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}w("transfer_complete",{files:o.files,duration_ms:o.duration,speed:Math.round(o.speed)}),process.exit(ie)}catch(n){let o=oe(n);n.message.includes("not found")?v(`Session not found (${e})`,`The code may be expired or mistyped.
15
- Ask the sender for a new code.`):n.message.includes("expired")?v(`Session expired (${e})`,"Ask the sender to create a new session."):v(n.message),w("error",{message:n.message,exit_code:o}),i.destroy(),process.exit(o)}}switch(te){case"send":Be();break;case"receive":Le();break;case"--version":case"-v":console.log(`perkoon v${Y}`);break;case"--help":case"-h":case void 0:console.log(`
16
- perkoon v${Y} \u2014 Fast P2P file transfer. No cloud. No accounts.
13
+ Cancelled.`),i.destroy(),process.exit(R)};process.on("SIGINT",r),process.on("SIGTERM",r);try{let n={overwrite:Le,password:x};t&&(n.stdout=!0);let o=await i.receive(e,t?null:j,n);if(!O&&!B){if(process.stderr.write(`
14
+ `),!t)for(let a of o.files)v(`Saved: ${a}`);v(`Complete: ${(o.duration/1e3).toFixed(1)}s (${U(o.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}w("transfer_complete",{files:o.files,duration_ms:o.duration,speed:Math.round(o.speed)}),process.exit(re)}catch(n){let o=ce(n);n.message.includes("not found")?k(`Session not found (${e})`,`The code may be expired or mistyped.
15
+ Ask the sender for a new code.`):n.message.includes("expired")?k(`Session expired (${e})`,"Ask the sender to create a new session."):k(n.message),w("error",{message:n.message,exit_code:o}),i.destroy(),process.exit(o)}}switch(ie){case"send":Fe();break;case"receive":ze();break;case"--version":case"-v":console.log(`perkoon v${te}`);break;case"--help":case"-h":case void 0:console.log(`
16
+ perkoon v${te} \u2014 Fast P2P file transfer. No cloud. No accounts.
17
17
 
18
18
  Usage:
19
19
  perkoon send <file> Send a file
@@ -40,5 +40,5 @@ import{EventEmitter as Te}from"node:events";import Se from"node:crypto";import a
40
40
  4 Auth error (wrong password, access denied)
41
41
  5 Timeout (no peer connected)
42
42
 
43
- Learn more: ${se}
44
- `);break;default:v(`Unknown command: ${te}`,"Run perkoon --help for usage."),process.exit(R)}
43
+ Learn more: ${ne}
44
+ `);break;default:k(`Unknown command: ${ie}`,"Run perkoon --help for usage."),process.exit(R)}
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- import{EventEmitter as X}from"node:events";import Z from"node:crypto";import D from"node-datachannel";import{EventEmitter as N}from"node:events";var{PeerConnection:x}=D,v=class extends N{constructor(){super(),this.pc=null,this.dataChannel=null,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let i={iceServers:e.map(s=>(Array.isArray(s.urls)?s.urls:[s.urls]).map(n=>s.username&&s.credential?`${n}:${s.username}:${s.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new x(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{this.dataChannel=this._wrapChannel(s),this.emit("data-channel",this.dataChannel)})}async createOffer(){return new Promise(t=>{this.pc.onLocalDescription((i,s)=>{t({sdp:i,type:s})});let e=this.pc.createDataChannel("transfer",{ordered:!1,maxRetransmits:0});this.dataChannel=this._wrapChannel(e)})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((i,s)=>{e({sdp:i,type:s})}),this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()})}handleAnswer(t){this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()}addIceCandidate(t){t.candidate&&(this.remoteDescriptionSet?this.pc.addRemoteCandidate(t.candidate,t.mid||"0"):this.pendingCandidates.push(t))}_flushPendingCandidates(){for(let t of this.pendingCandidates)this.pc.addRemoteCandidate(t.candidate,t.mid||"0");this.pendingCandidates=[]}getDataChannel(){return this.dataChannel}async waitForDataChannel(t=3e4){return this.dataChannel?.readyState==="open"?this.dataChannel:new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Data channel open timeout"))},t),r=n=>{n.readyState==="open"?(clearTimeout(s),e(n)):n.addEventListener("open",()=>{clearTimeout(s),e(n)})};this.dataChannel?r(this.dataChannel):this.once("data-channel",n=>{r(n)})})}close(){if(this.dataChannel){try{this.dataChannel.close()}catch{}this.dataChannel=null}if(this.pc){try{this.pc.close()}catch{}this.pc=null}}_wrapChannel(t){let e=new Map,i="connecting";try{t.isOpen&&t.isOpen()&&(i="open")}catch{}let s={get readyState(){return i},get bufferedAmount(){try{return t.bufferedAmount()||0}catch{return 0}},send(n){n instanceof ArrayBuffer?t.sendMessageBinary(Buffer.from(n)):n instanceof Uint8Array?t.sendMessageBinary(Buffer.from(n.buffer,n.byteOffset,n.byteLength)):Buffer.isBuffer(n)?t.sendMessageBinary(n):t.sendMessage(String(n))},close(){i="closed";try{t.close()}catch{}},addEventListener(n,a){e.has(n)||e.set(n,new Set),e.get(n).add(a)},removeEventListener(n,a){let o=e.get(n);o&&o.delete(a)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let a;Buffer.isBuffer(n)?a=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):a=n,r("message",{data:a})});function r(n,a){let o=e.get(n);if(o)for(let c of o)try{c(a)}catch{}}return s}};import{Socket as W}from"phoenix";import U from"ws";import{EventEmitter as q}from"node:events";var k=class extends q{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,a)=>{this.socket=new W(r,{params:{},transport:U,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(o=>{this.connected||a(new Error(`Signaling connection failed: ${o?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",o=>{this.emit(o.type,o)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",o=>{this.connected=!0,this.peers=o.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",o=>{a(new Error(`Channel join failed: ${o.reason||"unknown"}`))}).receive("timeout",()=>{a(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}disconnect(){if(this._stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connected=!1}_startHeartbeat(){this._stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.connected&&this.channel&&this.channel.push("heartbeat",{})},1e4)}_stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}};import{open as $}from"node:fs/promises";import H from"node:path";import{lookup as Q}from"mime-types";async function E(u){let t=await $(u,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${u}`);return{name:H.basename(u),size:e.size,type:Q(u)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:a}=await t.read(n,0,r,i);return a===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+a)},async close(){await t.close()}}}import{open as j,mkdir as J}from"node:fs/promises";import V from"node:path";function B(u,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await J(u,{recursive:!0});let n=G(s.fileName),a=V.join(u,n);if(!t.overwrite)try{let{access:c}=await import("node:fs/promises");throw await c(a),new Error(`File already exists: ${a} (use --overwrite to replace)`)}catch(c){if(c.code!=="ENOENT")throw c}let o=await j(a,"w");s.size>0&&await o.truncate(s.size),e.set(i,{fh:o,filePath:a,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let a=s*n.pieceSize,o=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(o,0,o.byteLength,a),{success:!0}}catch(c){return{success:!1,error:c.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}function G(u){return u.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function O(){let u=new Map;return{async startStreamingReceive(t,e,i){u.set(t,{pieceSize:e.pieceSize,totalPieces:Math.ceil(e.size/e.pieceSize),size:e.size,nextPiece:0,buffer:new Map,bytesWritten:0})},async writeChunk(t,e,i){let s=u.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let a=s.nextPiece*s.pieceSize,o=s.size-a,c=o<n.byteLength?n.subarray(0,o):n;process.stdout.write(c),s.bytesWritten+=c.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){u.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){u.delete(t)}}}var b=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=a=>{r&&clearTimeout(r),i(a)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var K={pieceSize:84*1024,maxConcurrentPieces:8,pieceTimeout:3e4,idleTimeout:6e4,maxPieceRetries:3,maxRetryQueueSize:1e4,maxInFlightMultiplier:10,highWaterMark:16*1024*1024,lowWaterMark:4*1024*1024,maxPiecesPerSecond:5e3,rateLimitWindowMs:1e3,channelRotationThreshold:512*1024*1024,channelRotationEnabled:!1,parallelChannels:1,parallelChannelsEnabled:!1,parallelChannelsThreshold:100*1024*1024,pacingEnabled:!0,pacingThreshold:0,pacingIntervalMs:10,targetBufferedAmount:8*1024*1024,minBufferedAmount:4*1024*1024,maxBufferedAmount:16*1024*1024,maxPiecesPerTick:32,burstPiecesPerTick:64,maxPendingReads:512,maxInFlightPieces:4e3},A=class extends b{constructor(t={}){super(),this.config={...K,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let a=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=a,this._checkProgressMilestones(t,i,a),this._emitProgress(t,i,a),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let a=i.inFlightPieces.get(n);if(!a)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let o=a.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=o,r+=o;let c=a.channelIndex>=0?a.channelIndex:0;i.channelBytes[c]!==void 0&&(i.channelBytes[c]+=o)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let o of i)if(typeof o!="number"||o<0||s.manifest&&o>=s.manifest.totalPieces)return}let r=Date.now(),n=0,a=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let o=[];for(let[c,h]of s.inFlightPieces)c<=e&&o.push([c,h]);for(let[c,h]of o){s.inFlightPieces.delete(c);let l=h.pieceSize||this._getPieceSize(s.manifest,c);s.bytesTransferred+=l,n+=l,a++,s.pieceTimeouts.delete(c)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let o of i){let c=s.inFlightPieces.get(o);if(c){s.inFlightPieces.delete(o);let h=c.pieceSize||this._getPieceSize(s.manifest,o);s.bytesTransferred+=h,n+=h,a++,s.piecesAckedCount++,s.pieceTimeouts.delete(o)}}if(a>0){s.lastActivityTime=r;let o=s.piecesAckedCount/s.manifest.totalPieces*100;s.lastProgress=o,this._checkProgressMilestones(t,s,o),this._emitProgress(t,s,o),s.piecesAckedCount===s.manifest.totalPieces&&this._completeSend(t)}}requeuePiece(t,e){let i=this.transfers.get(t);!i||i.type!=="send"||i.state==="completed"||i.state==="completing"||(i.inFlightPieces.delete(e),i.retryQueue.includes(e)||i.retryQueue.push(e),!i.paused&&!i.usePacing&&this._sendNextPieces(t))}markSendComplete(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.state==="completed"||e.state==="completing"||this._completeSend(t)}_checkChannelRotation(t,e,i){if(!this.config.channelRotationEnabled||e.pendingRotations.has(i))return;let s=e.channelBytes[i]||0,r=e.channelThresholds?.[i]||this.config.channelRotationThreshold;s>=r&&(e.pendingRotations.add(i),this.emit("channel-rotation-needed",{transferId:t,channelIndex:i,bytesTransferred:e.bytesTransferred,channelBytes:s,threshold:r,rotationCount:e.channelRotationCounts[i]||0}))}rotateSingleChannel(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||e>=s.channels.length)return;let r=s.channels[e],n=(s.channelRotationCounts[e]||0)+1;s.channels[e]=i,e===0&&(s.channel=i),s.channelBytes[e]=0,s.channelRotationCounts[e]=n,s.pendingRotations.delete(e),i.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!s.paused&&!s.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,i){let s=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(s,50)}};s(),setTimeout(()=>{if(t.readyState==="open")try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}},5e3)}rotateChannel(t,e){this.rotateChannels(t,[e])}initReceive(t,e,i={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s={id:t,type:"receive",manifest:e,state:"initialized",nextPieceToRequest:0,piecesReceivedCount:0,receivedWaterLevel:-1,receivedOutOfOrder:new Set,maxOutOfOrderSize:1e4,piecesRequested:new Set,bytesTransferred:0,streamingHandler:i.streamingHandler||null,backpressureCheckerId:null,fileBuffer:null,pieceBuffers:new Map,paused:!1,senderPaused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return s.streamingHandler||this._initReceiveBuffer(s,e),this.transfers.set(t,s),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(s)}}startReceive(t,e){let i=this.transfers.get(t);if(!i||i.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);i.state="transferring",i.startTime=i.startTime||Date.now(),i.requestPiece=e,this._startIdleTimeout(t),this._startBackpressureChecker(t),this._requestNextPieces(t)}_startBackpressureChecker(t){let e=this.transfers.get(t);!e||e.backpressureCheckerId||(e.backpressureCheckerId=setInterval(()=>{let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="cancelled"){this._stopBackpressureChecker(t);return}if(i.streamingHandler?.getBackpressureInfo){let s=i.streamingHandler.getBackpressureInfo();!s.isUnderPressure&&i.senderPaused&&(i.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:s.overflowSize}))}},200))}_stopBackpressureChecker(t){let e=this.transfers.get(t);!e||!e.backpressureCheckerId||(clearInterval(e.backpressureCheckerId),e.backpressureCheckerId=null)}async handlePieceData(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="receive")return{success:!1,error:"Transfer not found"};if(s.state==="completing"||s.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:s.lastProgress||0};let r=i.byteLength;if(s.piecesReceivedCount<100){if(e<0||e>=s.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(i instanceof ArrayBuffer||i instanceof Uint8Array||ArrayBuffer.isView(i)))return{success:!1,error:"Invalid piece data type"};if(r===0&&!(e===0&&s.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(s.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"}}if(s.streamingHandler?.getBackpressureInfo){let n=s.streamingHandler.getBackpressureInfo();n.isUnderPressure&&!s.senderPaused?(s.senderPaused=!0,this.emit("send-backpressure",{transferId:t,pause:!0,bufferSize:n.overflowSize})):!n.isUnderPressure&&s.senderPaused&&(s.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:n.overflowSize}))}try{if(e===s.receivedWaterLevel+1){for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;s.lastActivityTime=Date.now()}else if(e>s.receivedWaterLevel+1){if(s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}if(s.streamingHandler?s.streamingHandler.writeChunk(t,e,i).then(a=>{a&&!a.success&&!a.duplicate&&(a.error!=="OVERFLOW_TIMEOUT"&&console.warn("[TransferEngine] Write issue for piece",e,":",a.error),a.error==="SW_DOWNLOAD_DEAD"&&a.retryable===!1&&(console.error("[TransferEngine] Fatal write error - aborting transfer:",t),this.emit("transfer-failed",{transferId:t,error:"Download stream cancelled. Please try again.",fatal:!0}),this.cancel(t)))}).catch(a=>{console.error("[TransferEngine] Write error for piece",e,":",a.message)}):this._storePieceInMemory(s,e,i),s.receivedOutOfOrder.size>=100&&s.requestPiece){let a=Date.now(),o=s.lastGapCheckTime||0;if(a-o>5e3){s.lastGapCheckTime=a;let c=s.receivedWaterLevel+1,h=Math.min(c+10,e);for(let l=c;l<h;l++)!s.receivedOutOfOrder.has(l)&&!s.piecesRequested.has(l)&&(s.piecesRequested.add(l),s.requestPiece(l))}}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.pieceTimeouts.delete(e),s.pieceRetries.delete(e),s.bytesTransferred+=r;let n=s.piecesReceivedCount/s.manifest.totalPieces*100;return s.lastProgress=n,this._checkProgressMilestones(t,s,n),this._emitProgress(t,s,n),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(!s.paused&&s.piecesReceivedCount%10===0&&this._requestNextPieces(t),{success:!0})}catch(n){return{success:!1,error:n.message}}}async completeReceive(t){let e=this.transfers.get(t);if(!e||e.type!=="receive")return{success:!1,error:"Transfer not found"};if(e.piecesReceivedCount!==e.manifest.totalPieces)return{success:!1,error:`Incomplete: ${e.piecesReceivedCount}/${e.manifest.totalPieces}`};e.state="completing",this._stopIdleTimeout(t),this._stopBackpressureChecker(t);try{if(e.streamingHandler)return await e.streamingHandler.completeStream(t),e.state="completed",{success:!0,streaming:!0,manifest:e.manifest};{let i=this._assembleFile(e);return e.state="completed",{success:!0,blob:i,manifest:e.manifest,fileName:e.manifest.fileName}}}catch(i){return{success:!1,error:i.message}}}pause(t){let e=this.transfers.get(t);e&&(e.paused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t),this.emit("paused",{transferId:t}))}resume(t){let e=this.transfers.get(t);!e||!e.paused||(e.paused=!1,this._startIdleTimeout(t),e.type==="send"?e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t):this._requestNextPieces(t),this.emit("resumed",{transferId:t}))}pauseForBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused||(e.backpressurePaused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t))}resumeFromBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused&&(e.backpressurePaused=!1,!e.paused&&(this._startIdleTimeout(t),e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t)))}cancel(t){let e=this.transfers.get(t);e&&(this._cleanupTransfer(t),this.emit("cancelled",{transferId:t,type:e.type}))}cleanup(t){this._cleanupTransfer(t)}getStats(t){let e=this.transfers.get(t);if(!e)return null;let i=e.startTime?Date.now()-e.startTime:0,s=i>0?e.bytesTransferred/i*1e3:0;return{transferId:t,type:e.type,state:e.state,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),piecesCompleted:e.type==="send"?e.piecesAckedCount:e.piecesReceivedCount,totalPieces:e.manifest.totalPieces,speed:s,elapsed:i,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.retryQueue.length>=this.config.maxRetryQueueSize){this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=i)return;let s=e.channel,r=e.channels.length===1;if(r){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return}else{let l=!1;for(let p of e.channels)if(p.readyState==="open"&&p.bufferedAmount<=this.config.highWaterMark){l=!0;break}if(!l)return}let a=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(a<=0)return;let o=[];for(;e.retryQueue.length>0&&o.length<a;){let l=e.retryQueue.shift();e.inFlightPieces.has(l)||o.push(l)}for(;e.nextPieceToSend<e.manifest.totalPieces&&o.length<a;){let l=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(l)||o.push(l)}if(o.length===0)return;let c=Date.now();for(let l of o)e.inFlightPieces.set(l,{sendTime:c,pieceSize:this._getPieceSize(e.manifest,l)});let h=r?4:4*e.channels.length;try{for(let l=0;l<o.length;l+=h){let g=o.slice(l,l+h).map(async m=>{let f=await this._readPiece(e.file,e.manifest,m);return{pieceIndex:m,data:f}}),d=await Promise.all(g);if(r)for(let{pieceIndex:m,data:f}of d){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,m,f)),this._startPieceTimeout(t,m)}else for(let{pieceIndex:m,data:f}of d){let C=null,_=-1;for(let P=0;P<e.channels.length;P++){let y=(e.channelIndex+P)%e.channels.length,w=e.channels[y];if(w.readyState==="open"&&w.bufferedAmount<=this.config.highWaterMark){C=w,_=y,e.channelIndex=(y+1)%e.channels.length;break}}if(!C)return;C.send(this._encodePieceMessage(t,m,f)),e.inFlightPieces.set(m,{sendTime:Date.now(),channelIndex:_,pieceSize:this._getPieceSize(e.manifest,m)}),this._startPieceTimeout(t,m)}}}catch(l){for(let p of o)e.inFlightPieces.delete(p),e.retryQueue.push(p);this.emit("error",{transferId:t,error:`Failed to send pieces: ${l.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,a=this.config.maxInFlightPieces*.99,o=this.config.maxPendingReads*.95,c=r<2*1024*1024,h=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=a||i>=n&&c?"RECEIVER_SLOW":e.pendingReads>=o?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:h})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let l=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?l=32:r>this.config.minBufferedAmount?l=this.config.maxPiecesPerTick:l=this.config.burstPiecesPerTick;let p=this.config.maxPendingReads-e.pendingReads;if(p<=0)return;l=Math.min(l,p);let g=this.config.maxInFlightPieces-i;l=Math.min(l,g);let d=[];for(;e.retryQueue.length>0&&d.length<l;){let f=e.retryQueue.shift();e.inFlightPieces.has(f)||d.push(f)}for(;e.nextPieceToSend<e.manifest.totalPieces&&d.length<l;){let f=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(f)||d.push(f)}if(d.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=d.length;let m=Date.now();for(let f of d)e.inFlightPieces.set(f,{sendTime:m,pieceSize:this._getPieceSize(e.manifest,f)});this._readAndSendPieces(t,e,s,d)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(a=>this._readPiece(e.file,e.manifest,a).then(o=>({pieceIndex:a,data:o})).catch(o=>({pieceIndex:a,error:o}))),n=await Promise.all(r);for(let a of n){if(a.error){e.inFlightPieces.delete(a.pieceIndex),e.retryQueue.push(a.pieceIndex);continue}let{pieceIndex:o,data:c}=a,h=e.channel;if(!h||h.readyState!=="open"){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}if(h.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}try{h.send(this._encodePieceMessage(t,o,c)),this._startPieceTimeout(t,o)}catch{e.inFlightPieces.delete(o),e.retryQueue.push(o)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let o=0;o<e.totalPieces;o++){let c=s.get(o);if(!c)throw new Error(`Missing piece ${o}`);n.push(c)}let a=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),a}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),a=new Uint8Array(4+n.byteLength);return a.set(new Uint8Array(n),4),a}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,a=e.lastSpeedTime||e.startTime||s,o=s-a;if(o>=200){let p=e.bytesTransferred-n;r=o>0?p/o*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=c*r+(1-c)*e.smoothedSpeed);let h=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let l=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:l,speed:h})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){if(!t||!(t instanceof ArrayBuffer)||t.byteLength<4)return{pieceIndex:-1,data:null,error:"invalid_message"};try{let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}catch{return{pieceIndex:-1,data:null,error:"decode_failed"}}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let a=s-e.lastMilestoneCheck.time,o=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(a>0&&a<r&&o>0)n=o/a*1e3/1e6;else if(a>=r){let c=e.startTime?s-e.startTime:0;n=c>0?e.bytesTransferred/c*1e3/1e6:0}}else{let a=e.startTime?s-e.startTime:0;n=a>0?e.bytesTransferred/a*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,o=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,c=n<r&&!o,h=[];for(let[g,d]of e.pieceTimeouts)if(i-d>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(g<=e.receivedWaterLevel||e.receivedOutOfOrder.has(g))){e.pieceTimeouts.delete(g);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&g<=e.ackedWaterLevel){e.pieceTimeouts.delete(g);continue}h.push(g)}if(h.length===0||c)return;let p=h.slice(0,50);for(let g of p)this._handlePieceTimeout(t,g)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),a=Date.now()-n;a>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:a}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var Y="https://perkoon.com",L=class extends X{constructor(t=Y,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new A,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await E(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=await this._createSession(e.name,e.password);this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new k(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r}),this.transport=new v,this.transport.initialize(i.peer_id,i.ice_servers);let n=await this.transport.createOffer();this.signaling.sendOffer(r,n),this.transport.on("ice-candidate",({candidate:p,mid:g})=>{this.signaling.sendIceCandidate(r,{candidate:p,mid:g})}),this.signaling.on("ice_candidate",p=>{this.transport.addIceCandidate(p.candidate)});let a=await this._waitForSignal("answer");this.transport.handleAnswer(a.answer);let o=await this.transport.waitForDataChannel();this.emit("connected");let c=Z.randomUUID();this.engine.initSend(c,this.fileSource,o);let h=this.engine.transfers.get(c).manifest;this._setupControlHandler(o,c),o.send(JSON.stringify({type:"manifest",transfer_id:c,manifest:h})),this._setupProgressEvents(c),await this.engine.startSend(c);let l=await this._waitForCompletion(c);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:l.speed,duration:l.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new k(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver"),n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new v,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:d,mid:m})=>{this.signaling.sendIceCandidate(n,{candidate:d,mid:m})}),this.signaling.on("ice_candidate",d=>{this.transport.addIceCandidate(d.candidate)});let a=await this._waitForSignal("offer"),o=await this.transport.handleOffer(a.offer);this.signaling.sendAnswer(n,o);let c=await this.transport.waitForDataChannel();this.emit("connected");let h=i.stdout?O():B(e,{overwrite:i.overwrite}),l=[],g=await new Promise((d,m)=>{let f=null;c.addEventListener("message",async C=>{let _=C.data;if(typeof _=="string"){try{let P=JSON.parse(_);if(P.type==="manifest"){f=P.transfer_id;let y=P.manifest;this.emit("receiving-file",{name:y.fileName,size:y.fileSize}),await h.startStreamingReceive(f,{fileName:y.fileName,size:y.fileSize,fileSize:y.fileSize,type:y.fileType,pieceSize:y.pieceSize},{}),this.engine.initReceive(f,y,{streamingHandler:h}),this._setupProgressEvents(f),this.engine.startReceive(f,T=>{this._safeSend(c,JSON.stringify({type:"request",transfer_id:f,piece_index:T}))});let w=-1;this._ackInterval=setInterval(()=>{let T=this.engine.transfers.get(f);if(!T||T.state==="completed"||T.state==="completing"){clearInterval(this._ackInterval);return}let S=T.receivedWaterLevel;if(S>w){w=S;let R=T.receivedOutOfOrder.size>0?Array.from(T.receivedOutOfOrder):void 0;this._safeSend(c,JSON.stringify({type:"water_level_ack",transfer_id:f,water_level:S,out_of_order:R}))}},100)}}catch{}return}if(f&&(_ instanceof ArrayBuffer||Buffer.isBuffer(_))){let P=_ instanceof ArrayBuffer?_:_.buffer.slice(_.byteOffset,_.byteOffset+_.byteLength),{pieceIndex:y,data:w}=this.engine.decodePieceMessage(P);if(y>=0&&w&&(await this.engine.handlePieceData(f,y,w))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let S=this.engine.transfers.get(f);S&&this._safeSend(c,JSON.stringify({type:"water_level_ack",transfer_id:f,water_level:S.receivedWaterLevel}));let R=h.getFilePath(f);l.push(R),await this.engine.completeReceive(f),this._safeSend(c,JSON.stringify({type:"complete",transfer_id:f}));let z=this.engine.transfers.get(f),F=Date.now()-(z?.startTime||Date.now()),M=z?.manifest;d({files:l,speed:M?M.fileSize/(F/1e3):0,duration:F})}}}),setTimeout(()=>{m(new Error("Transfer timed out (10 minutes)"))},6e5)});return this.signaling.disconnect(),this.transport.close(),g}async _createSession(t,e){let i=`${this.serverUrl}/api/v1/sessions`,s={};t&&(s.name=t),e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${n.error||r.statusText}`)}return r.json()}async _joinSession(t,e){let i=`${this.serverUrl}/api/v1/sessions/${t}/join`,s={role:"receiver"};e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw r.status===404?new Error(`Session ${t} not found. Check the code and try again.`):r.status===410?new Error(`Session ${t} has expired.`):r.status===403?new Error(n.error||"Access denied"):new Error(`Failed to join session: ${n.error||r.statusText}`)}return r.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),s=r=>{(r.type==="ready"||r.type==="offer")&&(clearTimeout(i),this.signaling.removeListener("ready",s),this.signaling.removeListener("offer",s),t(r.from))};this.signaling.on("ready",s),this.signaling.on("offer",s)})}_waitForSignal(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error(`Timed out waiting for ${t} signal`))},3e4);this.signaling.once(t,r=>{clearTimeout(s),e(r)})})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let a=r>0?Math.round(n/r*100):0,o=Date.now()-(i.startTime||Date.now()),c=n*s.pieceSize,h=o>0?c/(o/1e3):0,l=h>0?Math.round((s.fileSize-c)/h):0;this.emit("progress",{transferId:t,percent:Math.min(a,100),speed:h,eta:l,bytesTransferred:c,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"request":this.engine.requeuePiece(e,s.piece_index);break;case"complete":{let r=this.engine.transfers.get(e);r&&r.state!=="completed"&&(r.piecesAckedCount=r.manifest.totalPieces,r.bytesTransferred=r.manifest.fileSize,r.inFlightPieces.clear(),r.state="completed")}this.emit("transfer-complete",{transferId:e});break;case"backpressure":s.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Transfer timed out (10 minutes)"))},6e5),r=()=>{let a=this.engine.transfers.get(t);if(!a){clearTimeout(s),i(new Error("Transfer context lost"));return}if(a.state==="completed"){clearTimeout(s);let o=Date.now()-a.startTime;e({speed:a.manifest.fileSize/(o/1e3),duration:o});return}if(a.state==="cancelled"||a.state==="failed"){clearTimeout(s),i(new Error("Transfer failed"));return}};this.on("transfer-complete",({transferId:a})=>{a===t&&r()});let n=setInterval(()=>{r();let a=this.engine.transfers.get(t);(!a||a.state==="completed"||a.state==="cancelled")&&clearInterval(n)},500)})}_safeSend(t,e){try{t.readyState==="open"&&t.send(e)}catch{}}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};export{v as NodeTransport,k as SignalingClient,L as TransferManager,B as createNodeFileSink,E as createNodeFileSource};
1
+ import{EventEmitter as Y}from"node:events";import I from"node:crypto";import N from"node-datachannel";import{EventEmitter as W}from"node:events";var{PeerConnection:$}=N,k=class extends W{constructor(){super(),this.pc=null,this.dataChannel=null,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let i={iceServers:e.map(s=>(Array.isArray(s.urls)?s.urls:[s.urls]).map(n=>s.username&&s.credential?`${n}:${s.username}:${s.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new $(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{this.dataChannel=this._wrapChannel(s),this.emit("data-channel",this.dataChannel)})}async createOffer(){return new Promise(t=>{this.pc.onLocalDescription((i,s)=>{t({sdp:i,type:s})});let e=this.pc.createDataChannel("transfer",{ordered:!1,maxRetransmits:0});this.dataChannel=this._wrapChannel(e)})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((i,s)=>{e({sdp:i,type:s})}),this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()})}handleAnswer(t){this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()}addIceCandidate(t){t.candidate&&(this.remoteDescriptionSet?this.pc.addRemoteCandidate(t.candidate,t.mid||"0"):this.pendingCandidates.push(t))}_flushPendingCandidates(){for(let t of this.pendingCandidates)this.pc.addRemoteCandidate(t.candidate,t.mid||"0");this.pendingCandidates=[]}getDataChannel(){return this.dataChannel}async waitForDataChannel(t=3e4){return this.dataChannel?.readyState==="open"?this.dataChannel:new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Data channel open timeout"))},t),r=n=>{n.readyState==="open"?(clearTimeout(s),e(n)):n.addEventListener("open",()=>{clearTimeout(s),e(n)})};this.dataChannel?r(this.dataChannel):this.once("data-channel",n=>{r(n)})})}close(){if(this.dataChannel){try{this.dataChannel.close()}catch{}this.dataChannel=null}if(this.pc){try{this.pc.close()}catch{}this.pc=null}}_wrapChannel(t){let e=new Map,i="connecting";try{t.isOpen&&t.isOpen()&&(i="open")}catch{}let s={get readyState(){return i},get bufferedAmount(){try{return t.bufferedAmount()||0}catch{return 0}},send(n){n instanceof ArrayBuffer?t.sendMessageBinary(Buffer.from(n)):n instanceof Uint8Array?t.sendMessageBinary(Buffer.from(n.buffer,n.byteOffset,n.byteLength)):Buffer.isBuffer(n)?t.sendMessageBinary(n):t.sendMessage(String(n))},close(){i="closed";try{t.close()}catch{}},addEventListener(n,a){e.has(n)||e.set(n,new Set),e.get(n).add(a)},removeEventListener(n,a){let o=e.get(n);o&&o.delete(a)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let a;Buffer.isBuffer(n)?a=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):a=n,r("message",{data:a})});function r(n,a){let o=e.get(n);if(o)for(let c of o)try{c(a)}catch{}}return s}};import{Socket as U}from"phoenix";import q from"ws";import{EventEmitter as H}from"node:events";var v=class extends H{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,a)=>{this.socket=new U(r,{params:{},transport:q,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(o=>{this.connected||a(new Error(`Signaling connection failed: ${o?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",o=>{this.emit(o.type,o)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",o=>{this.connected=!0,this.peers=o.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",o=>{a(new Error(`Channel join failed: ${o.reason||"unknown"}`))}).receive("timeout",()=>{a(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}disconnect(){if(this._stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connected=!1}_startHeartbeat(){this._stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.connected&&this.channel&&this.channel.push("heartbeat",{})},1e4)}_stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}};import{open as Q}from"node:fs/promises";import j from"node:path";import{lookup as J}from"mime-types";async function B(u){let t=await Q(u,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${u}`);return{name:j.basename(u),size:e.size,type:J(u)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:a}=await t.read(n,0,r,i);return a===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+a)},async close(){await t.close()}}}import{open as V,mkdir as G,access as O}from"node:fs/promises";import D from"node:path";function E(u,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await G(u,{recursive:!0});let n=X(s.fileName),a=D.join(u,n);t.overwrite||(a=await K(a));let o=await V(a,"w");s.size>0&&await o.truncate(s.size),e.set(i,{fh:o,filePath:a,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let a=s*n.pieceSize,o=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(o,0,o.byteLength,a),{success:!0}}catch(c){return{success:!1,error:c.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}async function K(u){try{await O(u)}catch{return u}let t=D.extname(u),e=u.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await O(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function X(u){return u.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function x(){let u=new Map;return{async startStreamingReceive(t,e,i){u.set(t,{pieceSize:e.pieceSize,totalPieces:Math.ceil(e.size/e.pieceSize),size:e.size,nextPiece:0,buffer:new Map,bytesWritten:0})},async writeChunk(t,e,i){let s=u.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let a=s.nextPiece*s.pieceSize,o=s.size-a,c=o<n.byteLength?n.subarray(0,o):n;process.stdout.write(c),s.bytesWritten+=c.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){u.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){u.delete(t)}}}var b=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=a=>{r&&clearTimeout(r),i(a)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var Z={pieceSize:84*1024,maxConcurrentPieces:8,pieceTimeout:3e4,idleTimeout:6e4,maxPieceRetries:3,maxRetryQueueSize:1e4,maxInFlightMultiplier:10,highWaterMark:16*1024*1024,lowWaterMark:4*1024*1024,maxPiecesPerSecond:5e3,rateLimitWindowMs:1e3,channelRotationThreshold:512*1024*1024,channelRotationEnabled:!1,parallelChannels:1,parallelChannelsEnabled:!1,parallelChannelsThreshold:100*1024*1024,pacingEnabled:!0,pacingThreshold:0,pacingIntervalMs:10,targetBufferedAmount:8*1024*1024,minBufferedAmount:4*1024*1024,maxBufferedAmount:16*1024*1024,maxPiecesPerTick:32,burstPiecesPerTick:64,maxPendingReads:512,maxInFlightPieces:4e3},A=class extends b{constructor(t={}){super(),this.config={...Z,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let a=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=a,this._checkProgressMilestones(t,i,a),this._emitProgress(t,i,a),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let a=i.inFlightPieces.get(n);if(!a)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let o=a.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=o,r+=o;let c=a.channelIndex>=0?a.channelIndex:0;i.channelBytes[c]!==void 0&&(i.channelBytes[c]+=o)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let o of i)if(typeof o!="number"||o<0||s.manifest&&o>=s.manifest.totalPieces)return}let r=Date.now(),n=0,a=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let o=[];for(let[c,h]of s.inFlightPieces)c<=e&&o.push([c,h]);for(let[c,h]of o){s.inFlightPieces.delete(c);let l=h.pieceSize||this._getPieceSize(s.manifest,c);s.bytesTransferred+=l,n+=l,a++,s.pieceTimeouts.delete(c)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let o of i){let c=s.inFlightPieces.get(o);if(c){s.inFlightPieces.delete(o);let h=c.pieceSize||this._getPieceSize(s.manifest,o);s.bytesTransferred+=h,n+=h,a++,s.piecesAckedCount++,s.pieceTimeouts.delete(o)}}if(a>0){s.lastActivityTime=r;let o=s.piecesAckedCount/s.manifest.totalPieces*100;s.lastProgress=o,this._checkProgressMilestones(t,s,o),this._emitProgress(t,s,o),s.piecesAckedCount===s.manifest.totalPieces&&this._completeSend(t)}}requeuePiece(t,e){let i=this.transfers.get(t);!i||i.type!=="send"||i.state==="completed"||i.state==="completing"||(i.inFlightPieces.delete(e),i.retryQueue.includes(e)||i.retryQueue.push(e),!i.paused&&!i.usePacing&&this._sendNextPieces(t))}markSendComplete(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.state==="completed"||e.state==="completing"||this._completeSend(t)}_checkChannelRotation(t,e,i){if(!this.config.channelRotationEnabled||e.pendingRotations.has(i))return;let s=e.channelBytes[i]||0,r=e.channelThresholds?.[i]||this.config.channelRotationThreshold;s>=r&&(e.pendingRotations.add(i),this.emit("channel-rotation-needed",{transferId:t,channelIndex:i,bytesTransferred:e.bytesTransferred,channelBytes:s,threshold:r,rotationCount:e.channelRotationCounts[i]||0}))}rotateSingleChannel(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||e>=s.channels.length)return;let r=s.channels[e],n=(s.channelRotationCounts[e]||0)+1;s.channels[e]=i,e===0&&(s.channel=i),s.channelBytes[e]=0,s.channelRotationCounts[e]=n,s.pendingRotations.delete(e),i.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!s.paused&&!s.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,i){let s=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(s,50)}};s(),setTimeout(()=>{if(t.readyState==="open")try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}},5e3)}rotateChannel(t,e){this.rotateChannels(t,[e])}initReceive(t,e,i={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s={id:t,type:"receive",manifest:e,state:"initialized",nextPieceToRequest:0,piecesReceivedCount:0,receivedWaterLevel:-1,receivedOutOfOrder:new Set,maxOutOfOrderSize:1e4,piecesRequested:new Set,bytesTransferred:0,streamingHandler:i.streamingHandler||null,backpressureCheckerId:null,fileBuffer:null,pieceBuffers:new Map,paused:!1,senderPaused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return s.streamingHandler||this._initReceiveBuffer(s,e),this.transfers.set(t,s),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(s)}}startReceive(t,e){let i=this.transfers.get(t);if(!i||i.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);i.state="transferring",i.startTime=i.startTime||Date.now(),i.requestPiece=e,this._startIdleTimeout(t),this._startBackpressureChecker(t),this._requestNextPieces(t)}_startBackpressureChecker(t){let e=this.transfers.get(t);!e||e.backpressureCheckerId||(e.backpressureCheckerId=setInterval(()=>{let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="cancelled"){this._stopBackpressureChecker(t);return}if(i.streamingHandler?.getBackpressureInfo){let s=i.streamingHandler.getBackpressureInfo();!s.isUnderPressure&&i.senderPaused&&(i.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:s.overflowSize}))}},200))}_stopBackpressureChecker(t){let e=this.transfers.get(t);!e||!e.backpressureCheckerId||(clearInterval(e.backpressureCheckerId),e.backpressureCheckerId=null)}async handlePieceData(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="receive")return{success:!1,error:"Transfer not found"};if(s.state==="completing"||s.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:s.lastProgress||0};let r=i.byteLength;if(s.piecesReceivedCount<100){if(e<0||e>=s.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(i instanceof ArrayBuffer||i instanceof Uint8Array||ArrayBuffer.isView(i)))return{success:!1,error:"Invalid piece data type"};if(r===0&&!(e===0&&s.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(s.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"}}if(s.streamingHandler?.getBackpressureInfo){let n=s.streamingHandler.getBackpressureInfo();n.isUnderPressure&&!s.senderPaused?(s.senderPaused=!0,this.emit("send-backpressure",{transferId:t,pause:!0,bufferSize:n.overflowSize})):!n.isUnderPressure&&s.senderPaused&&(s.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:n.overflowSize}))}try{if(e===s.receivedWaterLevel+1){for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;s.lastActivityTime=Date.now()}else if(e>s.receivedWaterLevel+1){if(s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}if(s.streamingHandler?s.streamingHandler.writeChunk(t,e,i).then(a=>{a&&!a.success&&!a.duplicate&&(a.error!=="OVERFLOW_TIMEOUT"&&console.warn("[TransferEngine] Write issue for piece",e,":",a.error),a.error==="SW_DOWNLOAD_DEAD"&&a.retryable===!1&&(console.error("[TransferEngine] Fatal write error - aborting transfer:",t),this.emit("transfer-failed",{transferId:t,error:"Download stream cancelled. Please try again.",fatal:!0}),this.cancel(t)))}).catch(a=>{console.error("[TransferEngine] Write error for piece",e,":",a.message)}):this._storePieceInMemory(s,e,i),s.receivedOutOfOrder.size>=100&&s.requestPiece){let a=Date.now(),o=s.lastGapCheckTime||0;if(a-o>5e3){s.lastGapCheckTime=a;let c=s.receivedWaterLevel+1,h=Math.min(c+10,e);for(let l=c;l<h;l++)!s.receivedOutOfOrder.has(l)&&!s.piecesRequested.has(l)&&(s.piecesRequested.add(l),s.requestPiece(l))}}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.pieceTimeouts.delete(e),s.pieceRetries.delete(e),s.bytesTransferred+=r;let n=s.piecesReceivedCount/s.manifest.totalPieces*100;return s.lastProgress=n,this._checkProgressMilestones(t,s,n),this._emitProgress(t,s,n),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(!s.paused&&s.piecesReceivedCount%10===0&&this._requestNextPieces(t),{success:!0})}catch(n){return{success:!1,error:n.message}}}async completeReceive(t){let e=this.transfers.get(t);if(!e||e.type!=="receive")return{success:!1,error:"Transfer not found"};if(e.piecesReceivedCount!==e.manifest.totalPieces)return{success:!1,error:`Incomplete: ${e.piecesReceivedCount}/${e.manifest.totalPieces}`};e.state="completing",this._stopIdleTimeout(t),this._stopBackpressureChecker(t);try{if(e.streamingHandler)return await e.streamingHandler.completeStream(t),e.state="completed",{success:!0,streaming:!0,manifest:e.manifest};{let i=this._assembleFile(e);return e.state="completed",{success:!0,blob:i,manifest:e.manifest,fileName:e.manifest.fileName}}}catch(i){return{success:!1,error:i.message}}}pause(t){let e=this.transfers.get(t);e&&(e.paused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t),this.emit("paused",{transferId:t}))}resume(t){let e=this.transfers.get(t);!e||!e.paused||(e.paused=!1,this._startIdleTimeout(t),e.type==="send"?e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t):this._requestNextPieces(t),this.emit("resumed",{transferId:t}))}pauseForBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused||(e.backpressurePaused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t))}resumeFromBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused&&(e.backpressurePaused=!1,!e.paused&&(this._startIdleTimeout(t),e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t)))}cancel(t){let e=this.transfers.get(t);e&&(this._cleanupTransfer(t),this.emit("cancelled",{transferId:t,type:e.type}))}cleanup(t){this._cleanupTransfer(t)}getStats(t){let e=this.transfers.get(t);if(!e)return null;let i=e.startTime?Date.now()-e.startTime:0,s=i>0?e.bytesTransferred/i*1e3:0;return{transferId:t,type:e.type,state:e.state,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),piecesCompleted:e.type==="send"?e.piecesAckedCount:e.piecesReceivedCount,totalPieces:e.manifest.totalPieces,speed:s,elapsed:i,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.retryQueue.length>=this.config.maxRetryQueueSize){this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=i)return;let s=e.channel,r=e.channels.length===1;if(r){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return}else{let l=!1;for(let p of e.channels)if(p.readyState==="open"&&p.bufferedAmount<=this.config.highWaterMark){l=!0;break}if(!l)return}let a=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(a<=0)return;let o=[];for(;e.retryQueue.length>0&&o.length<a;){let l=e.retryQueue.shift();e.inFlightPieces.has(l)||o.push(l)}for(;e.nextPieceToSend<e.manifest.totalPieces&&o.length<a;){let l=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(l)||o.push(l)}if(o.length===0)return;let c=Date.now();for(let l of o)e.inFlightPieces.set(l,{sendTime:c,pieceSize:this._getPieceSize(e.manifest,l)});let h=r?4:4*e.channels.length;try{for(let l=0;l<o.length;l+=h){let g=o.slice(l,l+h).map(async m=>{let f=await this._readPiece(e.file,e.manifest,m);return{pieceIndex:m,data:f}}),d=await Promise.all(g);if(r)for(let{pieceIndex:m,data:f}of d){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,m,f)),this._startPieceTimeout(t,m)}else for(let{pieceIndex:m,data:f}of d){let C=null,_=-1;for(let P=0;P<e.channels.length;P++){let y=(e.channelIndex+P)%e.channels.length,w=e.channels[y];if(w.readyState==="open"&&w.bufferedAmount<=this.config.highWaterMark){C=w,_=y,e.channelIndex=(y+1)%e.channels.length;break}}if(!C)return;C.send(this._encodePieceMessage(t,m,f)),e.inFlightPieces.set(m,{sendTime:Date.now(),channelIndex:_,pieceSize:this._getPieceSize(e.manifest,m)}),this._startPieceTimeout(t,m)}}}catch(l){for(let p of o)e.inFlightPieces.delete(p),e.retryQueue.push(p);this.emit("error",{transferId:t,error:`Failed to send pieces: ${l.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,a=this.config.maxInFlightPieces*.99,o=this.config.maxPendingReads*.95,c=r<2*1024*1024,h=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=a||i>=n&&c?"RECEIVER_SLOW":e.pendingReads>=o?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:h})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let l=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?l=32:r>this.config.minBufferedAmount?l=this.config.maxPiecesPerTick:l=this.config.burstPiecesPerTick;let p=this.config.maxPendingReads-e.pendingReads;if(p<=0)return;l=Math.min(l,p);let g=this.config.maxInFlightPieces-i;l=Math.min(l,g);let d=[];for(;e.retryQueue.length>0&&d.length<l;){let f=e.retryQueue.shift();e.inFlightPieces.has(f)||d.push(f)}for(;e.nextPieceToSend<e.manifest.totalPieces&&d.length<l;){let f=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(f)||d.push(f)}if(d.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=d.length;let m=Date.now();for(let f of d)e.inFlightPieces.set(f,{sendTime:m,pieceSize:this._getPieceSize(e.manifest,f)});this._readAndSendPieces(t,e,s,d)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(a=>this._readPiece(e.file,e.manifest,a).then(o=>({pieceIndex:a,data:o})).catch(o=>({pieceIndex:a,error:o}))),n=await Promise.all(r);for(let a of n){if(a.error){e.inFlightPieces.delete(a.pieceIndex),e.retryQueue.push(a.pieceIndex);continue}let{pieceIndex:o,data:c}=a,h=e.channel;if(!h||h.readyState!=="open"){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}if(h.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}try{h.send(this._encodePieceMessage(t,o,c)),this._startPieceTimeout(t,o)}catch{e.inFlightPieces.delete(o),e.retryQueue.push(o)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let o=0;o<e.totalPieces;o++){let c=s.get(o);if(!c)throw new Error(`Missing piece ${o}`);n.push(c)}let a=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),a}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),a=new Uint8Array(4+n.byteLength);return a.set(new Uint8Array(n),4),a}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,a=e.lastSpeedTime||e.startTime||s,o=s-a;if(o>=200){let p=e.bytesTransferred-n;r=o>0?p/o*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=c*r+(1-c)*e.smoothedSpeed);let h=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let l=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:l,speed:h})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){if(!t||!(t instanceof ArrayBuffer)||t.byteLength<4)return{pieceIndex:-1,data:null,error:"invalid_message"};try{let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}catch{return{pieceIndex:-1,data:null,error:"decode_failed"}}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let a=s-e.lastMilestoneCheck.time,o=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(a>0&&a<r&&o>0)n=o/a*1e3/1e6;else if(a>=r){let c=e.startTime?s-e.startTime:0;n=c>0?e.bytesTransferred/c*1e3/1e6:0}}else{let a=e.startTime?s-e.startTime:0;n=a>0?e.bytesTransferred/a*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,o=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,c=n<r&&!o,h=[];for(let[g,d]of e.pieceTimeouts)if(i-d>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(g<=e.receivedWaterLevel||e.receivedOutOfOrder.has(g))){e.pieceTimeouts.delete(g);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&g<=e.ackedWaterLevel){e.pieceTimeouts.delete(g);continue}h.push(g)}if(h.length===0||c)return;let p=h.slice(0,50);for(let g of p)this._handlePieceTimeout(t,g)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),a=Date.now()-n;a>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:a}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var ee="https://perkoon.com",L=class extends Y{constructor(t=ee,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new A,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await B(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=await this._createSession(e.name,e.password);this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new v(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r}),this.transport=new k,this.transport.initialize(i.peer_id,i.ice_servers);let n=await this.transport.createOffer();this.signaling.sendOffer(r,n),this.transport.on("ice-candidate",({candidate:p,mid:g})=>{this.signaling.sendIceCandidate(r,{candidate:p,mid:g})}),this.signaling.on("ice_candidate",p=>{this.transport.addIceCandidate(p.candidate)});let a=await this._waitForSignal("answer");this.transport.handleAnswer(a.answer);let o=await this.transport.waitForDataChannel();this.emit("connected");let c=I.randomUUID();this.engine.initSend(c,this.fileSource,o);let h=this.engine.transfers.get(c).manifest;this._setupControlHandler(o,c),o.send(JSON.stringify({type:"manifest",transfer_id:c,manifest:h})),this._setupProgressEvents(c),await this.engine.startSend(c);let l=await this._waitForCompletion(c);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:l.speed,duration:l.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new v(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver"),n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new k,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:d,mid:m})=>{this.signaling.sendIceCandidate(n,{candidate:d,mid:m})}),this.signaling.on("ice_candidate",d=>{this.transport.addIceCandidate(d.candidate)});let a=await this._waitForSignal("offer"),o=await this.transport.handleOffer(a.offer);this.signaling.sendAnswer(n,o);let c=await this.transport.waitForDataChannel();this.emit("connected");let h=i.stdout?x():E(e,{overwrite:i.overwrite}),l=[],g=await new Promise((d,m)=>{let f=null;c.addEventListener("message",async C=>{let _=C.data;if(typeof _=="string"){try{let P=JSON.parse(_);if(P.type==="manifest"){f=P.transfer_id;let y=P.manifest;this.emit("receiving-file",{name:y.fileName,size:y.fileSize}),await h.startStreamingReceive(f,{fileName:y.fileName,size:y.fileSize,fileSize:y.fileSize,type:y.fileType,pieceSize:y.pieceSize},{}),this.engine.initReceive(f,y,{streamingHandler:h}),this._setupProgressEvents(f),this.engine.startReceive(f,T=>{this._safeSend(c,JSON.stringify({type:"request",transfer_id:f,piece_index:T}))});let w=-1;this._ackInterval=setInterval(()=>{let T=this.engine.transfers.get(f);if(!T||T.state==="completed"||T.state==="completing"){clearInterval(this._ackInterval);return}let S=T.receivedWaterLevel;if(S>w){w=S;let R=T.receivedOutOfOrder.size>0?Array.from(T.receivedOutOfOrder):void 0;this._safeSend(c,JSON.stringify({type:"water_level_ack",transfer_id:f,water_level:S,out_of_order:R}))}},100)}}catch(P){P.message&&!P.message.includes("Unexpected token")&&m(P)}return}if(f&&(_ instanceof ArrayBuffer||Buffer.isBuffer(_))){let P=_ instanceof ArrayBuffer?_:_.buffer.slice(_.byteOffset,_.byteOffset+_.byteLength),{pieceIndex:y,data:w}=this.engine.decodePieceMessage(P);if(y>=0&&w&&(await this.engine.handlePieceData(f,y,w))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let S=this.engine.transfers.get(f);S&&this._safeSend(c,JSON.stringify({type:"water_level_ack",transfer_id:f,water_level:S.receivedWaterLevel}));let R=h.getFilePath(f);l.push(R),await this.engine.completeReceive(f),this._safeSend(c,JSON.stringify({type:"complete",transfer_id:f}));let z=this.engine.transfers.get(f),F=Date.now()-(z?.startTime||Date.now()),M=z?.manifest;d({files:l,speed:M?M.fileSize/(F/1e3):0,duration:F})}}}),setTimeout(()=>{m(new Error("Transfer timed out (10 minutes)"))},6e5)});return this.signaling.disconnect(),this.transport.close(),g}async _createSession(t,e){let i=`${this.serverUrl}/api/v1/sessions`,s={};t&&(s.name=t),e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${n.error||r.statusText}`)}return r.json()}async _joinSession(t,e){let i=`${this.serverUrl}/api/v1/sessions/${t}/join`,s={role:"receiver"};e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw r.status===404?new Error(`Session ${t} not found. Check the code and try again.`):r.status===410?new Error(`Session ${t} has expired.`):r.status===403?new Error(n.error||"Access denied"):new Error(`Failed to join session: ${n.error||r.statusText}`)}return r.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),s=r=>{(r.type==="ready"||r.type==="offer")&&(clearTimeout(i),this.signaling.removeListener("ready",s),this.signaling.removeListener("offer",s),t(r.from))};this.signaling.on("ready",s),this.signaling.on("offer",s)})}_waitForSignal(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error(`Timed out waiting for ${t} signal`))},3e4);this.signaling.once(t,r=>{clearTimeout(s),e(r)})})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let a=r>0?Math.round(n/r*100):0,o=Date.now()-(i.startTime||Date.now()),c=n*s.pieceSize,h=o>0?c/(o/1e3):0,l=h>0?Math.round((s.fileSize-c)/h):0;this.emit("progress",{transferId:t,percent:Math.min(a,100),speed:h,eta:l,bytesTransferred:c,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"request":this.engine.requeuePiece(e,s.piece_index);break;case"complete":{let r=this.engine.transfers.get(e);r&&r.state!=="completed"&&(r.piecesAckedCount=r.manifest.totalPieces,r.bytesTransferred=r.manifest.fileSize,r.inFlightPieces.clear(),r.state="completed")}this.emit("transfer-complete",{transferId:e});break;case"backpressure":s.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Transfer timed out (10 minutes)"))},6e5),r=()=>{let a=this.engine.transfers.get(t);if(!a){clearTimeout(s),i(new Error("Transfer context lost"));return}if(a.state==="completed"){clearTimeout(s);let o=Date.now()-a.startTime;e({speed:a.manifest.fileSize/(o/1e3),duration:o});return}if(a.state==="cancelled"||a.state==="failed"){clearTimeout(s),i(new Error("Transfer failed"));return}};this.on("transfer-complete",({transferId:a})=>{a===t&&r()});let n=setInterval(()=>{r();let a=this.engine.transfers.get(t);(!a||a.state==="completed"||a.state==="cancelled")&&clearInterval(n)},500)})}_safeSend(t,e){try{t.readyState==="open"&&t.send(e)}catch{}}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};export{k as NodeTransport,v as SignalingClient,L as TransferManager,E as createNodeFileSink,B as createNodeFileSource};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perkoon",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "P2P file transfer CLI — send files directly between devices, no cloud required",
5
5
  "type": "module",
6
6
  "main": "dist/client.js",