perkoon 0.3.3 → 0.4.0
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/AGENTS.md +4 -4
- package/README.md +12 -0
- package/dist/bin/perkoon.js +15 -15
- package/dist/client.js +1 -1
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Quick Start
|
|
4
4
|
```bash
|
|
5
|
-
npx -y perkoon@latest send
|
|
6
|
-
npx -y perkoon@latest receive CODE
|
|
7
|
-
npx -y perkoon@latest send
|
|
5
|
+
npx -y perkoon@latest send /path/to/file.pdf # Creates session, shows code
|
|
6
|
+
npx -y perkoon@latest receive CODE # Receives on other machine
|
|
7
|
+
npx -y perkoon@latest send /path/to/file.pdf --json # Machine-readable JSONL output
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## For AI Agents
|
|
@@ -15,7 +15,7 @@ npx -y perkoon@latest send ./file.pdf --json # Machine-readable JSONL output
|
|
|
15
15
|
|
|
16
16
|
## MCP Server (recommended)
|
|
17
17
|
```bash
|
|
18
|
-
npx -y @perkoon/mcp # Adds send_file
|
|
18
|
+
npx -y @perkoon/mcp # Adds send_file, receive_file, check_session as native tools
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Programmatic API
|
package/README.md
CHANGED
|
@@ -60,8 +60,20 @@ $ perkoon receive K7MX4QPR9W2N
|
|
|
60
60
|
--overwrite Replace existing files
|
|
61
61
|
--json Machine-readable JSON output
|
|
62
62
|
--quiet No progress output
|
|
63
|
+
--source <value> Analytics attribution: agent_cli (default), agent_mcp,
|
|
64
|
+
or agent_skill (override when discovered via SKILL.md)
|
|
65
|
+
--server <url> Override server URL (default: https://perkoon.com).
|
|
66
|
+
Useful for self-hosted Perkoon and local dev.
|
|
67
|
+
Also via PERKOON_URL env.
|
|
63
68
|
```
|
|
64
69
|
|
|
70
|
+
### Environment variables
|
|
71
|
+
|
|
72
|
+
| Var | Effect |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `PERKOON_URL` | Override server (same as `--server`). Useful for `dev.perkoon.com`. |
|
|
75
|
+
| `PERKOON_SOURCE` | Override analytics attribution (same as `--source`). The MCP wrapper sets this to `agent_mcp` when spawning the CLI. |
|
|
76
|
+
|
|
65
77
|
## JSON mode
|
|
66
78
|
|
|
67
79
|
For automation and AI agents, `--json` outputs structured events to stdout:
|
package/dist/bin/perkoon.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{EventEmitter as Ae}from"node:events";import H from"node:crypto";import de from"node-datachannel";import{EventEmitter as pe}from"node:events";var{PeerConnection:me}=de,z=class extends pe{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 me(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 ge}from"phoenix";import _e from"ws";import{EventEmitter as ye}from"node:events";var M=class extends ye{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 ge(r,{params:{},transport:_e,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 we}from"node:fs/promises";import Pe from"node:path";import{lookup as Se}from"mime-types";async function Y(c){let t=await we(c,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${c}`);return{name:Pe.basename(c),size:e.size,type:Se(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 Te,mkdir as ve,access as ee}from"node:fs/promises";import te from"node:path";function se(c,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await ve(c,{recursive:!0});let n=be(s.fileName),o=te.join(c,n);t.overwrite||(o=await ke(o));let a=await Te(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 ke(c){try{await ee(c)}catch{return c}let t=te.extname(c),e=c.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await ee(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function be(c){return c.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function ie(){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 Ce={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={...Ce,...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 w=a.slice(h,h+f).map(async P=>{let u=await this._readPiece(e.file,e.manifest,P);return{pieceIndex:P,data:u}}),T=await Promise.all(w);if(r)for(let{pieceIndex:P,data:u}of T){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,P,u)),this._startPieceTimeout(t,P)}else for(let{pieceIndex:P,data:u}of T){let _=null,d=-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,d=g,e.channelIndex=(g+1)%e.channels.length;break}}if(!_)return;_.send(this._encodePieceMessage(t,P,u)),e.inFlightPieces.set(P,{sendTime:Date.now(),channelIndex:d,pieceSize:this._getPieceSize(e.manifest,P)}),this._startPieceTimeout(t,P)}}}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 w=this.config.maxInFlightPieces-i;h=Math.min(h,w);let T=[];for(;e.retryQueue.length>0&&T.length<h;){let u=e.retryQueue.shift();e.inFlightPieces.has(u)||T.push(u)}for(;e.nextPieceToSend<e.manifest.totalPieces&&T.length<h;){let u=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(u)||T.push(u)}if(T.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=T.length;let P=Date.now();for(let u of T)e.inFlightPieces.set(u,{sendTime:P,pieceSize:this._getPieceSize(e.manifest,u)});this._readAndSendPieces(t,e,s,T)}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[w,T]of e.pieceTimeouts)if(i-T>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(w<=e.receivedWaterLevel||e.receivedOutOfOrder.has(w))){e.pieceTimeouts.delete(w);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&w<=e.ackedWaterLevel){e.pieceTimeouts.delete(w);continue}f.push(w)}if(f.length===0||l)return;let m=f.slice(0,50);for(let w of m)this._handlePieceTimeout(t,w)}_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 ne="0.3.3",q="https://perkoon.com",$=class extends Ae{constructor(t=q,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 Y(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=e.session?await this._joinSession(e.session,e.password,"sender",e.senderKey):await this._createSession(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()}_${H.randomUUID().replace(/-/g,"").slice(0,9)}`,o=[{id:`file_${Date.now()}_${H.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===n);this.emit("transfer-accepted",{batchId:a.batch_id});let l=`transfer_${Date.now()}_${H.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:d})=>{this.signaling.sendIceCandidate(r,{candidate:_,sdpMid:d||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",_=>{let d=_.candidate;this.transport.addIceCandidate({candidate:d.candidate||d,mid:d.sdpMid||d.mid||"0"})});let h=await this._waitForSignal("answer");this.transport.handleAnswer(h.answer);let m=await this.transport.waitForChannel("control"),w=await this.transport.waitForChannel(`transfer-${l}`);this.emit("connected"),this.engine.initSend(l,this.fileSource,w);let T=this.engine.transfers.get(l).manifest;this._setupControlHandler(m,l);let P=JSON.stringify({type:"manifest",transfer_id:l,manifest:T,batch_id:n,batch_total_files:1,batch_total_size:this.fileSource.size});m.send(P),this._setupProgressEvents(l),await this.engine.startSend(l);let u=await this._waitForCompletion(l);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:u.speed,duration:u.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");this.signaling.on("files_offered",u=>{this.emit("files-offered",{files:u.files,from:u.from}),this.signaling.channel.push("accept_transfer",{batch_id:u.batch_id})});let 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:u,mid:_})=>{this.signaling.sendIceCandidate(n,{candidate:u,sdpMid:_||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",u=>{let _=u.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"),this.fileSink=i.stdout?ie():se(e,{overwrite:i.overwrite});let m=this.fileSink,w=[],P=await new Promise((u,_)=>{let d=null;l.addEventListener("message",async A=>{if(typeof A.data=="string")try{let g=JSON.parse(A.data);if(g.type==="manifest"&&!d){d=g.transfer_id,this._activeTransferId=d;let v=g.manifest;this.emit("receiving-file",{name:v.fileName,size:v.fileSize}),await m.startStreamingReceive(d,{fileName:v.fileName,size:v.fileSize,fileSize:v.fileSize,type:v.fileType,pieceSize:v.pieceSize},{}),this.engine.initReceive(d,v,{streamingHandler:m}),this._setupProgressEvents(d),this.engine.startReceive(d,C=>{this._safeSend(l,JSON.stringify({type:"request",transfer_id:d,piece_index:C}))});let F=-1;this._ackInterval=setInterval(()=>{let C=this.engine.transfers.get(d);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:d,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(!d||!(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(d,F,C))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let D=this.engine.transfers.get(d);D&&this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:d,wl:D.receivedWaterLevel}));let ue=m.getFilePath(d);w.push(ue),await this.engine.completeReceive(d),this._safeSend(l,JSON.stringify({type:"complete",transfer_id:d}));let V=this.engine.transfers.get(d),X=Date.now()-(V?.startTime||Date.now()),Z=V?.manifest;u({files:w,speed:Z?Z.fileSize/(X/1e3):0,duration:X})}})});return this.signaling.disconnect(),this.transport.close(),P}async _createSession(t){let e=`${this.serverUrl}/api/v1/sessions`,i={client_version:ne};t&&(i.password=t);let s=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!s.ok){let r=await s.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.error||s.statusText}`)}return s.json()}async _joinSession(t,e,i="receiver",s=void 0){let r=`${this.serverUrl}/api/v1/sessions/${t}/join`,n={role:i,client_version:ne};e&&(n.password=e),s&&(n.sender_key=s);let o=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!o.ok){let a=await o.json().catch(()=>({}));throw o.status===404?new Error(`Session ${t} not found. Check the code and try again.`):o.status===410?new Error(`Session ${t} has expired.`):o.status===403?new Error(a.error||"Access denied"):new Error(`Failed to join session: ${a.error||o.statusText}`)}return o.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{s(),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.fileSink&&this._activeTransferId&&(this.fileSink.abort(this._activeTransferId).catch(()=>{}),this.fileSink=null),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 re="0.3.3",ce=0,L=1,oe=2,Q=3,Be=4,Fe=5,E=process.argv.slice(2),ae=E[0],y={};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("--")?y[t]=E[++c]:y[t]=!0}else y._positional||(y._positional=E[c]);var R=q,B=y.json===!0,O=y.quiet===!0,ze=y.overwrite===!0,G=y.output||"./received",x=typeof y.password=="string"?y.password:void 0,Me=typeof y.session=="string"?y.session:void 0,$e=typeof y["sender-key"]=="string"?y["sender-key"]:void 0,J=typeof y.timeout=="string"?parseInt(y.timeout,10):NaN,K=!isNaN(J)&&J>0?J:300;B&&G==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
|
|
3
|
-
`),process.exit(
|
|
4
|
-
`)}function
|
|
5
|
-
`)}function
|
|
6
|
-
Error: ${
|
|
7
|
-
${t}`),
|
|
2
|
+
import{EventEmitter as Ie}from"node:events";import Q from"node:crypto";import _e from"node-datachannel";import{EventEmitter as Se}from"node:events";var{PeerConnection:ve}=_e,B=class extends Se{constructor(){super(),this.pc=null,this.channels=new Map,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let s={iceServers:e.map(i=>(Array.isArray(i.urls)?i.urls:[i.urls]).map(n=>i.username&&i.credential?`${n}:${i.username}:${i.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new ve(t,s),this.pc.onLocalCandidate((i,r)=>{this.emit("ice-candidate",{candidate:i,mid:r})}),this.pc.onStateChange(i=>{this.emit("connection-state",i)}),this.pc.onGatheringStateChange(i=>{this.emit("gathering-state",i)}),this.pc.onDataChannel(i=>{let r=this._wrapChannel(i),n=i.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 s=this.pc.createDataChannel("control",{ordered:!0});this.channels.set("control",this._wrapChannel(s));let i=`transfer-${t}`,r=this.pc.createDataChannel(i,{ordered:!0,maxRetransmits:3});this.channels.set(i,this._wrapChannel(r))})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((s,i)=>{e({sdp:s,type:i})}),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 s=this.channels.get(t);return s?.readyState==="open"?s:new Promise((i,r)=>{let n=setTimeout(()=>{r(new Error(`Data channel "${t}" open timeout`))},e),o=c=>{c.readyState==="open"?(clearTimeout(n),i(c)):c.addEventListener("open",()=>{clearTimeout(n),i(c)})};if(s)o(s);else{let c=(l,f)=>{f===t&&(this.removeListener("data-channel",c),o(l))};this.on("data-channel",c)}})}async waitForChannelByPrefix(t,e=3e4){for(let[s,i]of this.channels)if(s.startsWith(t))return i.readyState==="open"?{channel:i,label:s}:new Promise((r,n)=>{let o=setTimeout(()=>n(new Error(`Channel "${s}" open timeout`)),e);i.addEventListener("open",()=>{clearTimeout(o),r({channel:i,label:s})})});return new Promise((s,i)=>{let r=setTimeout(()=>{this.removeListener("data-channel",n),i(new Error(`No channel with prefix "${t}" within ${e}ms`))},e),n=(o,c)=>{c.startsWith(t)&&(this.removeListener("data-channel",n),o.readyState==="open"?(clearTimeout(r),s({channel:o,label:c})):o.addEventListener("open",()=>{clearTimeout(r),s({channel:o,label:c})}))};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,s="connecting";try{t.isOpen&&t.isOpen()&&(s="open")}catch{}let i={get readyState(){return s},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(){s="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 c=e.get(n);c&&c.delete(o)},set onmessage(n){i.addEventListener("message",n)},set onopen(n){i.addEventListener("open",n)},set onclose(n){i.addEventListener("close",n)},set onerror(n){i.addEventListener("error",n)}};t.onOpen(()=>{s="open",r("open",{})}),t.onClosed(()=>{s="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 c=e.get(n);if(c)for(let l of c)try{l(o)}catch{}}return i}};import{Socket as Pe}from"phoenix";import Te from"ws";import{EventEmitter as be}from"node:events";var z=class extends be{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,s,i){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,o)=>{this.socket=new Pe(r,{params:{},transport:Te,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(c=>{this.connected||o(new Error(`Signaling connection failed: ${c?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:s,role:i}),this.channel.on("message",c=>{this.emit(c.type,c)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",c=>{this.connected=!0,this.peers=c.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",c=>{o(new Error(`Channel join failed: ${c.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,s){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("files_added",{batch_id:t,files:e,total_size:s})}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 Ce}from"node:fs/promises";import ke from"node:path";import{lookup as Ee}from"mime-types";async function ee(a){let t=await Ce(a,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${a}`);return{name:ke.basename(a),size:e.size,type:Ee(a)||"application/octet-stream",async readSlice(s,i){let r=i-s,n=Buffer.alloc(r),{bytesRead:o}=await t.read(n,0,r,s);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 Re,mkdir as Le,access as te}from"node:fs/promises";import se from"node:path";function ie(a,t={}){let e=new Map;return{async startStreamingReceive(s,i,r){await Le(a,{recursive:!0});let n=Fe(i.fileName),o=se.join(a,n);t.overwrite||(o=await Ae(o));let c=await Re(o,"w");i.size>0&&await c.truncate(i.size),e.set(s,{fh:c,filePath:o,pieceSize:i.pieceSize,size:i.size,fileName:i.fileName})},async writeChunk(s,i,r){let n=e.get(s);if(!n)return{success:!1,error:"NO_HANDLE"};let o=i*n.pieceSize,c=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(c,0,c.byteLength,o),{success:!0}}catch(l){return{success:!1,error:l.message}}},async completeStream(s){let i=e.get(s);i&&(await i.fh.datasync(),await i.fh.close(),e.delete(s))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(s){return e.get(s)?.filePath},async abort(s){let i=e.get(s);if(i){try{await i.fh.close()}catch{}e.delete(s);try{let{unlink:r}=await import("node:fs/promises");await r(i.filePath)}catch{}}}}}async function Ae(a){try{await te(a)}catch{return a}let t=se.extname(a),e=a.slice(0,-t.length||void 0);for(let s=1;s<1e3;s++){let i=`${e}_${s}${t}`;try{await te(i)}catch{return i}}return`${e}_${Date.now()}${t}`}function Fe(a){return a.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function ne(){let a=new Map;return{async startStreamingReceive(t,e,s){a.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,s){let i=a.get(t);if(!i)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(s)?s:Buffer.from(s);for(i.buffer.set(e,r);i.buffer.has(i.nextPiece);){let n=i.buffer.get(i.nextPiece);i.buffer.delete(i.nextPiece);let o=i.nextPiece*i.pieceSize,c=i.size-o,l=c<n.byteLength?n.subarray(0,c):n;process.stdout.write(l),i.bytesWritten+=l.byteLength,i.nextPiece++}return{success:!0}},async completeStream(t){a.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){a.delete(t)}}}var U=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 s=this._onceListeners.get(t);s&&s.delete(e)}}off(t,e){let s=this._listeners.get(t);s&&s.delete(e);let i=this._onceListeners.get(t);i&&i.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let s=this._listeners.get(t);s&&s.forEach(r=>{try{r(e)}catch{}});let i=this._onceListeners.get(t);if(i){let r=[...i];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,s=this._onceListeners.get(t)?.size||0;return e+s}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((s,i)=>{let r,n=o=>{r&&clearTimeout(r),s(o)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),i(new Error(`Timeout waiting for event: ${t}`))},e))})}};function Me(){let a=Be();return{browser:a.browser,version:a.version,platform:ze(),supportsFileSystemAccess:"showSaveFilePicker"in window,supportsDirectoryPicker:"showDirectoryPicker"in window,supportsOPFS:$e(),supportsWebRTC:"RTCPeerConnection"in window,supportsDataChannel:"RTCPeerConnection"in window&&typeof RTCPeerConnection.prototype.createDataChannel=="function",maxChannelBufferSize:xe(a.browser),recommendedChunkSize:Ne(a.browser),maxFileSize:De(a.browser),maxFileSizeLabel:Ue(a.browser),supportsTransferable:typeof ArrayBuffer.prototype.transfer=="function"||typeof structuredClone=="function",supportsSharedArrayBuffer:typeof SharedArrayBuffer<"u",supportsStreams:"ReadableStream"in window&&"WritableStream"in window,isMemoryConstrained:Oe(),isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}}function Be(){let a=navigator.userAgent,t="unknown",e="0";if(a.includes("Edg/")){t="edge";let s=a.match(/Edg\/(\d+)/);e=s?s[1]:"0"}else if(a.includes("Chrome/")){t="chrome";let s=a.match(/Chrome\/(\d+)/);e=s?s[1]:"0"}else if(a.includes("Firefox/")){t="firefox";let s=a.match(/Firefox\/(\d+)/);e=s?s[1]:"0"}else if(a.includes("Safari/")&&!a.includes("Chrome")){t="safari";let s=a.match(/Version\/(\d+)/);e=s?s[1]:"0"}return{browser:t,version:parseInt(e,10)}}function ze(){let a=navigator.userAgent;return a.includes("Windows")?"windows":a.includes("Mac")?"macos":a.includes("Linux")?"linux":a.includes("Android")?"android":a.includes("iPhone")||a.includes("iPad")?"ios":"unknown"}function $e(){return"storage"in navigator&&"getDirectory"in navigator.storage&&typeof FileSystemFileHandle<"u"&&"createSyncAccessHandle"in FileSystemFileHandle.prototype}function Oe(){return"deviceMemory"in navigator?navigator.deviceMemory<4:/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)}function xe(a){switch(a){case"chrome":case"edge":return 256*1024*1024;default:return 16*1024*1024}}function Ne(a){switch(a){case"chrome":case"edge":return 256*1024;case"firefox":return 64*1024;case"safari":return 128*1024;default:return 64*1024}}function De(a){switch(a){case"chrome":case"edge":case"brave":case"opera":case"vivaldi":case"samsung":case"ucbrowser":case"firefox":return 1/0;case"safari":return 4*1024*1024*1024;default:return 2*1024*1024*1024}}function Ue(a){switch(a){case"chrome":case"edge":case"brave":case"opera":case"vivaldi":case"samsung":case"ucbrowser":case"firefox":return"Unlimited";case"safari":return"4GB";default:return"2GB"}}var H=null;function re(){return H||(H=Me()),H}var oe={chrome:1/0,edge:1/0,brave:1/0,opera:1/0,vivaldi:1/0,samsung:1/0,ucbrowser:1/0,firefox:1/0,safari:4294967296,ios:4294967296,unknown:2147483648},ae={chrome:"Unlimited",edge:"Unlimited",brave:"Unlimited",opera:"Unlimited",vivaldi:"Unlimited",samsung:"Unlimited",ucbrowser:"Unlimited",firefox:"Unlimited",safari:"4GB",ios:"4GB",unknown:"2GB"};function ce(a){return oe[a]||oe.unknown}function le(a){return ae[a]||ae.unknown}var We={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},W=class extends U{constructor(t={}){super(),this.config={...We,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,s){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let i=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:i,channel:s,channels:[s],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,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:i}}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 s=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=s;for(let i of e.channels)i.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),s?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let s=this.transfers.get(t);if(!s||s.type!=="send")return;let i=s.channels.length;s.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),s.channelBytes.push(0),s.channelRotationCounts.push(0),s.channelThresholds||(s.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+i*.25);s.channelThresholds.push(r),!s.paused&&s.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let s=this.transfers.get(t);if(!s||s.type!=="send")return;let i=s.inFlightPieces.get(e);if(!i)return;let r=i.channelIndex>=0?i.channelIndex:0;s.inFlightPieces.delete(e),s.lastActivityTime=Date.now(),s.piecesAckedCount++;let n=this._getPieceSize(s.manifest,e);s.bytesTransferred+=n,s.channelBytes[r]!==void 0&&(s.channelBytes[r]+=n),this._checkChannelRotation(t,s,r);let o=this._calculateProgress(s);if(this._checkProgressMilestones(t,s,o),this._emitProgress(t,s),s.piecesAckedCount===s.manifest.totalPieces){this._completeSend(t);return}!s.paused&&!s.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){if(Array.isArray(e))for(let s of e)this.handlePieceAck(t,s)}handleWaterLevelAck(t,e,s){let i=this.transfers.get(t);if(!(!i||i.type!=="send")){if(typeof e=="number"&&e>=0)for(let r=0;r<=e;r++)this.handlePieceAck(t,r);if(Array.isArray(s))for(let r of s)this.handlePieceAck(t,r)}}requeuePiece(t,e){let s=this.transfers.get(t);!s||s.type!=="send"||s.state==="completed"||s.state==="completing"||(s.inFlightPieces.delete(e),s.retryQueue.includes(e)||s.retryQueue.push(e),s.lastActivityTime=Date.now(),!s.paused&&!s.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,s){if(!this.config.channelRotationEnabled||e.pendingRotations.has(s))return;let i=e.channelBytes[s]||0,r=e.channelThresholds?.[s]||this.config.channelRotationThreshold;i>=r&&(e.pendingRotations.add(s),this.emit("channel-rotation-needed",{transferId:t,channelIndex:s,bytesTransferred:e.bytesTransferred,channelBytes:i,threshold:r,rotationCount:e.channelRotationCounts[s]||0}))}rotateSingleChannel(t,e,s){let i=this.transfers.get(t);if(!i||i.type!=="send"||e>=i.channels.length)return;let r=i.channels[e],n=(i.channelRotationCounts[e]||0)+1;i.channels[e]=s,e===0&&(i.channel=s),i.channelBytes[e]=0,i.channelRotationCounts[e]=n,i.pendingRotations.delete(e),s.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!i.paused&&!i.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,s){let i=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(i,50)}};i(),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,s={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let i=re(),r=e.fileSize,n=ce(i.browser);if(r>n){let c=le(i.browser),l=(r/(1024*1024*1024)).toFixed(2);throw new Error(`FILE_TOO_LARGE: ${l}GB file exceeds ${i.browser}'s ${c} limit. Use a Chromium browser (Chrome, Edge, Brave) for unlimited file sizes.`)}let o={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:s.streamingHandler||null,fileBuffer:null,pieceBuffers:new Map,paused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return o.streamingHandler||this._initReceiveBuffer(o,e),this.transfers.set(t,o),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(o)}}startReceive(t,e){let s=this.transfers.get(t);if(!s||s.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);s.state="transferring",s.startTime=s.startTime||Date.now(),s.requestPiece=e,this._startIdleTimeout(t),this._requestNextPieces(t)}async handlePieceData(t,e,s){let i=this.transfers.get(t);if(!i||i.type!=="receive")return{success:!1,error:"Transfer not found"};if(i.state==="completing"||i.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<0||e>=i.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(s instanceof ArrayBuffer||s instanceof Uint8Array||ArrayBuffer.isView(s)))return{success:!1,error:"Invalid piece data type"};let r=s.byteLength;if(r===0&&!(e===0&&i.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(i.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"};let c=e===i.manifest.totalPieces-1?1:Math.floor(n*.5);if(this._checkRateLimit(t),this._clearPieceTimeout(t,e),e<=i.receivedWaterLevel||i.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:this._calculateProgress(i)};try{if(i.lastActivityTime=Date.now(),i.streamingHandler){let f=await i.streamingHandler.writeChunk(t,e,new Uint8Array(s));if(f===!1||f&&f.success===!1)return{success:!1,error:f&&f.error||"Failed to write piece to streaming handler",retryable:!0,pieceIndex:e}}else this._storePieceInMemory(i,e,s);if(e===i.receivedWaterLevel+1)for(i.receivedWaterLevel=e;i.receivedOutOfOrder.has(i.receivedWaterLevel+1);)i.receivedOutOfOrder.delete(i.receivedWaterLevel+1),i.receivedWaterLevel++;else{if(!!!i.streamingHandler&&i.receivedOutOfOrder.size>=i.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};i.receivedOutOfOrder.add(e)}i.piecesReceivedCount++,i.piecesRequested.delete(e),i.bytesTransferred+=r;let l=this._calculateProgress(i);return this._checkProgressMilestones(t,i,l),this._emitProgress(t,i),i.piecesReceivedCount===i.manifest.totalPieces?(i.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(i.paused||this._requestNextPieces(t),{success:!0,progress:this._calculateProgress(i),piecesReceived:i.piecesReceivedCount,totalPieces:i.manifest.totalPieces})}catch(l){return{success:!1,error:l.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);try{return e.streamingHandler?(await e.streamingHandler.completeStream(t),{success:!0,streaming:!0,manifest:e.manifest}):{success:!0,blob:this._assembleFile(e),manifest:e.manifest,fileName:e.manifest.fileName}}catch(s){return{success:!1,error:s.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}))}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 s=e.startTime?Date.now()-e.startTime:0,i=s>0?e.bytesTransferred/s*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:i,elapsed:s,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||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 s=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=s)return;let i=e.channel,r=e.channels.length===1;if(r){if(i.readyState!=="open"||i.bufferedAmount>this.config.highWaterMark)return}else{let u=!1;for(let S of e.channels)if(S.readyState==="open"&&S.bufferedAmount<=this.config.highWaterMark){u=!0;break}if(!u)return}let o=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(o<=0)return;let c=[];for(;e.retryQueue.length>0&&c.length<o;){let u=e.retryQueue.shift();e.inFlightPieces.has(u)||c.push(u)}for(;e.nextPieceToSend<e.manifest.totalPieces&&c.length<o;){let u=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(u)||c.push(u)}if(c.length===0)return;let l=Date.now();for(let u of c)e.inFlightPieces.set(u,l);let f=r?4:4*e.channels.length;try{for(let u=0;u<c.length;u+=f){let A=c.slice(u,u+f).map(async v=>{let g=await this._readPiece(e.file,e.manifest,v);return{pieceIndex:v,data:g}}),F=await Promise.all(A);if(r)for(let{pieceIndex:v,data:g}of F){if(i.readyState!=="open"||i.bufferedAmount>this.config.highWaterMark)return;i.send(this._encodePieceMessage(t,v,g)),e.lastActivityTime=Date.now(),this._startPieceTimeout(t,v)}else for(let{pieceIndex:v,data:g}of F){let w=null,h=-1;for(let C=0;C<e.channels.length;C++){let m=(e.channelIndex+C)%e.channels.length,_=e.channels[m];if(_.readyState==="open"&&_.bufferedAmount<=this.config.highWaterMark){w=_,h=m,e.channelIndex=(m+1)%e.channels.length;break}}if(!w)return;w.send(this._encodePieceMessage(t,v,g)),e.inFlightPieces.set(v,{sendTime:Date.now(),channelIndex:h}),e.lastActivityTime=Date.now(),this._startPieceTimeout(t,v)}}}catch(u){for(let S of c)e.inFlightPieces.delete(S),e.retryQueue.push(S);this.emit("error",{transferId:t,error:`Failed to send pieces: ${u.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&(e.pieceTimeouts.forEach(s=>clearTimeout(s)),e.pieceTimeouts.clear()),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.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 s=e.inFlightPieces.size;if(s>=this.config.maxInFlightPieces)return;let i=e.channel;if(!i||i.readyState!=="open")return;let r=i.bufferedAmount,n=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?n=32:r>this.config.minBufferedAmount?n=this.config.maxPiecesPerTick:n=this.config.burstPiecesPerTick;let o=this.config.maxPendingReads-e.pendingReads;if(o<=0)return;n=Math.min(n,o);let c=this.config.maxInFlightPieces-s;n=Math.min(n,c);let l=[];for(;e.retryQueue.length>0&&l.length<n;){let u=e.retryQueue.shift();e.inFlightPieces.has(u)||l.push(u)}for(;e.nextPieceToSend<e.manifest.totalPieces&&l.length<n;){let u=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(u)||l.push(u)}if(l.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=l.length;let f=Date.now();for(let u of l)e.inFlightPieces.set(u,f);this._readAndSendPieces(t,e,i,l)}async _readAndSendPieces(t,e,s,i){try{let r=i.map(o=>this._readPiece(e.file,e.manifest,o).then(c=>({pieceIndex:o,data:c})).catch(c=>({pieceIndex:o,error:c}))),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:c,data:l}=o,f=e.channel;if(!f||f.readyState!=="open"){e.inFlightPieces.delete(c),e.retryQueue.push(c);continue}if(f.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(c),e.retryQueue.unshift(c);continue}try{f.send(this._encodePieceMessage(t,c,l)),e.lastActivityTime=Date.now(),this._startPieceTimeout(t,c)}catch{e.inFlightPieces.delete(c),e.retryQueue.unshift(c)}}}catch{for(let n of i)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=i.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let s=this._getNextPiecesToRequest(e);for(let i of s)e.piecesRequested.add(i),e.requestPiece(i),this._startPieceTimeout(t,i)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let s=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&s.length<e;){let i=t.nextPieceToRequest;t.nextPieceToRequest++,!(i<=t.receivedWaterLevel||t.receivedOutOfOrder.has(i))&&!t.piecesRequested.has(i)&&s.push(i)}return s}_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,s){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(s));else{let i=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,i,s.byteLength).set(new Uint8Array(s))}}_assembleFile(t){let{manifest:e,fileBuffer:s,pieceBuffers:i,useChunkedBuffer:r}=t;if(r){let n=[];for(let c=0;c<e.totalPieces;c++){let l=i.get(c);if(!l)throw new Error(`Missing piece ${c}`);n.push(l)}let o=new Blob(n,{type:e.fileType||"application/octet-stream"});return i.clear(),o}else return new Blob([s],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,s=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:s,createdAt:Date.now()}}async _readPiece(t,e,s){let i=s*e.pieceSize,r=Math.min(i+e.pieceSize,t.size),o=await t.slice(i,r).arrayBuffer(),c=new Uint8Array(4+o.byteLength);return c.set(new Uint8Array(o),4),c}_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){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let i=0,r=e.lastSpeedBytes||0,n=e.lastSpeedTime||e.startTime||s,o=s-n;if(o>=200){let f=e.bytesTransferred-r;i=o>0?f/o*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?i=e.smoothedSpeed:e.startTime&&s>e.startTime&&(i=e.bytesTransferred/(s-e.startTime)*1e3);i=Math.max(0,i),Number.isFinite(i)||(i=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=i:i>0&&(e.smoothedSpeed=c*i+(1-c)*e.smoothedSpeed);let l=Math.round(e.smoothedSpeed);e.lastProgressEmit=s,this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),speed:l})}_encodePieceMessage(t,e,s){return new DataView(s.buffer).setUint32(0,e,!0),s.buffer}decodePieceMessage(t){let s=new DataView(t).getUint32(0,!0),i=t.slice(4);return{pieceIndex:s,data:i}}_checkProgressMilestones(t,e,s){let i=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let o=i-e.lastMilestoneCheck.time,c=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(o>0&&o<r&&c>0)n=c/o*1e3/1e6;else if(o>=r){let l=e.startTime?i-e.startTime:0;n=l>0?e.bytesTransferred/l*1e3/1e6:0}}else{let o=e.startTime?i-e.startTime:0;n=o>0?e.bytesTransferred/o*1e3/1e6:0}e.lastMilestoneCheck={time:i,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),s>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=i,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),s>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let s=this.transfers.get(t);if(!s)return;let i=s.pieceTimeouts.get(e);i&&clearTimeout(i);let r=setTimeout(()=>{this._handlePieceTimeout(t,e)},this.config.pieceTimeout);s.pieceTimeouts.set(e,r)}_clearPieceTimeout(t,e){let s=this.transfers.get(t);if(!s)return;let i=s.pieceTimeouts.get(e);i&&(clearTimeout(i),s.pieceTimeouts.delete(e))}_handlePieceTimeout(t,e){let s=this.transfers.get(t);if(!s||s.state==="completed"||s.state==="completing")return;if(s.type==="receive"&&s.receivedOutOfOrder&&(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))){s.pieceTimeouts.delete(e);return}let i=Date.now()-s.lastActivityTime,r=this.config.pieceTimeout*.5,o=s.manifest.totalPieces-s.piecesReceivedCount<=10;if(i<r&&!o){this._startPieceTimeout(t,e);return}let c=s.pieceRetries.get(e)||0;c<this.config.maxPieceRetries?(s.pieceRetries.set(e,c+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:c+1}),s.type==="receive"?(s.piecesRequested.delete(e),s.piecesRequested.add(e),s.requestPiece(e),this._startPieceTimeout(t,e)):(s.inFlightPieces.delete(e),s.retryQueue.push(e),s.usePacing||this._sendNextPieces(t))):(s.inFlightPieces.delete(e),s.pieceRetries.delete(e),this._clearPieceTimeout(t,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 s=this.transfers.get(t);if(!s||s.state==="completing"||s.state==="completed")return;let i=Date.now()-s.lastActivityTime;i>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:i}):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,s)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(s)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),e.pieceTimeouts.forEach(s=>clearTimeout(s)),e.pieceTimeouts.clear(),this._stopIdleTimeout(t),e.channels)for(let s of e.channels)s.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(),s=this.rateLimits.get(t);return s||(s={count:0,windowStart:e},this.rateLimits.set(t,s)),e-s.windowStart>this.config.rateLimitWindowMs&&(s.count=0,s.windowStart=e),s.count++,{allowed:s.count<=this.config.maxPiecesPerSecond,currentRate:s.count,limit:this.config.maxPiecesPerSecond}}};var fe="0.4.0",I="https://perkoon.com",$=class extends Ie{constructor(t=I,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new W,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await ee(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let s=e.session?await this._joinSession(e.session,e.password,"sender",e.senderKey):await this._createSession(e.password,e.source);this.sessionCode=s.session_code,this.emit("session-created",{sessionCode:s.session_code,expiresAt:s.expires_at}),this.signaling=new z(this.serverUrl);let{peers:i}=await this.signaling.connect(s.session_code,s.token,s.peer_id,"sender");this.emit("waiting-for-receiver");let r=i.length>0?i[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r});let n=`batch_${Date.now()}_${Q.randomUUID().replace(/-/g,"").slice(0,9)}`,o=[{id:`file_${Date.now()}_${Q.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 c=await this._waitForSignal("transfer_accepted",this.peerTimeout,w=>w.batch_id===n);this.emit("transfer-accepted",{batchId:c.batch_id});let l=`transfer_${Date.now()}_${Q.randomUUID().replace(/-/g,"").slice(0,9)}`;this.transport=new B,this.transport.initialize(s.peer_id,s.ice_servers);let f=await this.transport.createOffer(l);this.signaling.sendOffer(r,f),this.transport.on("ice-candidate",({candidate:w,mid:h})=>{this.signaling.sendIceCandidate(r,{candidate:w,sdpMid:h||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",w=>{let h=w.candidate;this.transport.addIceCandidate({candidate:h.candidate||h,mid:h.sdpMid||h.mid||"0"})});let u=await this._waitForSignal("answer");this.transport.handleAnswer(u.answer);let S=await this.transport.waitForChannel("control"),A=await this.transport.waitForChannel(`transfer-${l}`);this.emit("connected"),this.engine.initSend(l,this.fileSource,A);let F=this.engine.transfers.get(l).manifest;this._setupControlHandler(S,l);let v=JSON.stringify({type:"manifest",transfer_id:l,manifest:F,batch_id:n,batch_total_files:1,batch_total_size:this.fileSource.size});S.send(v),this._setupProgressEvents(l),await this.engine.startSend(l);let g=await this._waitForCompletion(l);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:s.session_code,speed:g.speed,duration:g.duration}}async receive(t,e,s={}){let i=await this._joinSession(t,s.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new z(this.serverUrl);let{peers:r}=await this.signaling.connect(t,i.token,i.peer_id,"receiver");this.signaling.on("files_offered",g=>{this.emit("files-offered",{files:g.files,from:g.from}),this.signaling.channel.push("accept_transfer",{batch_id:g.batch_id})});let 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 B,this.transport.initialize(i.peer_id,i.ice_servers),this.transport.on("ice-candidate",({candidate:g,mid:w})=>{this.signaling.sendIceCandidate(n,{candidate:g,sdpMid:w||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",g=>{let w=g.candidate;this.transport.addIceCandidate({candidate:w.candidate||w,mid:w.sdpMid||w.mid||"0"})});let o=await this._waitForSignal("offer"),c=await this.transport.handleOffer(o.offer);this.signaling.sendAnswer(n,c);let l=await this.transport.waitForChannel("control"),{channel:f,label:u}=await this.transport.waitForChannelByPrefix("transfer-");this.emit("connected"),this.fileSink=s.stdout?ne():ie(e,{overwrite:s.overwrite});let S=this.fileSink,A=[],v=await new Promise((g,w)=>{let h=null;l.addEventListener("message",async C=>{if(typeof C.data=="string")try{let m=JSON.parse(C.data);if(m.type==="manifest"&&!h){h=m.transfer_id,this._activeTransferId=h;let _=m.manifest;this.emit("receiving-file",{name:_.fileName,size:_.fileSize}),await S.startStreamingReceive(h,{fileName:_.fileName,size:_.fileSize,fileSize:_.fileSize,type:_.fileType,pieceSize:_.pieceSize},{}),this.engine.initReceive(h,_,{streamingHandler:S}),this._setupProgressEvents(h),this.engine.startReceive(h,b=>{this._safeSend(l,JSON.stringify({type:"request",transfer_id:h,piece_index:b}))});let M=-1;this._ackInterval=setInterval(()=>{let b=this.engine.transfers.get(h);if(!b||b.state==="completed"||b.state==="completing"){clearInterval(this._ackInterval);return}let N=b.receivedWaterLevel;if(N>M){M=N;let D=b.receivedOutOfOrder.size>0?Array.from(b.receivedOutOfOrder):void 0;this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:h,wl:N,ooo:D}))}},100)}}catch(m){m.message&&!m.message.includes("Unexpected token")&&w(m)}}),f.addEventListener("message",async C=>{let m=C.data;if(!h||!(m instanceof ArrayBuffer||Buffer.isBuffer(m)))return;let _=m instanceof ArrayBuffer?m:m.buffer.slice(m.byteOffset,m.byteOffset+m.byteLength),{pieceIndex:M,data:b}=this.engine.decodePieceMessage(_);if(M>=0&&b&&(await this.engine.handlePieceData(h,M,b))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let D=this.engine.transfers.get(h);D&&this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:h,wl:D.receivedWaterLevel}));let ye=S.getFilePath(h);A.push(ye),await this.engine.completeReceive(h),this._safeSend(l,JSON.stringify({type:"complete",transfer_id:h}));let V=this.engine.transfers.get(h),Z=Date.now()-(V?.startTime||Date.now()),Y=V?.manifest;g({files:A,speed:Y?Y.fileSize/(Z/1e3):0,duration:Z})}})});return this.signaling.disconnect(),this.transport.close(),v}async _createSession(t,e="agent_cli"){let s=`${this.serverUrl}/api/v1/sessions`,i={client_version:fe,source:e};t&&(i.password=t);let r=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});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,s="receiver",i=void 0){let r=`${this.serverUrl}/api/v1/sessions/${t}/join`,n={role:s,client_version:fe};e&&(n.password=e),i&&(n.sender_key=i);let o=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!o.ok){let c=await o.json().catch(()=>({}));throw o.status===404?new Error(`Session ${t} not found. Check the code and try again.`):o.status===410?new Error(`Session ${t} has expired.`):o.status===403?new Error(c.error||"Access denied"):new Error(`Failed to join session: ${c.error||o.statusText}`)}return o.json()}_waitForPeer(){return new Promise((t,e)=>{let s=setTimeout(()=>{i(),e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),i=()=>{clearTimeout(s),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"))&&(i(),t(n.from))};this.signaling.on("ready",r),this.signaling.on("offer",r),this.signaling.on("peer_joined",r)})}_waitForSignal(t,e=3e4,s=null){return new Promise((i,r)=>{let n=setTimeout(()=>{this.signaling.removeListener(t,o),r(new Error(`Timed out waiting for ${t} signal`))},e),o=c=>{s&&!s(c)||(this.signaling.removeListener(t,o),clearTimeout(n),i(c))};this.signaling.on(t,o)})}_setupProgressEvents(t){let e=setInterval(()=>{let s=this.engine.transfers.get(t);if(!s){clearInterval(e);return}let i=s.manifest,r=i.totalPieces,n;s.type==="send"?n=s.piecesAckedCount||0:n=s.piecesReceivedCount||0;let o=r>0?Math.round(n/r*100):0,c=Date.now()-(s.startTime||Date.now()),l=n*i.pieceSize,f=c>0?l/(c/1e3):0,u=f>0?Math.round((i.fileSize-l)/f):0;this.emit("progress",{transferId:t,percent:Math.min(o,100),speed:f,eta:u,bytesTransferred:l,totalBytes:i.fileSize}),(s.state==="completed"||s.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",s=>{if(typeof s.data=="string")try{let i=JSON.parse(s.data);switch(i.type){case"ack":this.engine.handlePieceAck(e,i.piece_index);break;case"ack_wl":this.engine.handleWaterLevelAck(e,i.wl,i.ooo);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,i.water_level,i.out_of_order);break;case"ack_batch":Array.isArray(i.pieces)&&i.pieces.length>0&&this.engine.handlePieceAckBatch(e,i.pieces);break;case"request":this.engine.requeuePiece(e,i.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":i.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,s)=>{let i=()=>{let n=this.engine.transfers.get(t);if(!n){clearInterval(r),s(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),s(new Error("Transfer failed"));return}},r=setInterval(()=>{i()},500);this.on("transfer-complete",({transferId:n})=>{n===t&&i()})})}_safeSend(t,e){try{t.readyState==="open"&&t.send(e)}catch{}}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSink&&this._activeTransferId&&(this.fileSink.abort(this._activeTransferId).catch(()=>{}),this.fileSink=null),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};import qe from"node:path";import{access as je,stat as He}from"node:fs/promises";var ue="0.4.0",pe=0,R=1,he=2,G=3,Qe=4,Ge=5,k=process.argv.slice(2),de=k[0],p={};for(let a=1;a<k.length;a++)if(k[a].startsWith("--")){let t=k[a].slice(2);k[a+1]&&!k[a+1].startsWith("--")?p[t]=k[++a]:p[t]=!0}else p._positional||(p._positional=k[a]);var E=(typeof p.server=="string"?p.server:null)||process.env.PERKOON_URL||I,L=p.json===!0,x=p.quiet===!0,Je=p.overwrite===!0,X=p.output||"./received",O=typeof p.password=="string"?p.password:void 0,Xe=typeof p.session=="string"?p.session:void 0,Ke=typeof p["sender-key"]=="string"?p["sender-key"]:void 0,Ve=typeof p.source=="string"?p.source:void 0,Ze=Ve||process.env.PERKOON_SOURCE||"agent_cli",J=typeof p.timeout=="string"?parseInt(p.timeout,10):NaN,K=!isNaN(J)&&J>0?J:300;L&&X==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
|
|
3
|
+
`),process.exit(R));function d(a){!x&&!L&&process.stderr.write(a+`
|
|
4
|
+
`)}function y(a,t={}){L&&process.stdout.write(JSON.stringify({event:a,...t})+`
|
|
5
|
+
`)}function P(a){d(` \u2713 ${a}`)}function q(a){return a>=1024*1024*1024?`${(a/(1024*1024*1024)).toFixed(1)} GB`:a>=1024*1024?`${(a/(1024*1024)).toFixed(1)} MB`:a>=1024?`${(a/1024).toFixed(0)} KB`:`${a} B`}function j(a){return q(a)+"/s"}function me(a){return a<=0?"...":a<60?`${a}s`:`${Math.floor(a/60)}m ${a%60}s`}function ge(a,t=30){let e=Math.round(t*a/100);return"\u2588".repeat(e)+"\u2591".repeat(t-e)}function T(a,t){d(`
|
|
6
|
+
Error: ${a}`),t&&d(`
|
|
7
|
+
${t}`),d("")}function we(a){let t=a.message||"";return t.includes("No peer connected")||t.includes("timed out")?Ge:t.includes("Password required")||t.includes("Invalid password")||t.includes("Access denied")?Qe:(t.includes("Failed to create session")||t.includes("Failed to join")||t.includes("not found")||t.includes("expired"),G)}async function Ye(){let a=p._positional;a||(T("No file specified.","Usage: perkoon send <file>"),process.exit(R));let t=qe.resolve(a);try{await je(t),(await He(t)).isFile()||(T(`Not a regular file: ${t}`),process.exit(he))}catch{T(`File not found: ${t}`),process.exit(he)}let e=new $(E,{timeout:K*1e3});e.on("file-ready",({name:r,size:n})=>{d(""),P(`${r} (${q(n)})`),y("file_ready",{name:r,size:n})}),e.on("session-created",({sessionCode:r})=>{P(`Code: ${r}`),O&&P("Password protected"),d("");let n=O?` --password ${O}`:"";d(` Receiver command: perkoon receive ${r}${n}`),d(` Or open in browser: ${E}/${r}`),d(""),y("session_created",{session_code:r,share_url:`${E}/${r}`})}),e.on("waiting-for-receiver",()=>{d(" Waiting for receiver..."),y("waiting_for_receiver")}),e.on("receiver-connected",()=>{P("Receiver connected"),y("receiver_connected")}),e.on("waiting-for-acceptance",()=>{d(" Waiting for receiver to accept..."),y("waiting_for_acceptance")}),e.on("transfer-accepted",()=>{P("Transfer accepted"),y("transfer_accepted")}),e.on("connected",()=>{P("Direct connection established"),d(""),y("webrtc_connected")});let s="";e.on("progress",({percent:r,speed:n,eta:o,bytesTransferred:c})=>{if(L)y("progress",{percent:r,speed:Math.round(n),eta:o,bytes_transferred:c});else if(!x){let l=` ${ge(r)} ${String(r).padStart(3)}% ${j(n).padStart(10)} ETA ${me(o)}`;l!==s&&(process.stderr.write(`\r${l}`),s=l)}});let i=()=>{d(`
|
|
8
8
|
|
|
9
|
-
Cancelled.`),e.destroy(),process.exit(
|
|
10
|
-
`);let n=
|
|
11
|
-
Or share the link: ${
|
|
9
|
+
Cancelled.`),e.destroy(),process.exit(R)};process.on("SIGINT",i),process.on("SIGTERM",i);try{let r=await e.send(t,{password:O,session:Xe,senderKey:Ke,source:Ze});if(!x&&!L){process.stderr.write(`
|
|
10
|
+
`);let n=q(r.speed*(r.duration/1e3));P(`Complete: ${n} in ${(r.duration/1e3).toFixed(1)}s (${j(r.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}y("transfer_complete",{session_code:r.sessionCode,duration_ms:r.duration,speed:Math.round(r.speed)}),process.exit(pe)}catch(r){let n=we(r);r.message.includes("No peer connected")?T(`No receiver joined after ${K}s.`,`Make sure they entered the right code.
|
|
11
|
+
Or share the link: ${E}/${e.sessionCode||""}`):r.message.includes("outdated")||r.message.includes("npm install")?T(r.message.replace("Failed to create session: ","")):r.message.includes("Failed to create session")?T("Could not reach perkoon.com","Check your internet connection and try again."):T(r.message),y("error",{message:r.message,exit_code:n}),e.destroy(),process.exit(n)}}async function et(){let a=p._positional;a||(T("No session code specified.","Usage: perkoon receive <code>"),process.exit(R)),/^[A-Za-z0-9]{12}$/i.test(a)||(T(`Invalid code: ${a}`,"Codes are 12 alphanumeric characters, like K7MX4QPR9W2N."),process.exit(R));let t=X==="-",e=a.toUpperCase(),s=new $(E,{timeout:K*1e3});s.on("session-joined",()=>{d(""),P(`Joined session ${e}`),y("session_joined",{session_code:e})}),s.on("sender-found",()=>{P("Sender found"),y("sender_found")}),s.on("connected",()=>{P("Direct connection established"),y("webrtc_connected")}),s.on("receiving-file",({name:n,size:o})=>{P(`Receiving: ${n} (${q(o)})`),d(""),y("receiving_file",{name:n,size:o})});let i="";s.on("progress",({percent:n,speed:o,eta:c,bytesTransferred:l})=>{if(L)y("progress",{percent:n,speed:Math.round(o),eta:c,bytes_transferred:l});else if(!x){let f=` ${ge(n)} ${String(n).padStart(3)}% ${j(o).padStart(10)} ETA ${me(c)}`;f!==i&&(process.stderr.write(`\r${f}`),i=f)}});let r=()=>{d(`
|
|
12
12
|
|
|
13
|
-
Cancelled.`),
|
|
14
|
-
`),!t)for(let
|
|
15
|
-
Ask the sender for a new code.`):n.message.includes("expired")?
|
|
16
|
-
perkoon v${
|
|
13
|
+
Cancelled.`),s.destroy(),process.exit(R)};process.on("SIGINT",r),process.on("SIGTERM",r);try{let n={overwrite:Je,password:O};t&&(n.stdout=!0);let o=await s.receive(e,t?null:X,n);if(!x&&!L){if(process.stderr.write(`
|
|
14
|
+
`),!t)for(let c of o.files)P(`Saved: ${c}`);P(`Complete: ${(o.duration/1e3).toFixed(1)}s (${j(o.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}y("transfer_complete",{files:o.files,duration_ms:o.duration,speed:Math.round(o.speed)}),process.exit(pe)}catch(n){let o=we(n);n.message.includes("not found")?T(`Session not found (${e})`,`The code may be expired or mistyped.
|
|
15
|
+
Ask the sender for a new code.`):n.message.includes("expired")?T(`Session expired (${e})`,"Ask the sender to create a new session."):T(n.message),y("error",{message:n.message,exit_code:o}),s.destroy(),process.exit(o)}}switch(de){case"send":Ye();break;case"receive":et();break;case"--version":case"-v":console.log(`perkoon v${ue}`);break;case"--help":case"-h":case void 0:console.log(`
|
|
16
|
+
perkoon v${ue} \u2014 File transfer for humans and the things replacing them.
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
19
|
perkoon send <file> Send a file
|
|
@@ -42,5 +42,5 @@ import{EventEmitter as Ae}from"node:events";import H from"node:crypto";import de
|
|
|
42
42
|
4 Auth error (wrong password, access denied)
|
|
43
43
|
5 Timeout (no peer connected)
|
|
44
44
|
|
|
45
|
-
Learn more: ${
|
|
46
|
-
`);break;default:
|
|
45
|
+
Learn more: ${I}
|
|
46
|
+
`);break;default:T(`Unknown command: ${de}`,"Run perkoon --help for usage."),process.exit(R)}
|
package/dist/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{EventEmitter as se}from"node:events";import B from"node:crypto";import q from"node-datachannel";import{EventEmitter as H}from"node:events";var{PeerConnection:Q}=q,k=class extends H{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 Q(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,a)=>{e({sdp:n,type:a})});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),a=o=>{o.readyState==="open"?(clearTimeout(n),s(o)):o.addEventListener("open",()=>{clearTimeout(n),s(o)})};if(i)a(i);else{let o=(c,h)=>{h===t&&(this.removeListener("data-channel",o),a(c))};this.on("data-channel",o)}})}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 a=setTimeout(()=>n(new Error(`Channel "${i}" open timeout`)),e);s.addEventListener("open",()=>{clearTimeout(a),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=(a,o)=>{o.startsWith(t)&&(this.removeListener("data-channel",n),a.readyState==="open"?(clearTimeout(r),i({channel:a,label:o})):a.addEventListener("open",()=>{clearTimeout(r),i({channel:a,label:o})}))};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,a){e.has(n)||e.set(n,new Set),e.get(n).add(a)},removeEventListener(n,a){let o=e.get(n);o&&o.delete(a)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let a;Buffer.isBuffer(n)?a=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):a=n,r("message",{data:a})});function r(n,a){let o=e.get(n);if(o)for(let c of o)try{c(a)}catch{}}return s}};import{Socket as j}from"phoenix";import I from"ws";import{EventEmitter as J}from"node:events";var v=class extends J{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,a)=>{this.socket=new j(r,{params:{},transport:I,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(o=>{this.connected||a(new Error(`Signaling connection failed: ${o?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",o=>{this.emit(o.type,o)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",o=>{this.connected=!0,this.peers=o.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",o=>{a(new Error(`Channel join failed: ${o.reason||"unknown"}`))}).receive("timeout",()=>{a(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}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 V}from"node:fs/promises";import G from"node:path";import{lookup as K}from"mime-types";async function E(d){let t=await V(d,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${d}`);return{name:G.basename(d),size:e.size,type:K(d)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:a}=await t.read(n,0,r,i);return a===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+a)},async close(){await t.close()}}}import{open as X,mkdir as Z,access as x}from"node:fs/promises";import N from"node:path";function z(d,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await Z(d,{recursive:!0});let n=ee(s.fileName),a=N.join(d,n);t.overwrite||(a=await Y(a));let o=await X(a,"w");s.size>0&&await o.truncate(s.size),e.set(i,{fh:o,filePath:a,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let a=s*n.pieceSize,o=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(o,0,o.byteLength,a),{success:!0}}catch(c){return{success:!1,error:c.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}async function Y(d){try{await x(d)}catch{return d}let t=N.extname(d),e=d.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await x(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function ee(d){return d.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function W(){let d=new Map;return{async startStreamingReceive(t,e,i){d.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=d.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let a=s.nextPiece*s.pieceSize,o=s.size-a,c=o<n.byteLength?n.subarray(0,o):n;process.stdout.write(c),s.bytesWritten+=c.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){d.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){d.delete(t)}}}var L=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=a=>{r&&clearTimeout(r),i(a)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var te={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},R=class extends L{constructor(t={}){super(),this.config={...te,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let a=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=a,this._checkProgressMilestones(t,i,a),this._emitProgress(t,i,a),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let a=i.inFlightPieces.get(n);if(!a)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let o=a.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=o,r+=o;let c=a.channelIndex>=0?a.channelIndex:0;i.channelBytes[c]!==void 0&&(i.channelBytes[c]+=o)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let o of i)if(typeof o!="number"||o<0||s.manifest&&o>=s.manifest.totalPieces)return}let r=Date.now(),n=0,a=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let o=[];for(let[c,h]of s.inFlightPieces)c<=e&&o.push([c,h]);for(let[c,h]of o){s.inFlightPieces.delete(c);let l=h.pieceSize||this._getPieceSize(s.manifest,c);s.bytesTransferred+=l,n+=l,a++,s.pieceTimeouts.delete(c)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let o of i){let c=s.inFlightPieces.get(o);if(c){s.inFlightPieces.delete(o);let h=c.pieceSize||this._getPieceSize(s.manifest,o);s.bytesTransferred+=h,n+=h,a++,s.piecesAckedCount++,s.pieceTimeouts.delete(o)}}if(a>0){s.lastActivityTime=r;let o=s.piecesAckedCount/s.manifest.totalPieces*100;s.lastProgress=o,this._checkProgressMilestones(t,s,o),this._emitProgress(t,s,o),s.piecesAckedCount===s.manifest.totalPieces&&this._completeSend(t)}}requeuePiece(t,e){let i=this.transfers.get(t);!i||i.type!=="send"||i.state==="completed"||i.state==="completing"||(i.inFlightPieces.delete(e),i.retryQueue.includes(e)||i.retryQueue.push(e),!i.paused&&!i.usePacing&&this._sendNextPieces(t))}markSendComplete(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.state==="completed"||e.state==="completing"||this._completeSend(t)}_checkChannelRotation(t,e,i){if(!this.config.channelRotationEnabled||e.pendingRotations.has(i))return;let s=e.channelBytes[i]||0,r=e.channelThresholds?.[i]||this.config.channelRotationThreshold;s>=r&&(e.pendingRotations.add(i),this.emit("channel-rotation-needed",{transferId:t,channelIndex:i,bytesTransferred:e.bytesTransferred,channelBytes:s,threshold:r,rotationCount:e.channelRotationCounts[i]||0}))}rotateSingleChannel(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||e>=s.channels.length)return;let r=s.channels[e],n=(s.channelRotationCounts[e]||0)+1;s.channels[e]=i,e===0&&(s.channel=i),s.channelBytes[e]=0,s.channelRotationCounts[e]=n,s.pendingRotations.delete(e),i.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!s.paused&&!s.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,i){let s=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(s,50)}};s(),setTimeout(()=>{if(t.readyState==="open")try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}},5e3)}rotateChannel(t,e){this.rotateChannels(t,[e])}initReceive(t,e,i={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s={id:t,type:"receive",manifest:e,state:"initialized",nextPieceToRequest:0,piecesReceivedCount:0,receivedWaterLevel:-1,receivedOutOfOrder:new Set,maxOutOfOrderSize:1e4,piecesRequested:new Set,bytesTransferred:0,streamingHandler:i.streamingHandler||null,backpressureCheckerId:null,fileBuffer:null,pieceBuffers:new Map,paused:!1,senderPaused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return s.streamingHandler||this._initReceiveBuffer(s,e),this.transfers.set(t,s),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(s)}}startReceive(t,e){let i=this.transfers.get(t);if(!i||i.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);i.state="transferring",i.startTime=i.startTime||Date.now(),i.requestPiece=e,this._startIdleTimeout(t),this._startBackpressureChecker(t),this._requestNextPieces(t)}_startBackpressureChecker(t){let e=this.transfers.get(t);!e||e.backpressureCheckerId||(e.backpressureCheckerId=setInterval(()=>{let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="cancelled"){this._stopBackpressureChecker(t);return}if(i.streamingHandler?.getBackpressureInfo){let s=i.streamingHandler.getBackpressureInfo();!s.isUnderPressure&&i.senderPaused&&(i.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:s.overflowSize}))}},200))}_stopBackpressureChecker(t){let e=this.transfers.get(t);!e||!e.backpressureCheckerId||(clearInterval(e.backpressureCheckerId),e.backpressureCheckerId=null)}async handlePieceData(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="receive")return{success:!1,error:"Transfer not found"};if(s.state==="completing"||s.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:s.lastProgress||0};let r=i.byteLength;if(s.piecesReceivedCount<100){if(e<0||e>=s.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(i instanceof ArrayBuffer||i instanceof Uint8Array||ArrayBuffer.isView(i)))return{success:!1,error:"Invalid piece data type"};if(r===0&&!(e===0&&s.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(s.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"}}if(s.streamingHandler?.getBackpressureInfo){let n=s.streamingHandler.getBackpressureInfo();n.isUnderPressure&&!s.senderPaused?(s.senderPaused=!0,this.emit("send-backpressure",{transferId:t,pause:!0,bufferSize:n.overflowSize})):!n.isUnderPressure&&s.senderPaused&&(s.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:n.overflowSize}))}try{if(e===s.receivedWaterLevel+1){for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;s.lastActivityTime=Date.now()}else if(e>s.receivedWaterLevel+1){if(s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}if(s.streamingHandler?s.streamingHandler.writeChunk(t,e,i).then(a=>{a&&!a.success&&!a.duplicate&&(a.error!=="OVERFLOW_TIMEOUT"&&console.warn("[TransferEngine] Write issue for piece",e,":",a.error),a.error==="SW_DOWNLOAD_DEAD"&&a.retryable===!1&&(console.error("[TransferEngine] Fatal write error - aborting transfer:",t),this.emit("transfer-failed",{transferId:t,error:"Download stream cancelled. Please try again.",fatal:!0}),this.cancel(t)))}).catch(a=>{console.error("[TransferEngine] Write error for piece",e,":",a.message)}):this._storePieceInMemory(s,e,i),s.receivedOutOfOrder.size>=100&&s.requestPiece){let a=Date.now(),o=s.lastGapCheckTime||0;if(a-o>5e3){s.lastGapCheckTime=a;let c=s.receivedWaterLevel+1,h=Math.min(c+10,e);for(let l=c;l<h;l++)!s.receivedOutOfOrder.has(l)&&!s.piecesRequested.has(l)&&(s.piecesRequested.add(l),s.requestPiece(l))}}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.pieceTimeouts.delete(e),s.pieceRetries.delete(e),s.bytesTransferred+=r;let n=s.piecesReceivedCount/s.manifest.totalPieces*100;return s.lastProgress=n,this._checkProgressMilestones(t,s,n),this._emitProgress(t,s,n),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(!s.paused&&s.piecesReceivedCount%10===0&&this._requestNextPieces(t),{success:!0})}catch(n){return{success:!1,error:n.message}}}async completeReceive(t){let e=this.transfers.get(t);if(!e||e.type!=="receive")return{success:!1,error:"Transfer not found"};if(e.piecesReceivedCount!==e.manifest.totalPieces)return{success:!1,error:`Incomplete: ${e.piecesReceivedCount}/${e.manifest.totalPieces}`};e.state="completing",this._stopIdleTimeout(t),this._stopBackpressureChecker(t);try{if(e.streamingHandler)return await e.streamingHandler.completeStream(t),e.state="completed",{success:!0,streaming:!0,manifest:e.manifest};{let i=this._assembleFile(e);return e.state="completed",{success:!0,blob:i,manifest:e.manifest,fileName:e.manifest.fileName}}}catch(i){return{success:!1,error:i.message}}}pause(t){let e=this.transfers.get(t);e&&(e.paused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t),this.emit("paused",{transferId:t}))}resume(t){let e=this.transfers.get(t);!e||!e.paused||(e.paused=!1,this._startIdleTimeout(t),e.type==="send"?e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t):this._requestNextPieces(t),this.emit("resumed",{transferId:t}))}pauseForBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused||(e.backpressurePaused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t))}resumeFromBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused&&(e.backpressurePaused=!1,!e.paused&&(this._startIdleTimeout(t),e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t)))}cancel(t){let e=this.transfers.get(t);e&&(this._cleanupTransfer(t),this.emit("cancelled",{transferId:t,type:e.type}))}cleanup(t){this._cleanupTransfer(t)}getStats(t){let e=this.transfers.get(t);if(!e)return null;let i=e.startTime?Date.now()-e.startTime:0,s=i>0?e.bytesTransferred/i*1e3:0;return{transferId:t,type:e.type,state:e.state,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),piecesCompleted:e.type==="send"?e.piecesAckedCount:e.piecesReceivedCount,totalPieces:e.manifest.totalPieces,speed:s,elapsed:i,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.retryQueue.length>=this.config.maxRetryQueueSize){this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=i)return;let s=e.channel,r=e.channels.length===1;if(r){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return}else{let l=!1;for(let p of e.channels)if(p.readyState==="open"&&p.bufferedAmount<=this.config.highWaterMark){l=!0;break}if(!l)return}let a=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(a<=0)return;let o=[];for(;e.retryQueue.length>0&&o.length<a;){let l=e.retryQueue.shift();e.inFlightPieces.has(l)||o.push(l)}for(;e.nextPieceToSend<e.manifest.totalPieces&&o.length<a;){let l=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(l)||o.push(l)}if(o.length===0)return;let c=Date.now();for(let l of o)e.inFlightPieces.set(l,{sendTime:c,pieceSize:this._getPieceSize(e.manifest,l)});let h=r?4:4*e.channels.length;try{for(let l=0;l<o.length;l+=h){let _=o.slice(l,l+h).map(async y=>{let f=await this._readPiece(e.file,e.manifest,y);return{pieceIndex:y,data:f}}),w=await Promise.all(_);if(r)for(let{pieceIndex:y,data:f}of w){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,y,f)),this._startPieceTimeout(t,y)}else for(let{pieceIndex:y,data:f}of w){let g=null,u=-1;for(let T=0;T<e.channels.length;T++){let m=(e.channelIndex+T)%e.channels.length,P=e.channels[m];if(P.readyState==="open"&&P.bufferedAmount<=this.config.highWaterMark){g=P,u=m,e.channelIndex=(m+1)%e.channels.length;break}}if(!g)return;g.send(this._encodePieceMessage(t,y,f)),e.inFlightPieces.set(y,{sendTime:Date.now(),channelIndex:u,pieceSize:this._getPieceSize(e.manifest,y)}),this._startPieceTimeout(t,y)}}}catch(l){for(let p of o)e.inFlightPieces.delete(p),e.retryQueue.push(p);this.emit("error",{transferId:t,error:`Failed to send pieces: ${l.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,a=this.config.maxInFlightPieces*.99,o=this.config.maxPendingReads*.95,c=r<2*1024*1024,h=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=a||i>=n&&c?"RECEIVER_SLOW":e.pendingReads>=o?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:h})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let l=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?l=32:r>this.config.minBufferedAmount?l=this.config.maxPiecesPerTick:l=this.config.burstPiecesPerTick;let p=this.config.maxPendingReads-e.pendingReads;if(p<=0)return;l=Math.min(l,p);let _=this.config.maxInFlightPieces-i;l=Math.min(l,_);let w=[];for(;e.retryQueue.length>0&&w.length<l;){let f=e.retryQueue.shift();e.inFlightPieces.has(f)||w.push(f)}for(;e.nextPieceToSend<e.manifest.totalPieces&&w.length<l;){let f=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(f)||w.push(f)}if(w.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=w.length;let y=Date.now();for(let f of w)e.inFlightPieces.set(f,{sendTime:y,pieceSize:this._getPieceSize(e.manifest,f)});this._readAndSendPieces(t,e,s,w)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(a=>this._readPiece(e.file,e.manifest,a).then(o=>({pieceIndex:a,data:o})).catch(o=>({pieceIndex:a,error:o}))),n=await Promise.all(r);for(let a of n){if(a.error){e.inFlightPieces.delete(a.pieceIndex),e.retryQueue.push(a.pieceIndex);continue}let{pieceIndex:o,data:c}=a,h=e.channel;if(!h||h.readyState!=="open"){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}if(h.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}try{h.send(this._encodePieceMessage(t,o,c)),this._startPieceTimeout(t,o)}catch{e.inFlightPieces.delete(o),e.retryQueue.push(o)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let o=0;o<e.totalPieces;o++){let c=s.get(o);if(!c)throw new Error(`Missing piece ${o}`);n.push(c)}let a=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),a}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),a=new Uint8Array(4+n.byteLength);return a.set(new Uint8Array(n),4),a}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,a=e.lastSpeedTime||e.startTime||s,o=s-a;if(o>=200){let p=e.bytesTransferred-n;r=o>0?p/o*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=c*r+(1-c)*e.smoothedSpeed);let h=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let l=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:l,speed:h})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){if(!t||!(t instanceof ArrayBuffer)||t.byteLength<4)return{pieceIndex:-1,data:null,error:"invalid_message"};try{let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}catch{return{pieceIndex:-1,data:null,error:"decode_failed"}}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let a=s-e.lastMilestoneCheck.time,o=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(a>0&&a<r&&o>0)n=o/a*1e3/1e6;else if(a>=r){let c=e.startTime?s-e.startTime:0;n=c>0?e.bytesTransferred/c*1e3/1e6:0}}else{let a=e.startTime?s-e.startTime:0;n=a>0?e.bytesTransferred/a*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,o=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,c=n<r&&!o,h=[];for(let[_,w]of e.pieceTimeouts)if(i-w>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(_<=e.receivedWaterLevel||e.receivedOutOfOrder.has(_))){e.pieceTimeouts.delete(_);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&_<=e.ackedWaterLevel){e.pieceTimeouts.delete(_);continue}h.push(_)}if(h.length===0||c)return;let p=h.slice(0,50);for(let _ of p)this._handlePieceTimeout(t,_)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),a=Date.now()-n;a>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:a}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var $="0.3.3",ie="https://perkoon.com",F=class extends se{constructor(t=ie,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new R,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await E(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=e.session?await this._joinSession(e.session,e.password,"sender",e.senderKey):await this._createSession(e.password);this.sessionCode=i.session_code,this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new v(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r});let n=`batch_${Date.now()}_${B.randomUUID().replace(/-/g,"").slice(0,9)}`,a=[{id:`file_${Date.now()}_${B.randomUUID().replace(/-/g,"").slice(0,9)}`,name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type}];this.signaling.sendFilesAdded(n,a,this.fileSource.size),this.emit("waiting-for-acceptance");let o=await this._waitForSignal("transfer_accepted",this.peerTimeout,g=>g.batch_id===n);this.emit("transfer-accepted",{batchId:o.batch_id});let c=`transfer_${Date.now()}_${B.randomUUID().replace(/-/g,"").slice(0,9)}`;this.transport=new k,this.transport.initialize(i.peer_id,i.ice_servers);let h=await this.transport.createOffer(c);this.signaling.sendOffer(r,h),this.transport.on("ice-candidate",({candidate:g,mid:u})=>{this.signaling.sendIceCandidate(r,{candidate:g,sdpMid:u||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",g=>{let u=g.candidate;this.transport.addIceCandidate({candidate:u.candidate||u,mid:u.sdpMid||u.mid||"0"})});let l=await this._waitForSignal("answer");this.transport.handleAnswer(l.answer);let p=await this.transport.waitForChannel("control"),_=await this.transport.waitForChannel(`transfer-${c}`);this.emit("connected"),this.engine.initSend(c,this.fileSource,_);let w=this.engine.transfers.get(c).manifest;this._setupControlHandler(p,c);let y=JSON.stringify({type:"manifest",transfer_id:c,manifest:w,batch_id:n,batch_total_files:1,batch_total_size:this.fileSource.size});p.send(y),this._setupProgressEvents(c),await this.engine.startSend(c);let f=await this._waitForCompletion(c);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:f.speed,duration:f.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new v(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver");this.signaling.on("files_offered",f=>{this.emit("files-offered",{files:f.files,from:f.from}),this.signaling.channel.push("accept_transfer",{batch_id:f.batch_id})});let n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new k,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:f,mid:g})=>{this.signaling.sendIceCandidate(n,{candidate:f,sdpMid:g||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",f=>{let g=f.candidate;this.transport.addIceCandidate({candidate:g.candidate||g,mid:g.sdpMid||g.mid||"0"})});let a=await this._waitForSignal("offer"),o=await this.transport.handleOffer(a.offer);this.signaling.sendAnswer(n,o);let c=await this.transport.waitForChannel("control"),{channel:h,label:l}=await this.transport.waitForChannelByPrefix("transfer-");this.emit("connected"),this.fileSink=i.stdout?W():z(e,{overwrite:i.overwrite});let p=this.fileSink,_=[],y=await new Promise((f,g)=>{let u=null;c.addEventListener("message",async T=>{if(typeof T.data=="string")try{let m=JSON.parse(T.data);if(m.type==="manifest"&&!u){u=m.transfer_id,this._activeTransferId=u;let P=m.manifest;this.emit("receiving-file",{name:P.fileName,size:P.fileSize}),await p.startStreamingReceive(u,{fileName:P.fileName,size:P.fileSize,fileSize:P.fileSize,type:P.fileType,pieceSize:P.pieceSize},{}),this.engine.initReceive(u,P,{streamingHandler:p}),this._setupProgressEvents(u),this.engine.startReceive(u,S=>{this._safeSend(c,JSON.stringify({type:"request",transfer_id:u,piece_index:S}))});let b=-1;this._ackInterval=setInterval(()=>{let S=this.engine.transfers.get(u);if(!S||S.state==="completed"||S.state==="completing"){clearInterval(this._ackInterval);return}let C=S.receivedWaterLevel;if(C>b){b=C;let A=S.receivedOutOfOrder.size>0?Array.from(S.receivedOutOfOrder):void 0;this._safeSend(c,JSON.stringify({type:"ack_wl",transfer_id:u,wl:C,ooo:A}))}},100)}}catch(m){m.message&&!m.message.includes("Unexpected token")&&g(m)}}),h.addEventListener("message",async T=>{let m=T.data;if(!u||!(m instanceof ArrayBuffer||Buffer.isBuffer(m)))return;let P=m instanceof ArrayBuffer?m:m.buffer.slice(m.byteOffset,m.byteOffset+m.byteLength),{pieceIndex:b,data:S}=this.engine.decodePieceMessage(P);if(b>=0&&S&&(await this.engine.handlePieceData(u,b,S))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let A=this.engine.transfers.get(u);A&&this._safeSend(c,JSON.stringify({type:"ack_wl",transfer_id:u,wl:A.receivedWaterLevel}));let U=p.getFilePath(u);_.push(U),await this.engine.completeReceive(u),this._safeSend(c,JSON.stringify({type:"complete",transfer_id:u}));let M=this.engine.transfers.get(u),O=Date.now()-(M?.startTime||Date.now()),D=M?.manifest;f({files:_,speed:D?D.fileSize/(O/1e3):0,duration:O})}})});return this.signaling.disconnect(),this.transport.close(),y}async _createSession(t){let e=`${this.serverUrl}/api/v1/sessions`,i={client_version:$};t&&(i.password=t);let s=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!s.ok){let r=await s.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.error||s.statusText}`)}return s.json()}async _joinSession(t,e,i="receiver",s=void 0){let r=`${this.serverUrl}/api/v1/sessions/${t}/join`,n={role:i,client_version:$};e&&(n.password=e),s&&(n.sender_key=s);let a=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!a.ok){let o=await a.json().catch(()=>({}));throw a.status===404?new Error(`Session ${t} not found. Check the code and try again.`):a.status===410?new Error(`Session ${t} has expired.`):a.status===403?new Error(o.error||"Access denied"):new Error(`Failed to join session: ${o.error||a.statusText}`)}return a.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{s(),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,a),r(new Error(`Timed out waiting for ${t} signal`))},e),a=o=>{i&&!i(o)||(this.signaling.removeListener(t,a),clearTimeout(n),s(o))};this.signaling.on(t,a)})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let a=r>0?Math.round(n/r*100):0,o=Date.now()-(i.startTime||Date.now()),c=n*s.pieceSize,h=o>0?c/(o/1e3):0,l=h>0?Math.round((s.fileSize-c)/h):0;this.emit("progress",{transferId:t,percent:Math.min(a,100),speed:h,eta:l,bytesTransferred:c,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"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 a=Date.now()-n.startTime;e({speed:n.manifest.fileSize/(a/1e3),duration:a});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.fileSink&&this._activeTransferId&&(this.fileSink.abort(this._activeTransferId).catch(()=>{}),this.fileSink=null),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};export{k as NodeTransport,v as SignalingClient,F as TransferManager,z as createNodeFileSink,E as createNodeFileSource};
|
|
1
|
+
import{EventEmitter as we}from"node:events";import F from"node:crypto";import J from"node-datachannel";import{EventEmitter as V}from"node:events";var{PeerConnection:X}=J,v=class extends V{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 X(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{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,a)=>{e({sdp:n,type:a})});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),a=o=>{o.readyState==="open"?(clearTimeout(n),s(o)):o.addEventListener("open",()=>{clearTimeout(n),s(o)})};if(i)a(i);else{let o=(l,f)=>{f===t&&(this.removeListener("data-channel",o),a(l))};this.on("data-channel",o)}})}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 a=setTimeout(()=>n(new Error(`Channel "${i}" open timeout`)),e);s.addEventListener("open",()=>{clearTimeout(a),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=(a,o)=>{o.startsWith(t)&&(this.removeListener("data-channel",n),a.readyState==="open"?(clearTimeout(r),i({channel:a,label:o})):a.addEventListener("open",()=>{clearTimeout(r),i({channel:a,label:o})}))};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,a){e.has(n)||e.set(n,new Set),e.get(n).add(a)},removeEventListener(n,a){let o=e.get(n);o&&o.delete(a)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let a;Buffer.isBuffer(n)?a=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):a=n,r("message",{data:a})});function r(n,a){let o=e.get(n);if(o)for(let l of o)try{l(a)}catch{}}return s}};import{Socket as Z}from"phoenix";import K from"ws";import{EventEmitter as Y}from"node:events";var T=class extends Y{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,a)=>{this.socket=new Z(r,{params:{},transport:K,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(o=>{this.connected||a(new Error(`Signaling connection failed: ${o?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",o=>{this.emit(o.type,o)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",o=>{this.connected=!0,this.peers=o.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",o=>{a(new Error(`Channel join failed: ${o.reason||"unknown"}`))}).receive("timeout",()=>{a(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}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 ee}from"node:fs/promises";import te from"node:path";import{lookup as ie}from"mime-types";async function E(c){let t=await ee(c,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${c}`);return{name:te.basename(c),size:e.size,type:ie(c)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:a}=await t.read(n,0,r,i);return a===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+a)},async close(){await t.close()}}}import{open as se,mkdir as ne,access as N}from"node:fs/promises";import $ from"node:path";function B(c,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await ne(c,{recursive:!0});let n=ae(s.fileName),a=$.join(c,n);t.overwrite||(a=await re(a));let o=await se(a,"w");s.size>0&&await o.truncate(s.size),e.set(i,{fh:o,filePath:a,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let a=s*n.pieceSize,o=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(o,0,o.byteLength,a),{success:!0}}catch(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 re(c){try{await N(c)}catch{return c}let t=$.extname(c),e=c.slice(0,-t.length||void 0);for(let i=1;i<1e3;i++){let s=`${e}_${i}${t}`;try{await N(s)}catch{return s}}return`${e}_${Date.now()}${t}`}function ae(c){return c.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function U(){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 a=s.nextPiece*s.pieceSize,o=s.size-a,l=o<n.byteLength?n.subarray(0,o):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 A=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=a=>{r&&clearTimeout(r),i(a)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};function oe(){let c=ce();return{browser:c.browser,version:c.version,platform:le(),supportsFileSystemAccess:"showSaveFilePicker"in window,supportsDirectoryPicker:"showDirectoryPicker"in window,supportsOPFS:he(),supportsWebRTC:"RTCPeerConnection"in window,supportsDataChannel:"RTCPeerConnection"in window&&typeof RTCPeerConnection.prototype.createDataChannel=="function",maxChannelBufferSize:ue(c.browser),recommendedChunkSize:de(c.browser),maxFileSize:pe(c.browser),maxFileSizeLabel:me(c.browser),supportsTransferable:typeof ArrayBuffer.prototype.transfer=="function"||typeof structuredClone=="function",supportsSharedArrayBuffer:typeof SharedArrayBuffer<"u",supportsStreams:"ReadableStream"in window&&"WritableStream"in window,isMemoryConstrained:fe(),isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}}function ce(){let c=navigator.userAgent,t="unknown",e="0";if(c.includes("Edg/")){t="edge";let i=c.match(/Edg\/(\d+)/);e=i?i[1]:"0"}else if(c.includes("Chrome/")){t="chrome";let i=c.match(/Chrome\/(\d+)/);e=i?i[1]:"0"}else if(c.includes("Firefox/")){t="firefox";let i=c.match(/Firefox\/(\d+)/);e=i?i[1]:"0"}else if(c.includes("Safari/")&&!c.includes("Chrome")){t="safari";let i=c.match(/Version\/(\d+)/);e=i?i[1]:"0"}return{browser:t,version:parseInt(e,10)}}function le(){let c=navigator.userAgent;return c.includes("Windows")?"windows":c.includes("Mac")?"macos":c.includes("Linux")?"linux":c.includes("Android")?"android":c.includes("iPhone")||c.includes("iPad")?"ios":"unknown"}function he(){return"storage"in navigator&&"getDirectory"in navigator.storage&&typeof FileSystemFileHandle<"u"&&"createSyncAccessHandle"in FileSystemFileHandle.prototype}function fe(){return"deviceMemory"in navigator?navigator.deviceMemory<4:/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)}function ue(c){switch(c){case"chrome":case"edge":return 256*1024*1024;default:return 16*1024*1024}}function de(c){switch(c){case"chrome":case"edge":return 256*1024;case"firefox":return 64*1024;case"safari":return 128*1024;default:return 64*1024}}function pe(c){switch(c){case"chrome":case"edge":case"brave":case"opera":case"vivaldi":case"samsung":case"ucbrowser":case"firefox":return 1/0;case"safari":return 4*1024*1024*1024;default:return 2*1024*1024*1024}}function me(c){switch(c){case"chrome":case"edge":case"brave":case"opera":case"vivaldi":case"samsung":case"ucbrowser":case"firefox":return"Unlimited";case"safari":return"4GB";default:return"2GB"}}var z=null;function W(){return z||(z=oe()),z}var q={chrome:1/0,edge:1/0,brave:1/0,opera:1/0,vivaldi:1/0,samsung:1/0,ucbrowser:1/0,firefox:1/0,safari:4294967296,ios:4294967296,unknown:2147483648},H={chrome:"Unlimited",edge:"Unlimited",brave:"Unlimited",opera:"Unlimited",vivaldi:"Unlimited",samsung:"Unlimited",ucbrowser:"Unlimited",firefox:"Unlimited",safari:"4GB",ios:"4GB",unknown:"2GB"};function Q(c){return q[c]||q.unknown}function j(c){return H[c]||H.unknown}var ge={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},R=class extends A{constructor(t={}){super(),this.config={...ge,...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,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.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let a=this._calculateProgress(i);if(this._checkProgressMilestones(t,i,a),this._emitProgress(t,i),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){if(Array.isArray(e))for(let i of e)this.handlePieceAck(t,i)}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!(!s||s.type!=="send")){if(typeof e=="number"&&e>=0)for(let r=0;r<=e;r++)this.handlePieceAck(t,r);if(Array.isArray(i))for(let r of i)this.handlePieceAck(t,r)}}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.lastActivityTime=Date.now(),!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=W(),r=e.fileSize,n=Q(s.browser);if(r>n){let o=j(s.browser),l=(r/(1024*1024*1024)).toFixed(2);throw new Error(`FILE_TOO_LARGE: ${l}GB file exceeds ${s.browser}'s ${o} limit. Use a Chromium browser (Chrome, Edge, Brave) for unlimited file sizes.`)}let a={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,fileBuffer:null,pieceBuffers:new Map,paused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return a.streamingHandler||this._initReceiveBuffer(a,e),this.transfers.set(t,a),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(a)}}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._requestNextPieces(t)}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<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"};let r=i.byteLength;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"};let o=e===s.manifest.totalPieces-1?1:Math.floor(n*.5);if(this._checkRateLimit(t),this._clearPieceTimeout(t,e),e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:this._calculateProgress(s)};try{if(s.lastActivityTime=Date.now(),s.streamingHandler){let f=await s.streamingHandler.writeChunk(t,e,new Uint8Array(i));if(f===!1||f&&f.success===!1)return{success:!1,error:f&&f.error||"Failed to write piece to streaming handler",retryable:!0,pieceIndex:e}}else this._storePieceInMemory(s,e,i);if(e===s.receivedWaterLevel+1)for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;else{if(!!!s.streamingHandler&&s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.bytesTransferred+=r;let l=this._calculateProgress(s);return this._checkProgressMilestones(t,s,l),this._emitProgress(t,s),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(s.paused||this._requestNextPieces(t),{success:!0,progress:this._calculateProgress(s),piecesReceived:s.piecesReceivedCount,totalPieces:s.manifest.totalPieces})}catch(l){return{success:!1,error:l.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);try{return e.streamingHandler?(await e.streamingHandler.completeStream(t),{success:!0,streaming:!0,manifest:e.manifest}):{success:!0,blob:this._assembleFile(e),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}))}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.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 w of e.channels)if(w.readyState==="open"&&w.bufferedAmount<=this.config.highWaterMark){h=!0;break}if(!h)return}let a=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(a<=0)return;let o=[];for(;e.retryQueue.length>0&&o.length<a;){let h=e.retryQueue.shift();e.inFlightPieces.has(h)||o.push(h)}for(;e.nextPieceToSend<e.manifest.totalPieces&&o.length<a;){let h=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(h)||o.push(h)}if(o.length===0)return;let l=Date.now();for(let h of o)e.inFlightPieces.set(h,l);let f=r?4:4*e.channels.length;try{for(let h=0;h<o.length;h+=f){let P=o.slice(h,h+f).map(async y=>{let p=await this._readPiece(e.file,e.manifest,y);return{pieceIndex:y,data:p}}),b=await Promise.all(P);if(r)for(let{pieceIndex:y,data:p}of b){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,y,p)),e.lastActivityTime=Date.now(),this._startPieceTimeout(t,y)}else for(let{pieceIndex:y,data:p}of b){let m=null,u=-1;for(let S=0;S<e.channels.length;S++){let d=(e.channelIndex+S)%e.channels.length,g=e.channels[d];if(g.readyState==="open"&&g.bufferedAmount<=this.config.highWaterMark){m=g,u=d,e.channelIndex=(d+1)%e.channels.length;break}}if(!m)return;m.send(this._encodePieceMessage(t,y,p)),e.inFlightPieces.set(y,{sendTime:Date.now(),channelIndex:u}),e.lastActivityTime=Date.now(),this._startPieceTimeout(t,y)}}}catch(h){for(let w of o)e.inFlightPieces.delete(w),e.retryQueue.push(w);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.forEach(i=>clearTimeout(i)),e.pieceTimeouts.clear()),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.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;if(i>=this.config.maxInFlightPieces)return;let s=e.channel;if(!s||s.readyState!=="open")return;let r=s.bufferedAmount,n=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?n=32:r>this.config.minBufferedAmount?n=this.config.maxPiecesPerTick:n=this.config.burstPiecesPerTick;let a=this.config.maxPendingReads-e.pendingReads;if(a<=0)return;n=Math.min(n,a);let o=this.config.maxInFlightPieces-i;n=Math.min(n,o);let l=[];for(;e.retryQueue.length>0&&l.length<n;){let h=e.retryQueue.shift();e.inFlightPieces.has(h)||l.push(h)}for(;e.nextPieceToSend<e.manifest.totalPieces&&l.length<n;){let h=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(h)||l.push(h)}if(l.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=l.length;let f=Date.now();for(let h of l)e.inFlightPieces.set(h,f);this._readAndSendPieces(t,e,s,l)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(a=>this._readPiece(e.file,e.manifest,a).then(o=>({pieceIndex:a,data:o})).catch(o=>({pieceIndex:a,error:o}))),n=await Promise.all(r);for(let a of n){if(a.error){e.inFlightPieces.delete(a.pieceIndex),e.retryQueue.push(a.pieceIndex);continue}let{pieceIndex:o,data:l}=a,f=e.channel;if(!f||f.readyState!=="open"){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}if(f.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(o),e.retryQueue.unshift(o);continue}try{f.send(this._encodePieceMessage(t,o,l)),e.lastActivityTime=Date.now(),this._startPieceTimeout(t,o)}catch{e.inFlightPieces.delete(o),e.retryQueue.unshift(o)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let o=0;o<e.totalPieces;o++){let l=s.get(o);if(!l)throw new Error(`Missing piece ${o}`);n.push(l)}let a=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),a}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),a=await t.slice(s,r).arrayBuffer(),o=new Uint8Array(4+a.byteLength);return o.set(new Uint8Array(a),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){let i=Date.now();if(i-(e.lastProgressEmit||0)<100)return;let s=0,r=e.lastSpeedBytes||0,n=e.lastSpeedTime||e.startTime||i,a=i-n;if(a>=200){let f=e.bytesTransferred-r;s=a>0?f/a*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=i}else e.smoothedSpeed?s=e.smoothedSpeed:e.startTime&&i>e.startTime&&(s=e.bytesTransferred/(i-e.startTime)*1e3);s=Math.max(0,s),Number.isFinite(s)||(s=0);let o=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=s:s>0&&(e.smoothedSpeed=o*s+(1-o)*e.smoothedSpeed);let l=Math.round(e.smoothedSpeed);e.lastProgressEmit=i,this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),speed:l})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let a=s-e.lastMilestoneCheck.time,o=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(a>0&&a<r&&o>0)n=o/a*1e3/1e6;else if(a>=r){let l=e.startTime?s-e.startTime:0;n=l>0?e.bytesTransferred/l*1e3/1e6:0}}else{let a=e.startTime?s-e.startTime:0;n=a>0?e.bytesTransferred/a*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;let s=i.pieceTimeouts.get(e);s&&clearTimeout(s);let r=setTimeout(()=>{this._handlePieceTimeout(t,e)},this.config.pieceTimeout);i.pieceTimeouts.set(e,r)}_clearPieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;let s=i.pieceTimeouts.get(e);s&&(clearTimeout(s),i.pieceTimeouts.delete(e))}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="completing")return;if(i.type==="receive"&&i.receivedOutOfOrder&&(e<=i.receivedWaterLevel||i.receivedOutOfOrder.has(e))){i.pieceTimeouts.delete(e);return}let s=Date.now()-i.lastActivityTime,r=this.config.pieceTimeout*.5,a=i.manifest.totalPieces-i.piecesReceivedCount<=10;if(s<r&&!a){this._startPieceTimeout(t,e);return}let o=i.pieceRetries.get(e)||0;o<this.config.maxPieceRetries?(i.pieceRetries.set(e,o+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:o+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),this._startPieceTimeout(t,e)):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this._clearPieceTimeout(t,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=Date.now()-i.lastActivityTime;s>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:s}):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),e.pieceTimeouts.forEach(i=>clearTimeout(i)),e.pieceTimeouts.clear(),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 I="0.4.0",ye="https://perkoon.com",M=class extends we{constructor(t=ye,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new R,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await E(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=e.session?await this._joinSession(e.session,e.password,"sender",e.senderKey):await this._createSession(e.password,e.source);this.sessionCode=i.session_code,this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new T(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()}_${F.randomUUID().replace(/-/g,"").slice(0,9)}`,a=[{id:`file_${Date.now()}_${F.randomUUID().replace(/-/g,"").slice(0,9)}`,name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type}];this.signaling.sendFilesAdded(n,a,this.fileSource.size),this.emit("waiting-for-acceptance");let o=await this._waitForSignal("transfer_accepted",this.peerTimeout,m=>m.batch_id===n);this.emit("transfer-accepted",{batchId:o.batch_id});let l=`transfer_${Date.now()}_${F.randomUUID().replace(/-/g,"").slice(0,9)}`;this.transport=new v,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:m,mid:u})=>{this.signaling.sendIceCandidate(r,{candidate:m,sdpMid:u||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",m=>{let u=m.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 w=await this.transport.waitForChannel("control"),P=await this.transport.waitForChannel(`transfer-${l}`);this.emit("connected"),this.engine.initSend(l,this.fileSource,P);let b=this.engine.transfers.get(l).manifest;this._setupControlHandler(w,l);let y=JSON.stringify({type:"manifest",transfer_id:l,manifest:b,batch_id:n,batch_total_files:1,batch_total_size:this.fileSource.size});w.send(y),this._setupProgressEvents(l),await this.engine.startSend(l);let p=await this._waitForCompletion(l);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:p.speed,duration:p.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new T(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver");this.signaling.on("files_offered",p=>{this.emit("files-offered",{files:p.files,from:p.from}),this.signaling.channel.push("accept_transfer",{batch_id:p.batch_id})});let n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new v,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:p,mid:m})=>{this.signaling.sendIceCandidate(n,{candidate:p,sdpMid:m||"0",sdpMLineIndex:0})}),this.signaling.on("ice_candidate",p=>{let m=p.candidate;this.transport.addIceCandidate({candidate:m.candidate||m,mid:m.sdpMid||m.mid||"0"})});let a=await this._waitForSignal("offer"),o=await this.transport.handleOffer(a.offer);this.signaling.sendAnswer(n,o);let l=await this.transport.waitForChannel("control"),{channel:f,label:h}=await this.transport.waitForChannelByPrefix("transfer-");this.emit("connected"),this.fileSink=i.stdout?U():B(e,{overwrite:i.overwrite});let w=this.fileSink,P=[],y=await new Promise((p,m)=>{let u=null;l.addEventListener("message",async S=>{if(typeof S.data=="string")try{let d=JSON.parse(S.data);if(d.type==="manifest"&&!u){u=d.transfer_id,this._activeTransferId=u;let g=d.manifest;this.emit("receiving-file",{name:g.fileName,size:g.fileSize}),await w.startStreamingReceive(u,{fileName:g.fileName,size:g.fileSize,fileSize:g.fileSize,type:g.fileType,pieceSize:g.pieceSize},{}),this.engine.initReceive(u,g,{streamingHandler:w}),this._setupProgressEvents(u),this.engine.startReceive(u,_=>{this._safeSend(l,JSON.stringify({type:"request",transfer_id:u,piece_index:_}))});let C=-1;this._ackInterval=setInterval(()=>{let _=this.engine.transfers.get(u);if(!_||_.state==="completed"||_.state==="completing"){clearInterval(this._ackInterval);return}let k=_.receivedWaterLevel;if(k>C){C=k;let L=_.receivedOutOfOrder.size>0?Array.from(_.receivedOutOfOrder):void 0;this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:u,wl:k,ooo:L}))}},100)}}catch(d){d.message&&!d.message.includes("Unexpected token")&&m(d)}}),f.addEventListener("message",async S=>{let d=S.data;if(!u||!(d instanceof ArrayBuffer||Buffer.isBuffer(d)))return;let g=d instanceof ArrayBuffer?d:d.buffer.slice(d.byteOffset,d.byteOffset+d.byteLength),{pieceIndex:C,data:_}=this.engine.decodePieceMessage(g);if(C>=0&&_&&(await this.engine.handlePieceData(u,C,_))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let L=this.engine.transfers.get(u);L&&this._safeSend(l,JSON.stringify({type:"ack_wl",transfer_id:u,wl:L.receivedWaterLevel}));let G=w.getFilePath(u);P.push(G),await this.engine.completeReceive(u),this._safeSend(l,JSON.stringify({type:"complete",transfer_id:u}));let O=this.engine.transfers.get(u),x=Date.now()-(O?.startTime||Date.now()),D=O?.manifest;p({files:P,speed:D?D.fileSize/(x/1e3):0,duration:x})}})});return this.signaling.disconnect(),this.transport.close(),y}async _createSession(t,e="agent_cli"){let i=`${this.serverUrl}/api/v1/sessions`,s={client_version:I,source:e};t&&(s.password=t);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,i="receiver",s=void 0){let r=`${this.serverUrl}/api/v1/sessions/${t}/join`,n={role:i,client_version:I};e&&(n.password=e),s&&(n.sender_key=s);let a=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!a.ok){let o=await a.json().catch(()=>({}));throw a.status===404?new Error(`Session ${t} not found. Check the code and try again.`):a.status===410?new Error(`Session ${t} has expired.`):a.status===403?new Error(o.error||"Access denied"):new Error(`Failed to join session: ${o.error||a.statusText}`)}return a.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{s(),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,a),r(new Error(`Timed out waiting for ${t} signal`))},e),a=o=>{i&&!i(o)||(this.signaling.removeListener(t,a),clearTimeout(n),s(o))};this.signaling.on(t,a)})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let a=r>0?Math.round(n/r*100):0,o=Date.now()-(i.startTime||Date.now()),l=n*s.pieceSize,f=o>0?l/(o/1e3):0,h=f>0?Math.round((s.fileSize-l)/f):0;this.emit("progress",{transferId:t,percent:Math.min(a,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 a=Date.now()-n.startTime;e({speed:n.manifest.fileSize/(a/1e3),duration:a});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.fileSink&&this._activeTransferId&&(this.fileSink.abort(this._activeTransferId).catch(()=>{}),this.fileSink=null),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};export{v as NodeTransport,T as SignalingClient,M as TransferManager,B as createNodeFileSink,E as createNodeFileSource};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perkoon",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "File transfer for humans and the things replacing them. P2P CLI — send files directly between devices.",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "File transfer for humans and the things replacing them. P2P CLI — send files directly between devices via WebRTC. No upload, no size limit. Agent-friendly: --json NDJSON output, --source attribution, --server override for self-hosted, deterministic exit codes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/client.js",
|
|
7
7
|
"bin": {
|