perkoon 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -29,6 +29,8 @@ $ perkoon send report.pdf
29
29
 
30
30
  Waiting for receiver...
31
31
  ✓ Receiver connected
32
+ Waiting for receiver to accept...
33
+ ✓ Receiver accepted
32
34
  ✓ Direct connection established
33
35
  ✓ Complete: 2.5 MB in 0.3s (8.3 MB/s)
34
36
  ```
@@ -44,7 +46,7 @@ $ perkoon receive K7MX4QPR9W2N
44
46
  ✓ Complete: 0.3s (8.3 MB/s)
45
47
  ```
46
48
 
47
- **Or receive in the browser** — open the link shown by the sender. No install needed on the receiving end.
49
+ **Or receive in the browser** — open the link shown by the sender. No install needed on the receiving end. The browser user will see a file acceptance dialog, click Accept, and the file transfers directly via WebRTC.
48
50
 
49
51
  ## Options
50
52
 
@@ -69,6 +71,8 @@ $ perkoon send file.zip --json --quiet
69
71
  {"event":"session_created","session_code":"K7MX4QPR9W2N","share_url":"https://perkoon.com/K7MX4QPR9W2N"}
70
72
  {"event":"waiting_for_receiver"}
71
73
  {"event":"receiver_connected"}
74
+ {"event":"waiting_for_acceptance"}
75
+ {"event":"transfer_accepted"}
72
76
  {"event":"webrtc_connected"}
73
77
  {"event":"progress","percent":50,"speed":5242880,"eta":1,"bytes_transferred":524288}
74
78
  {"event":"transfer_complete","session_code":"K7MX4QPR9W2N","duration_ms":2000,"speed":5242880}
@@ -89,10 +93,17 @@ $ perkoon send file.zip --json --quiet
89
93
 
90
94
  Files transfer directly between devices using WebRTC data channels. The perkoon.com server handles signaling only — your data never touches our servers.
91
95
 
96
+ **CLI → CLI:**
92
97
  1. Sender creates a session and gets a 12-character code
93
- 2. Receiver joins with that code (CLI or browser)
94
- 3. WebRTC peer connection is established
95
- 4. File data flows directly between the two devices
98
+ 2. Receiver joins with that code
99
+ 3. WebRTC peer connection is established directly
100
+ 4. File data flows between the two devices
101
+
102
+ **CLI → Browser:**
103
+ 1. Sender creates a session and gets a share URL
104
+ 2. Receiver opens the URL in any browser — no install needed
105
+ 3. Browser shows the file and an Accept button
106
+ 4. On accept, WebRTC connection is established and file transfers directly
96
107
 
97
108
  ## Requirements
98
109
 
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import{EventEmitter as Ce}from"node:events";import j from"node:crypto";import ue from"node-datachannel";import{EventEmitter as de}from"node:events";var{PeerConnection:pe}=ue,z=class extends de{constructor(){super(),this.pc=null,this.channels=new Map,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 pe(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=>{let r=this._wrapChannel(s),n=s.getLabel();this.channels.set(n,r),this.emit("data-channel",r,n)})}async createOffer(t){return new Promise(e=>{this.pc.onLocalDescription((n,o)=>{e({sdp:n,type:o})});let i=this.pc.createDataChannel("control",{ordered:!0});this.channels.set("control",this._wrapChannel(i));let s=`transfer-${t}`,r=this.pc.createDataChannel(s,{ordered:!0,maxRetransmits:3});this.channels.set(s,this._wrapChannel(r))})}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=[]}getChannel(t){return this.channels.get(t)||null}async waitForChannel(t,e=3e4){let i=this.channels.get(t);return i?.readyState==="open"?i:new Promise((s,r)=>{let n=setTimeout(()=>{r(new Error(`Data channel "${t}" open timeout`))},e),o=a=>{a.readyState==="open"?(clearTimeout(n),s(a)):a.addEventListener("open",()=>{clearTimeout(n),s(a)})};if(i)o(i);else{let a=(l,f)=>{f===t&&(this.removeListener("data-channel",a),o(l))};this.on("data-channel",a)}})}async waitForChannelByPrefix(t,e=3e4){for(let[i,s]of this.channels)if(i.startsWith(t))return s.readyState==="open"?{channel:s,label:i}:new Promise((r,n)=>{let o=setTimeout(()=>n(new Error(`Channel "${i}" open timeout`)),e);s.addEventListener("open",()=>{clearTimeout(o),r({channel:s,label:i})})});return new Promise((i,s)=>{let r=setTimeout(()=>{this.removeListener("data-channel",n),s(new Error(`No channel with prefix "${t}" within ${e}ms`))},e),n=(o,a)=>{a.startsWith(t)&&(this.removeListener("data-channel",n),o.readyState==="open"?(clearTimeout(r),i({channel:o,label:a})):o.addEventListener("open",()=>{clearTimeout(r),i({channel:o,label:a})}))};this.on("data-channel",n)})}close(){for(let[,t]of this.channels)try{t.close()}catch{}if(this.channels.clear(),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 me}from"phoenix";import ge from"ws";import{EventEmitter as _e}from"node:events";var M=class extends _e{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 me(r,{params:{},transport:ge,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})}sendFilesAdded(t,e,i){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("files_added",{batch_id:t,files:e,total_size:i})}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 ye}from"node:fs/promises";import we from"node:path";import{lookup as Pe}from"mime-types";async function Z(c){let t=await ye(c,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${c}`);return{name:we.basename(c),size:e.size,type:Pe(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 Se,mkdir as Te,access as Y}from"node:fs/promises";import ee from"node:path";function te(c,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await Te(c,{recursive:!0});let n=ke(s.fileName),o=ee.join(c,n);t.overwrite||(o=await ve(o));let a=await Se(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 ve(c){try{await Y(c)}catch{return c}let t=ee.extname(c),e=c.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await Y(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function ke(c){return c.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function se(){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 W=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 be={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},U=class extends W{constructor(t={}){super(),this.config={...be,...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,f]of s.inFlightPieces)l<=e&&a.push([l,f]);for(let[l,f]of a){s.inFlightPieces.delete(l);let h=f.pieceSize||this._getPieceSize(s.manifest,l);s.bytesTransferred+=h,n+=h,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 f=l.pieceSize||this._getPieceSize(s.manifest,a);s.bytesTransferred+=f,n+=f,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,f=Math.min(l+10,e);for(let h=l;h<f;h++)!s.receivedOutOfOrder.has(h)&&!s.piecesRequested.has(h)&&(s.piecesRequested.add(h),s.requestPiece(h))}}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 h=!1;for(let m of e.channels)if(m.readyState==="open"&&m.bufferedAmount<=this.config.highWaterMark){h=!0;break}if(!h)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 h=e.retryQueue.shift();e.inFlightPieces.has(h)||a.push(h)}for(;e.nextPieceToSend<e.manifest.totalPieces&&a.length<o;){let h=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(h)||a.push(h)}if(a.length===0)return;let l=Date.now();for(let h of a)e.inFlightPieces.set(h,{sendTime:l,pieceSize:this._getPieceSize(e.manifest,h)});let f=r?4:4*e.channels.length;try{for(let h=0;h<a.length;h+=f){let y=a.slice(h,h+f).map(async w=>{let d=await this._readPiece(e.file,e.manifest,w);return{pieceIndex:w,data:d}}),S=await Promise.all(y);if(r)for(let{pieceIndex:w,data:d}of S){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,w,d)),this._startPieceTimeout(t,w)}else for(let{pieceIndex:w,data:d}of S){let _=null,u=-1;for(let A=0;A<e.channels.length;A++){let g=(e.channelIndex+A)%e.channels.length,v=e.channels[g];if(v.readyState==="open"&&v.bufferedAmount<=this.config.highWaterMark){_=v,u=g,e.channelIndex=(g+1)%e.channels.length;break}}if(!_)return;_.send(this._encodePieceMessage(t,w,d)),e.inFlightPieces.set(w,{sendTime:Date.now(),channelIndex:u,pieceSize:this._getPieceSize(e.manifest,w)}),this._startPieceTimeout(t,w)}}}catch(h){for(let m of a)e.inFlightPieces.delete(m),e.retryQueue.push(m);this.emit("error",{transferId:t,error:`Failed to send pieces: ${h.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,f=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:f})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let h=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?h=32:r>this.config.minBufferedAmount?h=this.config.maxPiecesPerTick:h=this.config.burstPiecesPerTick;let m=this.config.maxPendingReads-e.pendingReads;if(m<=0)return;h=Math.min(h,m);let y=this.config.maxInFlightPieces-i;h=Math.min(h,y);let S=[];for(;e.retryQueue.length>0&&S.length<h;){let d=e.retryQueue.shift();e.inFlightPieces.has(d)||S.push(d)}for(;e.nextPieceToSend<e.manifest.totalPieces&&S.length<h;){let d=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(d)||S.push(d)}if(S.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=S.length;let w=Date.now();for(let d of S)e.inFlightPieces.set(d,{sendTime:w,pieceSize:this._getPieceSize(e.manifest,d)});this._readAndSendPieces(t,e,s,S)}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,f=e.channel;if(!f||f.readyState!=="open"){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}if(f.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}try{f.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 f=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let h=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:h,speed:f})}_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,f=[];for(let[y,S]of e.pieceTimeouts)if(i-S>=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}f.push(y)}if(f.length===0||l)return;let m=f.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 Ae="https://perkoon.com",$=class extends Ce{constructor(t=Ae,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new U,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await Z(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.sessionCode=i.session_code,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});let n=`batch_${Date.now()}_${j.randomUUID().replace(/-/g,"").slice(0,9)}`,o=[{id:`file_${Date.now()}_${j.randomUUID().replace(/-/g,"").slice(0,9)}`,name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type}];this.signaling.sendFilesAdded(n,o,this.fileSource.size),this.emit("waiting-for-acceptance");let a=await this._waitForSignal("transfer_accepted",this.peerTimeout,_=>!_.batch_id||_.batch_id===n);this.emit("transfer-accepted",{batchId:a.batch_id});let l=`transfer_${Date.now()}_${j.randomUUID().replace(/-/g,"").slice(0,9)}`;this.transport=new z,this.transport.initialize(i.peer_id,i.ice_servers);let f=await this.transport.createOffer(l);this.signaling.sendOffer(r,f),this.transport.on("ice-candidate",({candidate:_,mid:u})=>{this.signaling.sendIceCandidate(r,{candidate:_,sdpMid:u||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",_=>{let u=_.candidate;this.transport.addIceCandidate({candidate:u.candidate||u,mid:u.sdpMid||u.mid||"0"})});let h=await this._waitForSignal("answer");this.transport.handleAnswer(h.answer);let m=await this.transport.waitForChannel("control"),y=await this.transport.waitForChannel(`transfer-${l}`);this.emit("connected"),this.engine.initSend(l,this.fileSource,y);let S=this.engine.transfers.get(l).manifest;this._setupControlHandler(m,l);let w=JSON.stringify({type:"manifest",transfer_id:l,manifest:S,batch_id:n,batch_total_files:1,batch_total_size:this.fileSource.size});m.send(w),this._setupProgressEvents(l),await this.engine.startSend(l);let d=await this._waitForCompletion(l);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:d.speed,duration:d.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:d,mid:_})=>{this.signaling.sendIceCandidate(n,{candidate:d,sdpMid:_||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",d=>{let _=d.candidate;this.transport.addIceCandidate({candidate:_.candidate||_,mid:_.sdpMid||_.mid||"0"})});let o=await this._waitForSignal("offer"),a=await this.transport.handleOffer(o.offer);this.signaling.sendAnswer(n,a);let l=await this.transport.waitForChannel("control"),{channel:f,label:h}=await this.transport.waitForChannelByPrefix("transfer-");this.emit("connected");let m=i.stdout?se():te(e,{overwrite:i.overwrite}),y=[],w=await new Promise((d,_)=>{let u=null;l.addEventListener("message",async A=>{if(typeof A.data=="string")try{let g=JSON.parse(A.data);if(g.type==="manifest"&&!u){u=g.transfer_id;let v=g.manifest;this.emit("receiving-file",{name:v.fileName,size:v.fileSize}),await m.startStreamingReceive(u,{fileName:v.fileName,size:v.fileSize,fileSize:v.fileSize,type:v.fileType,pieceSize:v.pieceSize},{}),this.engine.initReceive(u,v,{streamingHandler:m}),this._setupProgressEvents(u),this.engine.startReceive(u,C=>{this._safeSend(l,JSON.stringify({type:"request",transfer_id:u,piece_index:C}))});let F=-1;this._ackInterval=setInterval(()=>{let C=this.engine.transfers.get(u);if(!C||C.state==="completed"||C.state==="completing"){clearInterval(this._ackInterval);return}let N=C.receivedWaterLevel;if(N>F){F=N;let D=C.receivedOutOfOrder.size>0?Array.from(C.receivedOutOfOrder):void 0;this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:u,wl:N,ooo:D}))}},100)}}catch(g){g.message&&!g.message.includes("Unexpected token")&&_(g)}}),f.addEventListener("message",async A=>{let g=A.data;if(!u||!(g instanceof ArrayBuffer||Buffer.isBuffer(g)))return;let v=g instanceof ArrayBuffer?g:g.buffer.slice(g.byteOffset,g.byteOffset+g.byteLength),{pieceIndex:F,data:C}=this.engine.decodePieceMessage(v);if(F>=0&&C&&(await this.engine.handlePieceData(u,F,C))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let D=this.engine.transfers.get(u);D&&this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:u,wl:D.receivedWaterLevel}));let fe=m.getFilePath(u);y.push(fe),await this.engine.completeReceive(u),this._safeSend(l,JSON.stringify({type:"complete",transfer_id:u}));let X=this.engine.transfers.get(u),K=Date.now()-(X?.startTime||Date.now()),V=X?.manifest;d({files:y,speed:V?V.fileSize/(K/1e3):0,duration:K})}})});return this.signaling.disconnect(),this.transport.close(),w}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=()=>{clearTimeout(i),this.signaling.removeListener("ready",r),this.signaling.removeListener("offer",r),this.signaling.removeListener("peer_joined",r)},r=n=>{(n.type==="ready"||n.type==="offer"||n.type==="peer_joined"&&(!n.role||n.role==="receiver"))&&(s(),t(n.from))};this.signaling.on("ready",r),this.signaling.on("offer",r),this.signaling.on("peer_joined",r)})}_waitForSignal(t,e=3e4,i=null){return new Promise((s,r)=>{let n=setTimeout(()=>{this.signaling.removeListener(t,o),r(new Error(`Timed out waiting for ${t} signal`))},e),o=a=>{i&&!i(a)||(this.signaling.removeListener(t,o),clearTimeout(n),s(a))};this.signaling.on(t,o)})}_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,f=a>0?l/(a/1e3):0,h=f>0?Math.round((s.fileSize-l)/f):0;this.emit("progress",{transferId:t,percent:Math.min(o,100),speed:f,eta:h,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"ack_wl":this.engine.handleWaterLevelAck(e,s.wl,s.ooo);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"ack_batch":Array.isArray(s.pieces)&&s.pieces.length>0&&this.engine.handlePieceAckBatch(e,s.pieces);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=()=>{let n=this.engine.transfers.get(t);if(!n){clearInterval(r),i(new Error("Transfer context lost"));return}if(n.state==="completed"){clearInterval(r);let o=Date.now()-n.startTime;e({speed:n.manifest.fileSize/(o/1e3),duration:o});return}if(n.state==="cancelled"||n.state==="failed"){clearInterval(r),i(new Error("Transfer failed"));return}},r=setInterval(()=>{s()},500);this.on("transfer-complete",({transferId:n})=>{n===t&&s()})})}_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 Ee from"node:path";import{access as Re,stat as Le}from"node:fs/promises";var ie="0.2.0",oe="https://perkoon.com",ae=0,L=1,ne=2,Q=3,Be=4,Fe=5,E=process.argv.slice(2),re=E[0],T={};for(let c=1;c<E.length;c++)if(E[c].startsWith("--")){let t=E[c].slice(2);E[c+1]&&!E[c+1].startsWith("--")?T[t]=E[++c]:T[t]=!0}else T._positional||(T._positional=E[c]);var R=typeof T.server=="string"?T.server:oe,B=T.json===!0,O=T.quiet===!0,ze=T.overwrite===!0,G=T.output||"./received",x=typeof T.password=="string"?T.password:void 0,I=typeof T.timeout=="string"?parseInt(T.timeout,10):NaN,J=!isNaN(I)&&I>0?I:300;B&&G==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
2
+ import{EventEmitter as Ce}from"node:events";import j from"node:crypto";import ue from"node-datachannel";import{EventEmitter as de}from"node:events";var{PeerConnection:pe}=ue,z=class extends de{constructor(){super(),this.pc=null,this.channels=new Map,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 pe(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=>{let r=this._wrapChannel(s),n=s.getLabel();this.channels.set(n,r),this.emit("data-channel",r,n)})}async createOffer(t){return new Promise(e=>{this.pc.onLocalDescription((n,o)=>{e({sdp:n,type:o})});let i=this.pc.createDataChannel("control",{ordered:!0});this.channels.set("control",this._wrapChannel(i));let s=`transfer-${t}`,r=this.pc.createDataChannel(s,{ordered:!0,maxRetransmits:3});this.channels.set(s,this._wrapChannel(r))})}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=[]}getChannel(t){return this.channels.get(t)||null}async waitForChannel(t,e=3e4){let i=this.channels.get(t);return i?.readyState==="open"?i:new Promise((s,r)=>{let n=setTimeout(()=>{r(new Error(`Data channel "${t}" open timeout`))},e),o=a=>{a.readyState==="open"?(clearTimeout(n),s(a)):a.addEventListener("open",()=>{clearTimeout(n),s(a)})};if(i)o(i);else{let a=(l,f)=>{f===t&&(this.removeListener("data-channel",a),o(l))};this.on("data-channel",a)}})}async waitForChannelByPrefix(t,e=3e4){for(let[i,s]of this.channels)if(i.startsWith(t))return s.readyState==="open"?{channel:s,label:i}:new Promise((r,n)=>{let o=setTimeout(()=>n(new Error(`Channel "${i}" open timeout`)),e);s.addEventListener("open",()=>{clearTimeout(o),r({channel:s,label:i})})});return new Promise((i,s)=>{let r=setTimeout(()=>{this.removeListener("data-channel",n),s(new Error(`No channel with prefix "${t}" within ${e}ms`))},e),n=(o,a)=>{a.startsWith(t)&&(this.removeListener("data-channel",n),o.readyState==="open"?(clearTimeout(r),i({channel:o,label:a})):o.addEventListener("open",()=>{clearTimeout(r),i({channel:o,label:a})}))};this.on("data-channel",n)})}close(){for(let[,t]of this.channels)try{t.close()}catch{}if(this.channels.clear(),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 me}from"phoenix";import ge from"ws";import{EventEmitter as _e}from"node:events";var M=class extends _e{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 me(r,{params:{},transport:ge,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})}sendFilesAdded(t,e,i){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("files_added",{batch_id:t,files:e,total_size:i})}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 ye}from"node:fs/promises";import we from"node:path";import{lookup as Pe}from"mime-types";async function Z(c){let t=await ye(c,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${c}`);return{name:we.basename(c),size:e.size,type:Pe(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 Se,mkdir as Te,access as Y}from"node:fs/promises";import ee from"node:path";function te(c,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await Te(c,{recursive:!0});let n=ke(s.fileName),o=ee.join(c,n);t.overwrite||(o=await ve(o));let a=await Se(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 ve(c){try{await Y(c)}catch{return c}let t=ee.extname(c),e=c.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await Y(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function ke(c){return c.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function se(){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 W=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 be={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},U=class extends W{constructor(t={}){super(),this.config={...be,...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,f]of s.inFlightPieces)l<=e&&a.push([l,f]);for(let[l,f]of a){s.inFlightPieces.delete(l);let h=f.pieceSize||this._getPieceSize(s.manifest,l);s.bytesTransferred+=h,n+=h,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 f=l.pieceSize||this._getPieceSize(s.manifest,a);s.bytesTransferred+=f,n+=f,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,f=Math.min(l+10,e);for(let h=l;h<f;h++)!s.receivedOutOfOrder.has(h)&&!s.piecesRequested.has(h)&&(s.piecesRequested.add(h),s.requestPiece(h))}}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 h=!1;for(let m of e.channels)if(m.readyState==="open"&&m.bufferedAmount<=this.config.highWaterMark){h=!0;break}if(!h)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 h=e.retryQueue.shift();e.inFlightPieces.has(h)||a.push(h)}for(;e.nextPieceToSend<e.manifest.totalPieces&&a.length<o;){let h=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(h)||a.push(h)}if(a.length===0)return;let l=Date.now();for(let h of a)e.inFlightPieces.set(h,{sendTime:l,pieceSize:this._getPieceSize(e.manifest,h)});let f=r?4:4*e.channels.length;try{for(let h=0;h<a.length;h+=f){let y=a.slice(h,h+f).map(async w=>{let d=await this._readPiece(e.file,e.manifest,w);return{pieceIndex:w,data:d}}),S=await Promise.all(y);if(r)for(let{pieceIndex:w,data:d}of S){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,w,d)),this._startPieceTimeout(t,w)}else for(let{pieceIndex:w,data:d}of S){let _=null,u=-1;for(let A=0;A<e.channels.length;A++){let g=(e.channelIndex+A)%e.channels.length,T=e.channels[g];if(T.readyState==="open"&&T.bufferedAmount<=this.config.highWaterMark){_=T,u=g,e.channelIndex=(g+1)%e.channels.length;break}}if(!_)return;_.send(this._encodePieceMessage(t,w,d)),e.inFlightPieces.set(w,{sendTime:Date.now(),channelIndex:u,pieceSize:this._getPieceSize(e.manifest,w)}),this._startPieceTimeout(t,w)}}}catch(h){for(let m of a)e.inFlightPieces.delete(m),e.retryQueue.push(m);this.emit("error",{transferId:t,error:`Failed to send pieces: ${h.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,f=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:f})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let h=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?h=32:r>this.config.minBufferedAmount?h=this.config.maxPiecesPerTick:h=this.config.burstPiecesPerTick;let m=this.config.maxPendingReads-e.pendingReads;if(m<=0)return;h=Math.min(h,m);let y=this.config.maxInFlightPieces-i;h=Math.min(h,y);let S=[];for(;e.retryQueue.length>0&&S.length<h;){let d=e.retryQueue.shift();e.inFlightPieces.has(d)||S.push(d)}for(;e.nextPieceToSend<e.manifest.totalPieces&&S.length<h;){let d=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(d)||S.push(d)}if(S.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=S.length;let w=Date.now();for(let d of S)e.inFlightPieces.set(d,{sendTime:w,pieceSize:this._getPieceSize(e.manifest,d)});this._readAndSendPieces(t,e,s,S)}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,f=e.channel;if(!f||f.readyState!=="open"){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}if(f.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(a),e.retryQueue.push(a);continue}try{f.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 f=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let h=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:h,speed:f})}_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,f=[];for(let[y,S]of e.pieceTimeouts)if(i-S>=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}f.push(y)}if(f.length===0||l)return;let m=f.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 Ae="https://perkoon.com",$=class extends Ce{constructor(t=Ae,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new U,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await Z(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.sessionCode=i.session_code,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});let n=`batch_${Date.now()}_${j.randomUUID().replace(/-/g,"").slice(0,9)}`,o=[{id:`file_${Date.now()}_${j.randomUUID().replace(/-/g,"").slice(0,9)}`,name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type}];this.signaling.sendFilesAdded(n,o,this.fileSource.size),this.emit("waiting-for-acceptance");let a=await this._waitForSignal("transfer_accepted",this.peerTimeout,_=>!_.batch_id||_.batch_id===n);this.emit("transfer-accepted",{batchId:a.batch_id});let l=`transfer_${Date.now()}_${j.randomUUID().replace(/-/g,"").slice(0,9)}`;this.transport=new z,this.transport.initialize(i.peer_id,i.ice_servers);let f=await this.transport.createOffer(l);this.signaling.sendOffer(r,f),this.transport.on("ice-candidate",({candidate:_,mid:u})=>{this.signaling.sendIceCandidate(r,{candidate:_,sdpMid:u||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",_=>{let u=_.candidate;this.transport.addIceCandidate({candidate:u.candidate||u,mid:u.sdpMid||u.mid||"0"})});let h=await this._waitForSignal("answer");this.transport.handleAnswer(h.answer);let m=await this.transport.waitForChannel("control"),y=await this.transport.waitForChannel(`transfer-${l}`);this.emit("connected"),this.engine.initSend(l,this.fileSource,y);let S=this.engine.transfers.get(l).manifest;this._setupControlHandler(m,l);let w=JSON.stringify({type:"manifest",transfer_id:l,manifest:S,batch_id:n,batch_total_files:1,batch_total_size:this.fileSource.size});m.send(w),this._setupProgressEvents(l),await this.engine.startSend(l);let d=await this._waitForCompletion(l);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:d.speed,duration:d.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:d,mid:_})=>{this.signaling.sendIceCandidate(n,{candidate:d,sdpMid:_||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",d=>{let _=d.candidate;this.transport.addIceCandidate({candidate:_.candidate||_,mid:_.sdpMid||_.mid||"0"})});let o=await this._waitForSignal("offer"),a=await this.transport.handleOffer(o.offer);this.signaling.sendAnswer(n,a);let l=await this.transport.waitForChannel("control"),{channel:f,label:h}=await this.transport.waitForChannelByPrefix("transfer-");this.emit("connected");let m=i.stdout?se():te(e,{overwrite:i.overwrite}),y=[],w=await new Promise((d,_)=>{let u=null;l.addEventListener("message",async A=>{if(typeof A.data=="string")try{let g=JSON.parse(A.data);if(g.type==="manifest"&&!u){u=g.transfer_id;let T=g.manifest;this.emit("receiving-file",{name:T.fileName,size:T.fileSize}),await m.startStreamingReceive(u,{fileName:T.fileName,size:T.fileSize,fileSize:T.fileSize,type:T.fileType,pieceSize:T.pieceSize},{}),this.engine.initReceive(u,T,{streamingHandler:m}),this._setupProgressEvents(u),this.engine.startReceive(u,C=>{this._safeSend(l,JSON.stringify({type:"request",transfer_id:u,piece_index:C}))});let F=-1;this._ackInterval=setInterval(()=>{let C=this.engine.transfers.get(u);if(!C||C.state==="completed"||C.state==="completing"){clearInterval(this._ackInterval);return}let N=C.receivedWaterLevel;if(N>F){F=N;let D=C.receivedOutOfOrder.size>0?Array.from(C.receivedOutOfOrder):void 0;this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:u,wl:N,ooo:D}))}},100)}}catch(g){g.message&&!g.message.includes("Unexpected token")&&_(g)}}),f.addEventListener("message",async A=>{let g=A.data;if(!u||!(g instanceof ArrayBuffer||Buffer.isBuffer(g)))return;let T=g instanceof ArrayBuffer?g:g.buffer.slice(g.byteOffset,g.byteOffset+g.byteLength),{pieceIndex:F,data:C}=this.engine.decodePieceMessage(T);if(F>=0&&C&&(await this.engine.handlePieceData(u,F,C))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let D=this.engine.transfers.get(u);D&&this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:u,wl:D.receivedWaterLevel}));let fe=m.getFilePath(u);y.push(fe),await this.engine.completeReceive(u),this._safeSend(l,JSON.stringify({type:"complete",transfer_id:u}));let X=this.engine.transfers.get(u),K=Date.now()-(X?.startTime||Date.now()),V=X?.manifest;d({files:y,speed:V?V.fileSize/(K/1e3):0,duration:K})}})});return this.signaling.disconnect(),this.transport.close(),w}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=()=>{clearTimeout(i),this.signaling.removeListener("ready",r),this.signaling.removeListener("offer",r),this.signaling.removeListener("peer_joined",r)},r=n=>{(n.type==="ready"||n.type==="offer"||n.type==="peer_joined"&&(!n.role||n.role==="receiver"))&&(s(),t(n.from))};this.signaling.on("ready",r),this.signaling.on("offer",r),this.signaling.on("peer_joined",r)})}_waitForSignal(t,e=3e4,i=null){return new Promise((s,r)=>{let n=setTimeout(()=>{this.signaling.removeListener(t,o),r(new Error(`Timed out waiting for ${t} signal`))},e),o=a=>{i&&!i(a)||(this.signaling.removeListener(t,o),clearTimeout(n),s(a))};this.signaling.on(t,o)})}_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,f=a>0?l/(a/1e3):0,h=f>0?Math.round((s.fileSize-l)/f):0;this.emit("progress",{transferId:t,percent:Math.min(o,100),speed:f,eta:h,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"ack_wl":this.engine.handleWaterLevelAck(e,s.wl,s.ooo);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"ack_batch":Array.isArray(s.pieces)&&s.pieces.length>0&&this.engine.handlePieceAckBatch(e,s.pieces);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=()=>{let n=this.engine.transfers.get(t);if(!n){clearInterval(r),i(new Error("Transfer context lost"));return}if(n.state==="completed"){clearInterval(r);let o=Date.now()-n.startTime;e({speed:n.manifest.fileSize/(o/1e3),duration:o});return}if(n.state==="cancelled"||n.state==="failed"){clearInterval(r),i(new Error("Transfer failed"));return}},r=setInterval(()=>{s()},500);this.on("transfer-complete",({transferId:n})=>{n===t&&s()})})}_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 Ee from"node:path";import{access as Re,stat as Le}from"node:fs/promises";var ie="0.2.1",oe="https://perkoon.com",ae=0,L=1,ne=2,Q=3,Be=4,Fe=5,E=process.argv.slice(2),re=E[0],v={};for(let c=1;c<E.length;c++)if(E[c].startsWith("--")){let t=E[c].slice(2);E[c+1]&&!E[c+1].startsWith("--")?v[t]=E[++c]:v[t]=!0}else v._positional||(v._positional=E[c]);var R=oe,B=v.json===!0,O=v.quiet===!0,ze=v.overwrite===!0,G=v.output||"./received",x=typeof v.password=="string"?v.password:void 0,I=typeof v.timeout=="string"?parseInt(v.timeout,10):NaN,J=!isNaN(I)&&I>0?I:300;B&&G==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
3
3
  `),process.exit(L));function p(c){!O&&!B&&process.stderr.write(c+`
4
4
  `)}function P(c,t={}){B&&process.stdout.write(JSON.stringify({event:c,...t})+`
5
5
  `)}function k(c){p(` \u2713 ${c}`)}function q(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 H(c){return q(c)+"/s"}function ce(c){return c<=0?"...":c<60?`${c}s`:`${Math.floor(c/60)}m ${c%60}s`}function le(c,t=30){let e=Math.round(t*c/100);return"\u2588".repeat(e)+"\u2591".repeat(t-e)}function b(c,t){p(`
6
6
  Error: ${c}`),t&&p(`
7
- ${t}`),p("")}function he(c){let t=c.message||"";return t.includes("No peer connected")||t.includes("timed out")?Fe:t.includes("Password required")||t.includes("Invalid password")||t.includes("Access denied")?Be:(t.includes("Failed to create session")||t.includes("Failed to join")||t.includes("not found")||t.includes("expired"),Q)}async function Me(){let c=T._positional;c||(b("No file specified.","Usage: perkoon send <file>"),process.exit(L));let t=Ee.resolve(c);try{await Re(t),(await Le(t)).isFile()||(b(`Not a regular file: ${t}`),process.exit(ne))}catch{b(`File not found: ${t}`),process.exit(ne)}let e=new $(R,{timeout:J*1e3});e.on("file-ready",({name:r,size:n})=>{p(""),k(`${r} (${q(n)})`),P("file_ready",{name:r,size:n})}),e.on("session-created",({sessionCode:r})=>{k(`Code: ${r}`),x&&k("Password protected"),p("");let n=x?` --password ${x}`:"";p(` Receiver command: perkoon receive ${r}${n}`),p(` Or open in browser: ${R}/${r}`),p(""),P("session_created",{session_code:r,share_url:`${R}/${r}`})}),e.on("waiting-for-receiver",()=>{p(" Waiting for receiver..."),P("waiting_for_receiver")}),e.on("receiver-connected",()=>{k("Receiver connected"),P("receiver_connected")}),e.on("waiting-for-acceptance",()=>{p(" Waiting for receiver to accept..."),P("waiting_for_acceptance")}),e.on("transfer-accepted",()=>{k("Transfer accepted"),P("transfer_accepted")}),e.on("connected",()=>{k("Direct connection established"),p(""),P("webrtc_connected")});let i="";e.on("progress",({percent:r,speed:n,eta:o,bytesTransferred:a})=>{if(B)P("progress",{percent:r,speed:Math.round(n),eta:o,bytes_transferred:a});else if(!O){let l=` ${le(r)} ${String(r).padStart(3)}% ${H(n).padStart(10)} ETA ${ce(o)}`;l!==i&&(process.stderr.write(`\r${l}`),i=l)}});let s=()=>{p(`
7
+ ${t}`),p("")}function he(c){let t=c.message||"";return t.includes("No peer connected")||t.includes("timed out")?Fe:t.includes("Password required")||t.includes("Invalid password")||t.includes("Access denied")?Be:(t.includes("Failed to create session")||t.includes("Failed to join")||t.includes("not found")||t.includes("expired"),Q)}async function Me(){let c=v._positional;c||(b("No file specified.","Usage: perkoon send <file>"),process.exit(L));let t=Ee.resolve(c);try{await Re(t),(await Le(t)).isFile()||(b(`Not a regular file: ${t}`),process.exit(ne))}catch{b(`File not found: ${t}`),process.exit(ne)}let e=new $(R,{timeout:J*1e3});e.on("file-ready",({name:r,size:n})=>{p(""),k(`${r} (${q(n)})`),P("file_ready",{name:r,size:n})}),e.on("session-created",({sessionCode:r})=>{k(`Code: ${r}`),x&&k("Password protected"),p("");let n=x?` --password ${x}`:"";p(` Receiver command: perkoon receive ${r}${n}`),p(` Or open in browser: ${R}/${r}`),p(""),P("session_created",{session_code:r,share_url:`${R}/${r}`})}),e.on("waiting-for-receiver",()=>{p(" Waiting for receiver..."),P("waiting_for_receiver")}),e.on("receiver-connected",()=>{k("Receiver connected"),P("receiver_connected")}),e.on("waiting-for-acceptance",()=>{p(" Waiting for receiver to accept..."),P("waiting_for_acceptance")}),e.on("transfer-accepted",()=>{k("Transfer accepted"),P("transfer_accepted")}),e.on("connected",()=>{k("Direct connection established"),p(""),P("webrtc_connected")});let i="";e.on("progress",({percent:r,speed:n,eta:o,bytesTransferred:a})=>{if(B)P("progress",{percent:r,speed:Math.round(n),eta:o,bytes_transferred:a});else if(!O){let l=` ${le(r)} ${String(r).padStart(3)}% ${H(n).padStart(10)} ETA ${ce(o)}`;l!==i&&(process.stderr.write(`\r${l}`),i=l)}});let s=()=>{p(`
8
8
 
9
9
  Cancelled.`),e.destroy(),process.exit(L)};process.on("SIGINT",s),process.on("SIGTERM",s);try{let r=await e.send(t,{password:x});if(!O&&!B){process.stderr.write(`
10
10
  `);let n=q(r.speed*(r.duration/1e3));k(`Complete: ${n} in ${(r.duration/1e3).toFixed(1)}s (${H(r.speed)})`),p(""),p(` Tip: Send files from your browser at ${R}`),p("")}P("transfer_complete",{session_code:r.sessionCode,duration_ms:r.duration,speed:Math.round(r.speed)}),process.exit(ae)}catch(r){let n=he(r);r.message.includes("No peer connected")?b(`No receiver joined after ${J}s.`,`Make sure they entered the right code.
11
- Or share the link: ${R}/${e.sessionCode||""}`):r.message.includes("Failed to create session")?b("Could not reach perkoon.com","Check your internet connection and try again."):b(r.message),P("error",{message:r.message,exit_code:n}),e.destroy(),process.exit(n)}}async function $e(){let c=T._positional;c||(b("No session code specified.","Usage: perkoon receive <code>"),process.exit(L)),/^[A-Za-z0-9]{12}$/i.test(c)||(b(`Invalid code: ${c}`,"Codes are 12 alphanumeric characters, like K7MX4QPR9W2N."),process.exit(L));let t=G==="-",e=c.toUpperCase(),i=new $(R,{timeout:J*1e3});i.on("session-joined",()=>{p(""),k(`Joined session ${e}`),P("session_joined",{session_code:e})}),i.on("sender-found",()=>{k("Sender found"),P("sender_found")}),i.on("connected",()=>{k("Direct connection established"),P("webrtc_connected")}),i.on("receiving-file",({name:n,size:o})=>{k(`Receiving: ${n} (${q(o)})`),p(""),P("receiving_file",{name:n,size:o})});let s="";i.on("progress",({percent:n,speed:o,eta:a,bytesTransferred:l})=>{if(B)P("progress",{percent:n,speed:Math.round(o),eta:a,bytes_transferred:l});else if(!O){let f=` ${le(n)} ${String(n).padStart(3)}% ${H(o).padStart(10)} ETA ${ce(a)}`;f!==s&&(process.stderr.write(`\r${f}`),s=f)}});let r=()=>{p(`
11
+ Or share the link: ${R}/${e.sessionCode||""}`):r.message.includes("Failed to create session")?b("Could not reach perkoon.com","Check your internet connection and try again."):b(r.message),P("error",{message:r.message,exit_code:n}),e.destroy(),process.exit(n)}}async function $e(){let c=v._positional;c||(b("No session code specified.","Usage: perkoon receive <code>"),process.exit(L)),/^[A-Za-z0-9]{12}$/i.test(c)||(b(`Invalid code: ${c}`,"Codes are 12 alphanumeric characters, like K7MX4QPR9W2N."),process.exit(L));let t=G==="-",e=c.toUpperCase(),i=new $(R,{timeout:J*1e3});i.on("session-joined",()=>{p(""),k(`Joined session ${e}`),P("session_joined",{session_code:e})}),i.on("sender-found",()=>{k("Sender found"),P("sender_found")}),i.on("connected",()=>{k("Direct connection established"),P("webrtc_connected")}),i.on("receiving-file",({name:n,size:o})=>{k(`Receiving: ${n} (${q(o)})`),p(""),P("receiving_file",{name:n,size:o})});let s="";i.on("progress",({percent:n,speed:o,eta:a,bytesTransferred:l})=>{if(B)P("progress",{percent:n,speed:Math.round(o),eta:a,bytes_transferred:l});else if(!O){let f=` ${le(n)} ${String(n).padStart(3)}% ${H(o).padStart(10)} ETA ${ce(a)}`;f!==s&&(process.stderr.write(`\r${f}`),s=f)}});let r=()=>{p(`
12
12
 
13
13
  Cancelled.`),i.destroy(),process.exit(L)};process.on("SIGINT",r),process.on("SIGTERM",r);try{let n={overwrite:ze,password:x};t&&(n.stdout=!0);let o=await i.receive(e,t?null:G,n);if(!O&&!B){if(process.stderr.write(`
14
14
  `),!t)for(let a of o.files)k(`Saved: ${a}`);k(`Complete: ${(o.duration/1e3).toFixed(1)}s (${H(o.speed)})`),p(""),p(` Tip: Send files from your browser at ${R}`),p("")}P("transfer_complete",{files:o.files,duration_ms:o.duration,speed:Math.round(o.speed)}),process.exit(ae)}catch(n){let o=he(n);n.message.includes("not found")?b(`Session not found (${e})`,`The code may be expired or mistyped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perkoon",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",