perkoon 0.1.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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Perkoon Proprietary License
2
+
3
+ Copyright (c) 2026 MB "Perkunu simtas" (Perkoon)
4
+ All rights reserved.
5
+
6
+ This software is the proprietary property of MB "Perkunu simtas", registered
7
+ in Lithuania, EU. It is provided as a compiled package for end-user use only.
8
+
9
+ You MAY:
10
+ - Install and use this software for personal or commercial file transfers
11
+ - Use this software as a dependency in your own projects
12
+
13
+ You MAY NOT:
14
+ - Modify, reverse-engineer, decompile, or disassemble this software
15
+ - Redistribute, sublicense, or sell copies of this software
16
+ - Remove or alter any proprietary notices
17
+
18
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ Contact: info@perkoon.com
27
+ Website: https://perkoon.com
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # perkoon
2
+
3
+ Fast P2P file transfer. No cloud. No accounts. Files go directly between devices using WebRTC.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install -g perkoon
9
+ ```
10
+
11
+ Or use without installing:
12
+
13
+ ```
14
+ npx perkoon send file.zip
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ **Send a file:**
20
+
21
+ ```
22
+ $ perkoon send report.pdf
23
+
24
+ ✓ report.pdf (2.5 MB)
25
+ ✓ Code: K7MX4QPR9W2N
26
+
27
+ Receiver command: perkoon receive K7MX4QPR9W2N
28
+ Or open in browser: https://perkoon.com/K7MX4QPR9W2N
29
+
30
+ Waiting for receiver...
31
+ ✓ Receiver connected
32
+ ✓ Direct connection established
33
+ ✓ Complete: 2.5 MB in 0.3s (8.3 MB/s)
34
+ ```
35
+
36
+ **Receive a file:**
37
+
38
+ ```
39
+ $ perkoon receive K7MX4QPR9W2N
40
+
41
+ ✓ Joined session K7MX4QPR9W2N
42
+ ✓ Direct connection established
43
+ ✓ Saved: ./received/report.pdf
44
+ ✓ Complete: 0.3s (8.3 MB/s)
45
+ ```
46
+
47
+ **Or receive in the browser** — open the link shown by the sender. No install needed on the receiving end.
48
+
49
+ ## Options
50
+
51
+ ```
52
+ --password <pw> Protect session with a password
53
+ --timeout <sec> Wait time for peer (default: 300)
54
+ --output <dir> Where to save files (default: ./received)
55
+ --output - Write received file to stdout
56
+ --overwrite Replace existing files
57
+ --json Machine-readable JSON output
58
+ --quiet No progress output
59
+ ```
60
+
61
+ ## JSON mode
62
+
63
+ For automation and AI agents, `--json` outputs structured events to stdout:
64
+
65
+ ```
66
+ $ perkoon send file.zip --json --quiet
67
+
68
+ {"event":"file_ready","name":"file.zip","size":1048576}
69
+ {"event":"session_created","session_code":"K7MX4QPR9W2N","share_url":"https://perkoon.com/K7MX4QPR9W2N"}
70
+ {"event":"waiting_for_receiver"}
71
+ {"event":"receiver_connected"}
72
+ {"event":"webrtc_connected"}
73
+ {"event":"progress","percent":50,"speed":5242880,"eta":1,"bytes_transferred":524288}
74
+ {"event":"transfer_complete","session_code":"K7MX4QPR9W2N","duration_ms":2000,"speed":5242880}
75
+ ```
76
+
77
+ ## Exit codes
78
+
79
+ | Code | Meaning |
80
+ |------|---------|
81
+ | 0 | Success |
82
+ | 1 | Usage error (bad arguments) |
83
+ | 2 | File error (not found, not a file) |
84
+ | 3 | Network/session error |
85
+ | 4 | Auth error (wrong password, access denied) |
86
+ | 5 | Timeout (no peer connected) |
87
+
88
+ ## How it works
89
+
90
+ Files transfer directly between devices using WebRTC data channels. The perkoon.com server handles signaling only — your data never touches our servers.
91
+
92
+ 1. Sender creates a session and gets a 12-character code
93
+ 2. Receiver joins with that code (CLI or browser)
94
+ 3. WebRTC peer connection is established
95
+ 4. File data flows directly between the two devices
96
+
97
+ ## Requirements
98
+
99
+ - Node.js >= 18
100
+
101
+ ## Links
102
+
103
+ - Website: https://perkoon.com
104
+ - Send files from the browser: https://perkoon.com
105
+
106
+ ## License
107
+
108
+ Proprietary. See [LICENSE](./LICENSE) for details.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import{EventEmitter as Te}from"node:events";import Se from"node:crypto";import ae from"node-datachannel";import{EventEmitter as ce}from"node:events";var{PeerConnection:le}=ae,z=class extends ce{constructor(){super(),this.pc=null,this.dataChannel=null,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let i={iceServers:e.map(s=>(Array.isArray(s.urls)?s.urls:[s.urls]).map(n=>s.username&&s.credential?`${n}:${s.username}:${s.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new le(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{this.dataChannel=this._wrapChannel(s),this.emit("data-channel",this.dataChannel)})}async createOffer(){return new Promise(t=>{this.pc.onLocalDescription((i,s)=>{t({sdp:i,type:s})});let e=this.pc.createDataChannel("transfer",{ordered:!1,maxRetransmits:0});this.dataChannel=this._wrapChannel(e)})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((i,s)=>{e({sdp:i,type:s})}),this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()})}handleAnswer(t){this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()}addIceCandidate(t){t.candidate&&(this.remoteDescriptionSet?this.pc.addRemoteCandidate(t.candidate,t.mid||"0"):this.pendingCandidates.push(t))}_flushPendingCandidates(){for(let t of this.pendingCandidates)this.pc.addRemoteCandidate(t.candidate,t.mid||"0");this.pendingCandidates=[]}getDataChannel(){return this.dataChannel}async waitForDataChannel(t=3e4){return this.dataChannel?.readyState==="open"?this.dataChannel:new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Data channel open timeout"))},t),r=n=>{n.readyState==="open"?(clearTimeout(s),e(n)):n.addEventListener("open",()=>{clearTimeout(s),e(n)})};this.dataChannel?r(this.dataChannel):this.once("data-channel",n=>{r(n)})})}close(){if(this.dataChannel){try{this.dataChannel.close()}catch{}this.dataChannel=null}if(this.pc){try{this.pc.close()}catch{}this.pc=null}}_wrapChannel(t){let e=new Map,i="connecting";try{t.isOpen&&t.isOpen()&&(i="open")}catch{}let s={get readyState(){return i},get bufferedAmount(){try{return t.bufferedAmount()||0}catch{return 0}},send(n){n instanceof ArrayBuffer?t.sendMessageBinary(Buffer.from(n)):n instanceof Uint8Array?t.sendMessageBinary(Buffer.from(n.buffer,n.byteOffset,n.byteLength)):Buffer.isBuffer(n)?t.sendMessageBinary(n):t.sendMessage(String(n))},close(){i="closed";try{t.close()}catch{}},addEventListener(n,o){e.has(n)||e.set(n,new Set),e.get(n).add(o)},removeEventListener(n,o){let a=e.get(n);a&&a.delete(o)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let o;Buffer.isBuffer(n)?o=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):o=n,r("message",{data:o})});function r(n,o){let a=e.get(n);if(a)for(let c of a)try{c(o)}catch{}}return s}};import{Socket as ue}from"phoenix";import fe from"ws";import{EventEmitter as he}from"node:events";var M=class extends he{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 ue(r,{params:{},transport:fe,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(a=>{this.connected||o(new Error(`Signaling connection failed: ${a?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",a=>{this.emit(a.type,a)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",a=>{this.connected=!0,this.peers=a.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",a=>{o(new Error(`Channel join failed: ${a.reason||"unknown"}`))}).receive("timeout",()=>{o(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}disconnect(){if(this._stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connected=!1}_startHeartbeat(){this._stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.connected&&this.channel&&this.channel.push("heartbeat",{})},1e4)}_stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}};import{open as de}from"node:fs/promises";import pe from"node:path";import{lookup as me}from"mime-types";async function K(l){let t=await de(l,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${l}`);return{name:pe.basename(l),size:e.size,type:me(l)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:o}=await t.read(n,0,r,i);return o===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+o)},async close(){await t.close()}}}import{open as ge,mkdir as ye}from"node:fs/promises";import we from"node:path";function V(l,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await ye(l,{recursive:!0});let n=_e(s.fileName),o=we.join(l,n);if(!t.overwrite)try{let{access:c}=await import("node:fs/promises");throw await c(o),new Error(`File already exists: ${o} (use --overwrite to replace)`)}catch(c){if(c.code!=="ENOENT")throw c}let a=await ge(o,"w");s.size>0&&await a.truncate(s.size),e.set(i,{fh:a,filePath:o,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let o=s*n.pieceSize,a=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(a,0,a.byteLength,o),{success:!0}}catch(c){return{success:!1,error:c.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}function _e(l){return l.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function Z(){let l=new Map;return{async startStreamingReceive(t,e,i){l.set(t,{pieceSize:e.pieceSize,totalPieces:Math.ceil(e.size/e.pieceSize),size:e.size,nextPiece:0,buffer:new Map,bytesWritten:0})},async writeChunk(t,e,i){let s=l.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let o=s.nextPiece*s.pieceSize,a=s.size-o,c=a<n.byteLength?n.subarray(0,a):n;process.stdout.write(c),s.bytesWritten+=c.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){l.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){l.delete(t)}}}var $=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=o=>{r&&clearTimeout(r),i(o)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var Pe={pieceSize:84*1024,maxConcurrentPieces:8,pieceTimeout:3e4,idleTimeout:6e4,maxPieceRetries:3,maxRetryQueueSize:1e4,maxInFlightMultiplier:10,highWaterMark:16*1024*1024,lowWaterMark:4*1024*1024,maxPiecesPerSecond:5e3,rateLimitWindowMs:1e3,channelRotationThreshold:512*1024*1024,channelRotationEnabled:!1,parallelChannels:1,parallelChannelsEnabled:!1,parallelChannelsThreshold:100*1024*1024,pacingEnabled:!0,pacingThreshold:0,pacingIntervalMs:10,targetBufferedAmount:8*1024*1024,minBufferedAmount:4*1024*1024,maxBufferedAmount:16*1024*1024,maxPiecesPerTick:32,burstPiecesPerTick:64,maxPendingReads:512,maxInFlightPieces:4e3},D=class extends ${constructor(t={}){super(),this.config={...Pe,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let o=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=o,this._checkProgressMilestones(t,i,o),this._emitProgress(t,i,o),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let o=i.inFlightPieces.get(n);if(!o)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let a=o.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=a,r+=a;let c=o.channelIndex>=0?o.channelIndex:0;i.channelBytes[c]!==void 0&&(i.channelBytes[c]+=a)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let a of i)if(typeof a!="number"||a<0||s.manifest&&a>=s.manifest.totalPieces)return}let r=Date.now(),n=0,o=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let a=[];for(let[c,f]of s.inFlightPieces)c<=e&&a.push([c,f]);for(let[c,f]of a){s.inFlightPieces.delete(c);let u=f.pieceSize||this._getPieceSize(s.manifest,c);s.bytesTransferred+=u,n+=u,o++,s.pieceTimeouts.delete(c)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let a of i){let c=s.inFlightPieces.get(a);if(c){s.inFlightPieces.delete(a);let f=c.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 c=s.receivedWaterLevel+1,f=Math.min(c+10,e);for(let u=c;u<f;u++)!s.receivedOutOfOrder.has(u)&&!s.piecesRequested.has(u)&&(s.piecesRequested.add(u),s.requestPiece(u))}}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 u=!1;for(let m of e.channels)if(m.readyState==="open"&&m.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 a=[];for(;e.retryQueue.length>0&&a.length<o;){let u=e.retryQueue.shift();e.inFlightPieces.has(u)||a.push(u)}for(;e.nextPieceToSend<e.manifest.totalPieces&&a.length<o;){let u=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(u)||a.push(u)}if(a.length===0)return;let c=Date.now();for(let u of a)e.inFlightPieces.set(u,{sendTime:c,pieceSize:this._getPieceSize(e.manifest,u)});let f=r?4:4*e.channels.length;try{for(let u=0;u<a.length;u+=f){let y=a.slice(u,u+f).map(async g=>{let h=await this._readPiece(e.file,e.manifest,g);return{pieceIndex:g,data:h}}),p=await Promise.all(y);if(r)for(let{pieceIndex:g,data:h}of p){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,g,h)),this._startPieceTimeout(t,g)}else for(let{pieceIndex:g,data:h}of p){let F=null,T=-1;for(let k=0;k<e.channels.length;k++){let w=(e.channelIndex+k)%e.channels.length,C=e.channels[w];if(C.readyState==="open"&&C.bufferedAmount<=this.config.highWaterMark){F=C,T=w,e.channelIndex=(w+1)%e.channels.length;break}}if(!F)return;F.send(this._encodePieceMessage(t,g,h)),e.inFlightPieces.set(g,{sendTime:Date.now(),channelIndex:T,pieceSize:this._getPieceSize(e.manifest,g)}),this._startPieceTimeout(t,g)}}}catch(u){for(let m of a)e.inFlightPieces.delete(m),e.retryQueue.push(m);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.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,o=this.config.maxInFlightPieces*.99,a=this.config.maxPendingReads*.95,c=r<2*1024*1024,f=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=o||i>=n&&c?"RECEIVER_SLOW":e.pendingReads>=a?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:f})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let u=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?u=32:r>this.config.minBufferedAmount?u=this.config.maxPiecesPerTick:u=this.config.burstPiecesPerTick;let m=this.config.maxPendingReads-e.pendingReads;if(m<=0)return;u=Math.min(u,m);let y=this.config.maxInFlightPieces-i;u=Math.min(u,y);let p=[];for(;e.retryQueue.length>0&&p.length<u;){let h=e.retryQueue.shift();e.inFlightPieces.has(h)||p.push(h)}for(;e.nextPieceToSend<e.manifest.totalPieces&&p.length<u;){let h=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(h)||p.push(h)}if(p.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=p.length;let g=Date.now();for(let h of p)e.inFlightPieces.set(h,{sendTime:g,pieceSize:this._getPieceSize(e.manifest,h)});this._readAndSendPieces(t,e,s,p)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(o=>this._readPiece(e.file,e.manifest,o).then(a=>({pieceIndex:o,data:a})).catch(a=>({pieceIndex:o,error:a}))),n=await Promise.all(r);for(let o of n){if(o.error){e.inFlightPieces.delete(o.pieceIndex),e.retryQueue.push(o.pieceIndex);continue}let{pieceIndex:a,data:c}=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,c)),this._startPieceTimeout(t,a)}catch{e.inFlightPieces.delete(a),e.retryQueue.push(a)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let a=0;a<e.totalPieces;a++){let c=s.get(a);if(!c)throw new Error(`Missing piece ${a}`);n.push(c)}let o=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),o}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),o=new Uint8Array(4+n.byteLength);return o.set(new Uint8Array(n),4),o}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,o=e.lastSpeedTime||e.startTime||s,a=s-o;if(a>=200){let m=e.bytesTransferred-n;r=a>0?m/a*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=c*r+(1-c)*e.smoothedSpeed);let f=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let u=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:u,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 c=e.startTime?s-e.startTime:0;n=c>0?e.bytesTransferred/c*1e3/1e6:0}}else{let o=e.startTime?s-e.startTime:0;n=o>0?e.bytesTransferred/o*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,a=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,c=n<r&&!a,f=[];for(let[y,p]of e.pieceTimeouts)if(i-p>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(y<=e.receivedWaterLevel||e.receivedOutOfOrder.has(y))){e.pieceTimeouts.delete(y);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&y<=e.ackedWaterLevel){e.pieceTimeouts.delete(y);continue}f.push(y)}if(f.length===0||c)return;let m=f.slice(0,50);for(let y of m)this._handlePieceTimeout(t,y)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),o=Date.now()-n;o>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:o}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var ve="https://perkoon.com",O=class extends Te{constructor(t=ve,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new D,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await K(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=await this._createSession(e.name,e.password);this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new M(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r}),this.transport=new z,this.transport.initialize(i.peer_id,i.ice_servers);let n=await this.transport.createOffer();this.signaling.sendOffer(r,n),this.transport.on("ice-candidate",({candidate:m,mid:y})=>{this.signaling.sendIceCandidate(r,{candidate:m,mid:y})}),this.signaling.on("ice_candidate",m=>{this.transport.addIceCandidate(m.candidate)});let o=await this._waitForSignal("answer");this.transport.handleAnswer(o.answer);let a=await this.transport.waitForDataChannel();this.emit("connected");let c=Se.randomUUID();this.engine.initSend(c,this.fileSource,a);let f=this.engine.transfers.get(c).manifest;this._setupControlHandler(a,c),a.send(JSON.stringify({type:"manifest",transfer_id:c,manifest:f})),this._setupProgressEvents(c),await this.engine.startSend(c);let u=await this._waitForCompletion(c);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"),n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new z,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:p,mid:g})=>{this.signaling.sendIceCandidate(n,{candidate:p,mid:g})}),this.signaling.on("ice_candidate",p=>{this.transport.addIceCandidate(p.candidate)});let o=await this._waitForSignal("offer"),a=await this.transport.handleOffer(o.offer);this.signaling.sendAnswer(n,a);let c=await this.transport.waitForDataChannel();this.emit("connected");let f=i.stdout?Z():V(e,{overwrite:i.overwrite}),u=[],y=await new Promise((p,g)=>{let h=null;c.addEventListener("message",async F=>{let T=F.data;if(typeof T=="string"){try{let k=JSON.parse(T);if(k.type==="manifest"){h=k.transfer_id;let w=k.manifest;this.emit("receiving-file",{name:w.fileName,size:w.fileSize}),await f.startStreamingReceive(h,{fileName:w.fileName,size:w.fileSize,fileSize:w.fileSize,type:w.fileType,pieceSize:w.pieceSize},{}),this.engine.initReceive(h,w,{streamingHandler:f}),this._setupProgressEvents(h),this.engine.startReceive(h,b=>{c.send(JSON.stringify({type:"request",transfer_id:h,piece_index:b}))});let C=-1;this._ackInterval=setInterval(()=>{let b=this.engine.transfers.get(h);if(!b||b.state==="completed"||b.state==="completing"){clearInterval(this._ackInterval);return}let L=b.receivedWaterLevel;if(L>C){C=L;let q=b.receivedOutOfOrder.size>0?Array.from(b.receivedOutOfOrder):void 0;c.send(JSON.stringify({type:"water_level_ack",transfer_id:h,water_level:L,out_of_order:q}))}},100)}}catch{}return}if(h&&(T instanceof ArrayBuffer||Buffer.isBuffer(T))){let k=T instanceof ArrayBuffer?T:T.buffer.slice(T.byteOffset,T.byteOffset+T.byteLength),{pieceIndex:w,data:C}=this.engine.decodePieceMessage(k);if(w>=0&&C&&(await this.engine.handlePieceData(h,w,C))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let L=this.engine.transfers.get(h);L&&c.send(JSON.stringify({type:"water_level_ack",transfer_id:h,water_level:L.receivedWaterLevel}));let q=f.getFilePath(h);u.push(q),await this.engine.completeReceive(h),c.send(JSON.stringify({type:"complete",transfer_id:h}));let G=this.engine.transfers.get(h),J=Date.now()-(G?.startTime||Date.now()),X=G?.manifest;p({files:u,speed:X?X.fileSize/(J/1e3):0,duration:J})}}}),setTimeout(()=>{g(new Error("Transfer timed out (10 minutes)"))},6e5)});return this.signaling.disconnect(),this.transport.close(),y}async _createSession(t,e){let i=`${this.serverUrl}/api/v1/sessions`,s={};t&&(s.name=t),e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${n.error||r.statusText}`)}return r.json()}async _joinSession(t,e){let i=`${this.serverUrl}/api/v1/sessions/${t}/join`,s={role:"receiver"};e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw r.status===404?new Error(`Session ${t} not found. Check the code and try again.`):r.status===410?new Error(`Session ${t} has expired.`):r.status===403?new Error(n.error||"Access denied"):new Error(`Failed to join session: ${n.error||r.statusText}`)}return r.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),s=r=>{(r.type==="ready"||r.type==="offer")&&(clearTimeout(i),this.signaling.removeListener("ready",s),this.signaling.removeListener("offer",s),t(r.from))};this.signaling.on("ready",s),this.signaling.on("offer",s)})}_waitForSignal(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error(`Timed out waiting for ${t} signal`))},3e4);this.signaling.once(t,r=>{clearTimeout(s),e(r)})})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let o=r>0?Math.round(n/r*100):0,a=Date.now()-(i.startTime||Date.now()),c=n*s.pieceSize,f=a>0?c/(a/1e3):0,u=f>0?Math.round((s.fileSize-c)/f):0;this.emit("progress",{transferId:t,percent:Math.min(o,100),speed:f,eta:u,bytesTransferred:c,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"request":this.engine.requeuePiece(e,s.piece_index);break;case"complete":{let r=this.engine.transfers.get(e);r&&r.state!=="completed"&&(r.piecesAckedCount=r.manifest.totalPieces,r.bytesTransferred=r.manifest.fileSize,r.inFlightPieces.clear(),r.state="completed")}this.emit("transfer-complete",{transferId:e});break;case"backpressure":s.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Transfer timed out (10 minutes)"))},6e5),r=()=>{let o=this.engine.transfers.get(t);if(!o){clearTimeout(s),i(new Error("Transfer context lost"));return}if(o.state==="completed"){clearTimeout(s);let a=Date.now()-o.startTime;e({speed:o.manifest.fileSize/(a/1e3),duration:a});return}if(o.state==="cancelled"||o.state==="failed"){clearTimeout(s),i(new Error("Transfer failed"));return}};this.on("transfer-complete",({transferId:o})=>{o===t&&r()});let n=setInterval(()=>{r();let o=this.engine.transfers.get(t);(!o||o.state==="completed"||o.state==="cancelled")&&clearInterval(n)},500)})}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};import ke from"node:path";import{access as Ce,stat as be}from"node:fs/promises";var Y="0.1.0",se="https://perkoon.com",ie=0,R=1,ee=2,H=3,Ae=4,Ee=5,A=process.argv.slice(2),te=A[0],P={};for(let l=1;l<A.length;l++)if(A[l].startsWith("--")){let t=A[l].slice(2);A[l+1]&&!A[l+1].startsWith("--")?P[t]=A[++l]:P[t]=!0}else P._positional||(P._positional=A[l]);var E=se,B=P.json===!0,N=P.quiet===!0,Re=P.overwrite===!0,j=P.output||"./received",x=typeof P.password=="string"?P.password:void 0,Q=typeof P.timeout=="string"?parseInt(P.timeout,10):NaN,I=!isNaN(Q)&&Q>0?Q:300;B&&j==="-"&&(process.stderr.write(` Error: --json and --output - are mutually exclusive
3
+ `),process.exit(R));function d(l){!N&&!B&&process.stderr.write(l+`
4
+ `)}function _(l,t={}){B&&process.stdout.write(JSON.stringify({event:l,...t})+`
5
+ `)}function S(l){d(` \u2713 ${l}`)}function W(l){return l>=1024*1024*1024?`${(l/(1024*1024*1024)).toFixed(1)} GB`:l>=1024*1024?`${(l/(1024*1024)).toFixed(1)} MB`:l>=1024?`${(l/1024).toFixed(0)} KB`:`${l} B`}function U(l){return W(l)+"/s"}function ne(l){return l<=0?"...":l<60?`${l}s`:`${Math.floor(l/60)}m ${l%60}s`}function re(l,t=30){let e=Math.round(t*l/100);return"\u2588".repeat(e)+"\u2591".repeat(t-e)}function v(l,t){d(`
6
+ Error: ${l}`),t&&d(`
7
+ ${t}`),d("")}function oe(l){let t=l.message||"";return t.includes("No peer connected")||t.includes("timed out")?Ee:t.includes("Password required")||t.includes("Invalid password")||t.includes("Access denied")?Ae:(t.includes("Failed to create session")||t.includes("Failed to join")||t.includes("not found")||t.includes("expired"),H)}async function Be(){let l=P._positional;l||(v("No file specified.","Usage: perkoon send <file>"),process.exit(R));let t=ke.resolve(l);try{await Ce(t),(await be(t)).isFile()||(v(`Not a regular file: ${t}`),process.exit(ee))}catch{v(`File not found: ${t}`),process.exit(ee)}let e=new O(E,{timeout:I*1e3});e.on("file-ready",({name:r,size:n})=>{d(""),S(`${r} (${W(n)})`),_("file_ready",{name:r,size:n})}),e.on("session-created",({sessionCode:r})=>{S(`Code: ${r}`),x&&S("Password protected"),d("");let n=x?` --password ${x}`:"";d(` Receiver command: perkoon receive ${r}${n}`),d(` Or open in browser: ${E}/${r}`),d(""),_("session_created",{session_code:r,share_url:`${E}/${r}`})}),e.on("waiting-for-receiver",()=>{d(" Waiting for receiver..."),_("waiting_for_receiver")}),e.on("receiver-connected",()=>{S("Receiver connected"),_("receiver_connected")}),e.on("connected",()=>{S("Direct connection established"),d(""),_("webrtc_connected")});let i="";e.on("progress",({percent:r,speed:n,eta:o,bytesTransferred:a})=>{if(B)_("progress",{percent:r,speed:Math.round(n),eta:o,bytes_transferred:a});else if(!N){let c=` ${re(r)} ${String(r).padStart(3)}% ${U(n).padStart(10)} ETA ${ne(o)}`;c!==i&&(process.stderr.write(`\r${c}`),i=c)}});let s=()=>{d(`
8
+
9
+ Cancelled.`),e.destroy(),process.exit(R)};process.on("SIGINT",s),process.on("SIGTERM",s);try{let r=await e.send(t,{password:x});if(!N&&!B){process.stderr.write(`
10
+ `);let n=W(r.speed*(r.duration/1e3));S(`Complete: ${n} in ${(r.duration/1e3).toFixed(1)}s (${U(r.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}_("transfer_complete",{session_code:r.sessionCode,duration_ms:r.duration,speed:Math.round(r.speed)}),process.exit(ie)}catch(r){let n=oe(r);r.message.includes("No peer connected")?v(`No receiver joined after ${I}s.`,`Make sure they entered the right code.
11
+ Or share the link: ${E}/${e.sessionCode||""}`):r.message.includes("Failed to create session")?v("Could not reach perkoon.com","Check your internet connection and try again."):v(r.message),_("error",{message:r.message,exit_code:n}),e.destroy(),process.exit(n)}}async function Le(){let l=P._positional;l||(v("No session code specified.","Usage: perkoon receive <code>"),process.exit(R)),/^[A-Za-z0-9]{12}$/i.test(l)||(v(`Invalid code: ${l}`,"Codes are 12 alphanumeric characters, like K7MX4QPR9W2N."),process.exit(R));let t=j==="-",e=l.toUpperCase(),i=new O(E,{timeout:I*1e3});i.on("session-joined",()=>{d(""),S(`Joined session ${e}`),_("session_joined",{session_code:e})}),i.on("sender-found",()=>{S("Sender found"),_("sender_found")}),i.on("connected",()=>{S("Direct connection established"),_("webrtc_connected")}),i.on("receiving-file",({name:n,size:o})=>{S(`Receiving: ${n} (${W(o)})`),d(""),_("receiving_file",{name:n,size:o})});let s="";i.on("progress",({percent:n,speed:o,eta:a,bytesTransferred:c})=>{if(B)_("progress",{percent:n,speed:Math.round(o),eta:a,bytes_transferred:c});else if(!N){let f=` ${re(n)} ${String(n).padStart(3)}% ${U(o).padStart(10)} ETA ${ne(a)}`;f!==s&&(process.stderr.write(`\r${f}`),s=f)}});let r=()=>{d(`
12
+
13
+ Cancelled.`),i.destroy(),process.exit(R)};process.on("SIGINT",r),process.on("SIGTERM",r);try{let n={overwrite:Re,password:x};t&&(n.stdout=!0);let o=await i.receive(e,t?null:j,n);if(!N&&!B){if(process.stderr.write(`
14
+ `),!t)for(let a of o.files)S(`Saved: ${a}`);S(`Complete: ${(o.duration/1e3).toFixed(1)}s (${U(o.speed)})`),d(""),d(` Tip: Send files from your browser at ${E}`),d("")}_("transfer_complete",{files:o.files,duration_ms:o.duration,speed:Math.round(o.speed)}),process.exit(ie)}catch(n){let o=oe(n);n.message.includes("not found")?v(`Session not found (${e})`,`The code may be expired or mistyped.
15
+ Ask the sender for a new code.`):n.message.includes("expired")?v(`Session expired (${e})`,"Ask the sender to create a new session."):v(n.message),_("error",{message:n.message,exit_code:o}),i.destroy(),process.exit(o)}}switch(te){case"send":Be();break;case"receive":Le();break;case"--version":case"-v":console.log(`perkoon v${Y}`);break;case"--help":case"-h":case void 0:console.log(`
16
+ perkoon v${Y} \u2014 Fast P2P file transfer. No cloud. No accounts.
17
+
18
+ Usage:
19
+ perkoon send <file> Send a file
20
+ perkoon receive <code> Receive files
21
+
22
+ Quick start:
23
+ $ perkoon send report.pdf # shows a code
24
+ $ perkoon receive K7MX4QPR9W2N # on the other machine
25
+
26
+ Options:
27
+ --password <pw> Protect session with a password
28
+ --timeout <sec> Wait time for peer (default: 300)
29
+ --output <dir> Where to save files (default: ./received)
30
+ --output - Write received file to stdout
31
+ --overwrite Replace existing files
32
+ --json Machine-readable JSON output
33
+ --quiet No progress output
34
+
35
+ Exit codes:
36
+ 0 Success
37
+ 1 Usage error
38
+ 2 File error (not found, not a file)
39
+ 3 Network/session error
40
+ 4 Auth error (wrong password, access denied)
41
+ 5 Timeout (no peer connected)
42
+
43
+ Learn more: ${se}
44
+ `);break;default:v(`Unknown command: ${te}`,"Run perkoon --help for usage."),process.exit(R)}
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ import{EventEmitter as X}from"node:events";import Z from"node:crypto";import D from"node-datachannel";import{EventEmitter as N}from"node:events";var{PeerConnection:x}=D,v=class extends N{constructor(){super(),this.pc=null,this.dataChannel=null,this.remoteDescriptionSet=!1,this.pendingCandidates=[]}initialize(t,e){let i={iceServers:e.map(s=>(Array.isArray(s.urls)?s.urls:[s.urls]).map(n=>s.username&&s.credential?`${n}:${s.username}:${s.credential}`:n).join(",")),maxMessageSize:262144};this.pc=new x(t,i),this.pc.onLocalCandidate((s,r)=>{this.emit("ice-candidate",{candidate:s,mid:r})}),this.pc.onStateChange(s=>{this.emit("connection-state",s)}),this.pc.onGatheringStateChange(s=>{this.emit("gathering-state",s)}),this.pc.onDataChannel(s=>{this.dataChannel=this._wrapChannel(s),this.emit("data-channel",this.dataChannel)})}async createOffer(){return new Promise(t=>{this.pc.onLocalDescription((i,s)=>{t({sdp:i,type:s})});let e=this.pc.createDataChannel("transfer",{ordered:!1,maxRetransmits:0});this.dataChannel=this._wrapChannel(e)})}async handleOffer(t){return new Promise(e=>{this.pc.onLocalDescription((i,s)=>{e({sdp:i,type:s})}),this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()})}handleAnswer(t){this.pc.setRemoteDescription(t.sdp,t.type),this.remoteDescriptionSet=!0,this._flushPendingCandidates()}addIceCandidate(t){t.candidate&&(this.remoteDescriptionSet?this.pc.addRemoteCandidate(t.candidate,t.mid||"0"):this.pendingCandidates.push(t))}_flushPendingCandidates(){for(let t of this.pendingCandidates)this.pc.addRemoteCandidate(t.candidate,t.mid||"0");this.pendingCandidates=[]}getDataChannel(){return this.dataChannel}async waitForDataChannel(t=3e4){return this.dataChannel?.readyState==="open"?this.dataChannel:new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Data channel open timeout"))},t),r=n=>{n.readyState==="open"?(clearTimeout(s),e(n)):n.addEventListener("open",()=>{clearTimeout(s),e(n)})};this.dataChannel?r(this.dataChannel):this.once("data-channel",n=>{r(n)})})}close(){if(this.dataChannel){try{this.dataChannel.close()}catch{}this.dataChannel=null}if(this.pc){try{this.pc.close()}catch{}this.pc=null}}_wrapChannel(t){let e=new Map,i="connecting";try{t.isOpen&&t.isOpen()&&(i="open")}catch{}let s={get readyState(){return i},get bufferedAmount(){try{return t.bufferedAmount()||0}catch{return 0}},send(n){n instanceof ArrayBuffer?t.sendMessageBinary(Buffer.from(n)):n instanceof Uint8Array?t.sendMessageBinary(Buffer.from(n.buffer,n.byteOffset,n.byteLength)):Buffer.isBuffer(n)?t.sendMessageBinary(n):t.sendMessage(String(n))},close(){i="closed";try{t.close()}catch{}},addEventListener(n,a){e.has(n)||e.set(n,new Set),e.get(n).add(a)},removeEventListener(n,a){let o=e.get(n);o&&o.delete(a)},set onmessage(n){s.addEventListener("message",n)},set onopen(n){s.addEventListener("open",n)},set onclose(n){s.addEventListener("close",n)},set onerror(n){s.addEventListener("error",n)}};t.onOpen(()=>{i="open",r("open",{})}),t.onClosed(()=>{i="closed",r("close",{})}),t.onError(n=>{r("error",{error:n})}),t.onMessage(n=>{let a;Buffer.isBuffer(n)?a=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):a=n,r("message",{data:a})});function r(n,a){let o=e.get(n);if(o)for(let c of o)try{c(a)}catch{}}return s}};import{Socket as W}from"phoenix";import U from"ws";import{EventEmitter as q}from"node:events";var k=class extends q{constructor(t){super(),this.serverUrl=t.replace(/\/$/,""),this.socket=null,this.channel=null,this.connected=!1,this.heartbeatInterval=null,this.peers=[]}async connect(t,e,i,s){let r=this.serverUrl.replace(/^https:/,"wss:").replace(/^http:/,"ws:")+"/socket";return new Promise((n,a)=>{this.socket=new W(r,{params:{},transport:U,timeout:15e3,reconnectAfterMs:()=>36e5}),this.socket.onError(o=>{this.connected||a(new Error(`Signaling connection failed: ${o?.message||"unknown error"}`))}),this.socket.connect(),this.channel=this.socket.channel(`p2p:${t}`,{token:e,peer_id:i,role:s}),this.channel.on("message",o=>{this.emit(o.type,o)}),this.channel.onError(()=>{this.connected=!1}),this.channel.onClose(()=>{this.connected=!1,this._stopHeartbeat(),this.emit("disconnected")}),this.channel.join().receive("ok",o=>{this.connected=!0,this.peers=o.peers||[],this._startHeartbeat(),n({peers:this.peers})}).receive("error",o=>{a(new Error(`Channel join failed: ${o.reason||"unknown"}`))}).receive("timeout",()=>{a(new Error("Channel join timed out"))})})}sendOffer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("offer",{to:t,offer:e})}sendAnswer(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("answer",{to:t,answer:e})}sendIceCandidate(t,e){if(!this.connected)throw new Error("Not connected to signaling server");this.channel.push("ice_candidate",{to:t,candidate:e})}disconnect(){if(this._stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connected=!1}_startHeartbeat(){this._stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.connected&&this.channel&&this.channel.push("heartbeat",{})},1e4)}_stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}};import{open as $}from"node:fs/promises";import H from"node:path";import{lookup as Q}from"mime-types";async function E(u){let t=await $(u,"r"),e=await t.stat();if(!e.isFile())throw await t.close(),new Error(`Not a regular file: ${u}`);return{name:H.basename(u),size:e.size,type:Q(u)||"application/octet-stream",async readSlice(i,s){let r=s-i,n=Buffer.alloc(r),{bytesRead:a}=await t.read(n,0,r,i);return a===r?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):n.buffer.slice(n.byteOffset,n.byteOffset+a)},async close(){await t.close()}}}import{open as j,mkdir as J}from"node:fs/promises";import V from"node:path";function B(u,t={}){let e=new Map;return{async startStreamingReceive(i,s,r){await J(u,{recursive:!0});let n=G(s.fileName),a=V.join(u,n);if(!t.overwrite)try{let{access:c}=await import("node:fs/promises");throw await c(a),new Error(`File already exists: ${a} (use --overwrite to replace)`)}catch(c){if(c.code!=="ENOENT")throw c}let o=await j(a,"w");s.size>0&&await o.truncate(s.size),e.set(i,{fh:o,filePath:a,pieceSize:s.pieceSize,size:s.size,fileName:s.fileName})},async writeChunk(i,s,r){let n=e.get(i);if(!n)return{success:!1,error:"NO_HANDLE"};let a=s*n.pieceSize,o=Buffer.isBuffer(r)?r:Buffer.from(r);try{return await n.fh.write(o,0,o.byteLength,a),{success:!0}}catch(c){return{success:!1,error:c.message}}},async completeStream(i){let s=e.get(i);s&&(await s.fh.datasync(),await s.fh.close(),e.delete(i))},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(i){return e.get(i)?.filePath},async abort(i){let s=e.get(i);if(s){try{await s.fh.close()}catch{}e.delete(i);try{let{unlink:r}=await import("node:fs/promises");await r(s.filePath)}catch{}}}}}function G(u){return u.replace(/[/\\]/g,"_").replace(/\0/g,"").replace(/[<>:"|?*\x00-\x1f]/g,"_").replace(/^\.+/,"_").replace(/\s+/g,"_")||"unnamed_file"}function O(){let u=new Map;return{async startStreamingReceive(t,e,i){u.set(t,{pieceSize:e.pieceSize,totalPieces:Math.ceil(e.size/e.pieceSize),size:e.size,nextPiece:0,buffer:new Map,bytesWritten:0})},async writeChunk(t,e,i){let s=u.get(t);if(!s)return{success:!1,error:"NO_HANDLE"};let r=Buffer.isBuffer(i)?i:Buffer.from(i);for(s.buffer.set(e,r);s.buffer.has(s.nextPiece);){let n=s.buffer.get(s.nextPiece);s.buffer.delete(s.nextPiece);let a=s.nextPiece*s.pieceSize,o=s.size-a,c=o<n.byteLength?n.subarray(0,o):n;process.stdout.write(c),s.bytesWritten+=c.byteLength,s.nextPiece++}return{success:!0}},async completeStream(t){u.delete(t)},getBackpressureInfo(){return{paused:!1,bufferSize:0}},getFilePath(t){return"stdout"},async abort(t){u.delete(t)}}}var b=class{constructor(){this._listeners=new Map,this._onceListeners=new Map}on(t,e){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e),()=>this.off(t,e)}once(t,e){return this._onceListeners.has(t)||this._onceListeners.set(t,new Set),this._onceListeners.get(t).add(e),()=>{let i=this._onceListeners.get(t);i&&i.delete(e)}}off(t,e){let i=this._listeners.get(t);i&&i.delete(e);let s=this._onceListeners.get(t);s&&s.delete(e)}removeAllListeners(t){t?(this._listeners.delete(t),this._onceListeners.delete(t)):(this._listeners.clear(),this._onceListeners.clear())}emit(t,e){let i=this._listeners.get(t);i&&i.forEach(r=>{try{r(e)}catch{}});let s=this._onceListeners.get(t);if(s){let r=[...s];this._onceListeners.delete(t),r.forEach(n=>{try{n(e)}catch{}})}}listenerCount(t){let e=this._listeners.get(t)?.size||0,i=this._onceListeners.get(t)?.size||0;return e+i}hasListeners(t){return this.listenerCount(t)>0}eventNames(){return[...new Set([...this._listeners.keys(),...this._onceListeners.keys()])]}waitFor(t,e){return new Promise((i,s)=>{let r,n=a=>{r&&clearTimeout(r),i(a)};this.once(t,n),e&&(r=setTimeout(()=>{this.off(t,n),s(new Error(`Timeout waiting for event: ${t}`))},e))})}};var K={pieceSize:84*1024,maxConcurrentPieces:8,pieceTimeout:3e4,idleTimeout:6e4,maxPieceRetries:3,maxRetryQueueSize:1e4,maxInFlightMultiplier:10,highWaterMark:16*1024*1024,lowWaterMark:4*1024*1024,maxPiecesPerSecond:5e3,rateLimitWindowMs:1e3,channelRotationThreshold:512*1024*1024,channelRotationEnabled:!1,parallelChannels:1,parallelChannelsEnabled:!1,parallelChannelsThreshold:100*1024*1024,pacingEnabled:!0,pacingThreshold:0,pacingIntervalMs:10,targetBufferedAmount:8*1024*1024,minBufferedAmount:4*1024*1024,maxBufferedAmount:16*1024*1024,maxPiecesPerTick:32,burstPiecesPerTick:64,maxPendingReads:512,maxInFlightPieces:4e3},A=class extends b{constructor(t={}){super(),this.config={...K,...t},this.transfers=new Map,this.rateLimits=new Map,this._onBufferedAmountLow=this._onBufferedAmountLow.bind(this)}initSend(t,e,i){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s=this._createManifest(e),r=this.config.parallelChannelsEnabled&&e.size>=this.config.parallelChannelsThreshold,n={id:t,type:"send",file:e,manifest:s,channel:i,channels:[i],channelIndex:0,useParallelChannels:r,state:"initialized",nextPieceToSend:0,piecesAckedCount:0,bytesTransferred:0,paused:!1,inFlightPieces:new Map,retryQueue:[],channelBytes:[0],channelThresholds:[this.config.channelRotationThreshold],channelRotationCounts:[0],pendingRotations:new Set,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,idleTimeoutId:null,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,pieceRetries:new Map};return this.transfers.set(t,n),{transferId:t,manifest:s}}async startSend(t){let e=this.transfers.get(t);if(!e||e.type!=="send")throw new Error(`Send transfer not found: ${t}`);if(e.state==="transferring")return;e.state="transferring",e.startTime=e.startTime||Date.now();let i=this.config.pacingEnabled&&e.manifest.fileSize>=this.config.pacingThreshold;e.usePacing=i;for(let s of e.channels)s.addEventListener("bufferedamountlow",this._onBufferedAmountLow);e.useParallelChannels&&e.channels.length<this.config.parallelChannels&&this.emit("parallel-channels-needed",{transferId:t,currentCount:e.channels.length,targetCount:this.config.parallelChannels}),this._startIdleTimeout(t),i?this._startPacingLoop(t):await this._sendNextPieces(t)}addChannel(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.channels.length;i.channels.push(e),e.addEventListener("bufferedamountlow",this._onBufferedAmountLow),i.channelBytes.push(0),i.channelRotationCounts.push(0),i.channelThresholds||(i.channelThresholds=[this.config.channelRotationThreshold]);let r=this.config.channelRotationThreshold*(1+s*.25);i.channelThresholds.push(r),!i.paused&&i.state==="transferring"&&this._sendNextPieces(t)}handlePieceAck(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send")return;let s=i.inFlightPieces.get(e);if(!s)return;let r=s.channelIndex>=0?s.channelIndex:0;i.inFlightPieces.delete(e),i.pieceTimeouts.delete(e),i.lastActivityTime=Date.now(),i.piecesAckedCount++;let n=s.pieceSize||this._getPieceSize(i.manifest,e);i.bytesTransferred+=n,i.channelBytes[r]!==void 0&&(i.channelBytes[r]+=n),this._checkChannelRotation(t,i,r);let a=i.piecesAckedCount/i.manifest.totalPieces*100;if(i.lastProgress=a,this._checkProgressMilestones(t,i,a),this._emitProgress(t,i,a),i.piecesAckedCount===i.manifest.totalPieces){this._completeSend(t);return}!i.paused&&!i.usePacing&&this._sendNextPieces(t)}handlePieceAckBatch(t,e){let i=this.transfers.get(t);if(!i||i.type!=="send"||!Array.isArray(e)||e.length>1e3)return;let s=Date.now(),r=0;for(let n of e){if(typeof n!="number"||n<0)continue;let a=i.inFlightPieces.get(n);if(!a)continue;i.inFlightPieces.delete(n),i.pieceTimeouts.delete(n),i.piecesAckedCount++;let o=a.pieceSize||this._getPieceSize(i.manifest,n);i.bytesTransferred+=o,r+=o;let c=a.channelIndex>=0?a.channelIndex:0;i.channelBytes[c]!==void 0&&(i.channelBytes[c]+=o)}if(r>0){i.lastActivityTime=s;let n=i.piecesAckedCount/i.manifest.totalPieces*100;i.lastProgress=n,this._checkProgressMilestones(t,i,n),this._emitProgress(t,i,n),i.piecesAckedCount===i.manifest.totalPieces&&this._completeSend(t)}}handleWaterLevelAck(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||typeof e!="number"||e<-1||s.manifest&&e>=s.manifest.totalPieces)return;if(i!==void 0){if(!Array.isArray(i)||i.length>1e3)return;for(let o of i)if(typeof o!="number"||o<0||s.manifest&&o>=s.manifest.totalPieces)return}let r=Date.now(),n=0,a=0;if(s.lastAckTime=r,s.ackedWaterLevel===void 0&&(s.ackedWaterLevel=-1),e>s.ackedWaterLevel){let o=[];for(let[c,h]of s.inFlightPieces)c<=e&&o.push([c,h]);for(let[c,h]of o){s.inFlightPieces.delete(c);let l=h.pieceSize||this._getPieceSize(s.manifest,c);s.bytesTransferred+=l,n+=l,a++,s.pieceTimeouts.delete(c)}s.ackedWaterLevel=e,s.piecesAckedCount=e+1}if(i&&Array.isArray(i))for(let o of i){let c=s.inFlightPieces.get(o);if(c){s.inFlightPieces.delete(o);let h=c.pieceSize||this._getPieceSize(s.manifest,o);s.bytesTransferred+=h,n+=h,a++,s.piecesAckedCount++,s.pieceTimeouts.delete(o)}}if(a>0){s.lastActivityTime=r;let o=s.piecesAckedCount/s.manifest.totalPieces*100;s.lastProgress=o,this._checkProgressMilestones(t,s,o),this._emitProgress(t,s,o),s.piecesAckedCount===s.manifest.totalPieces&&this._completeSend(t)}}requeuePiece(t,e){let i=this.transfers.get(t);!i||i.type!=="send"||i.state==="completed"||i.state==="completing"||(i.inFlightPieces.delete(e),i.retryQueue.includes(e)||i.retryQueue.push(e),!i.paused&&!i.usePacing&&this._sendNextPieces(t))}markSendComplete(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.state==="completed"||e.state==="completing"||this._completeSend(t)}_checkChannelRotation(t,e,i){if(!this.config.channelRotationEnabled||e.pendingRotations.has(i))return;let s=e.channelBytes[i]||0,r=e.channelThresholds?.[i]||this.config.channelRotationThreshold;s>=r&&(e.pendingRotations.add(i),this.emit("channel-rotation-needed",{transferId:t,channelIndex:i,bytesTransferred:e.bytesTransferred,channelBytes:s,threshold:r,rotationCount:e.channelRotationCounts[i]||0}))}rotateSingleChannel(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="send"||e>=s.channels.length)return;let r=s.channels[e],n=(s.channelRotationCounts[e]||0)+1;s.channels[e]=i,e===0&&(s.channel=i),s.channelBytes[e]=0,s.channelRotationCounts[e]=n,s.pendingRotations.delete(e),i.addEventListener("bufferedamountlow",this._onBufferedAmountLow),r&&r.readyState==="open"&&this._drainAndCloseChannel(r,e,n),!s.paused&&!s.usePacing&&this._sendNextPieces(t)}_drainAndCloseChannel(t,e,i){let s=()=>{if(t.readyState==="open"){if(t.bufferedAmount===0){try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}return}setTimeout(s,50)}};s(),setTimeout(()=>{if(t.readyState==="open")try{t.removeEventListener("bufferedamountlow",this._onBufferedAmountLow),t.close()}catch{}},5e3)}rotateChannel(t,e){this.rotateChannels(t,[e])}initReceive(t,e,i={}){if(this.transfers.has(t))throw new Error(`Transfer already exists: ${t}`);let s={id:t,type:"receive",manifest:e,state:"initialized",nextPieceToRequest:0,piecesReceivedCount:0,receivedWaterLevel:-1,receivedOutOfOrder:new Set,maxOutOfOrderSize:1e4,piecesRequested:new Set,bytesTransferred:0,streamingHandler:i.streamingHandler||null,backpressureCheckerId:null,fileBuffer:null,pieceBuffers:new Map,paused:!1,senderPaused:!1,startTime:null,lastActivityTime:Date.now(),lastSpeedBytes:0,lastSpeedTime:0,smoothedSpeed:0,lastProgressEmit:0,pieceTimeoutCheckerId:null,pieceTimeouts:new Map,idleTimeoutId:null,pieceRetries:new Map};return s.streamingHandler||this._initReceiveBuffer(s,e),this.transfers.set(t,s),{transferId:t,piecesToRequest:this._getNextPiecesToRequest(s)}}startReceive(t,e){let i=this.transfers.get(t);if(!i||i.type!=="receive")throw new Error(`Receive transfer not found: ${t}`);i.state="transferring",i.startTime=i.startTime||Date.now(),i.requestPiece=e,this._startIdleTimeout(t),this._startBackpressureChecker(t),this._requestNextPieces(t)}_startBackpressureChecker(t){let e=this.transfers.get(t);!e||e.backpressureCheckerId||(e.backpressureCheckerId=setInterval(()=>{let i=this.transfers.get(t);if(!i||i.state==="completed"||i.state==="cancelled"){this._stopBackpressureChecker(t);return}if(i.streamingHandler?.getBackpressureInfo){let s=i.streamingHandler.getBackpressureInfo();!s.isUnderPressure&&i.senderPaused&&(i.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:s.overflowSize}))}},200))}_stopBackpressureChecker(t){let e=this.transfers.get(t);!e||!e.backpressureCheckerId||(clearInterval(e.backpressureCheckerId),e.backpressureCheckerId=null)}async handlePieceData(t,e,i){let s=this.transfers.get(t);if(!s||s.type!=="receive")return{success:!1,error:"Transfer not found"};if(s.state==="completing"||s.state==="completed")return{success:!0,duplicate:!0,progress:100};if(e<=s.receivedWaterLevel||s.receivedOutOfOrder.has(e))return{success:!0,duplicate:!0,progress:s.lastProgress||0};let r=i.byteLength;if(s.piecesReceivedCount<100){if(e<0||e>=s.manifest.totalPieces)return{success:!1,error:"Invalid piece index"};if(!(i instanceof ArrayBuffer||i instanceof Uint8Array||ArrayBuffer.isView(i)))return{success:!1,error:"Invalid piece data type"};if(r===0&&!(e===0&&s.manifest.fileSize===0))return{success:!1,error:"Empty piece data"};let n=this._getPieceSize(s.manifest,e);if(r>n)return{success:!1,error:"Invalid piece size"}}if(s.streamingHandler?.getBackpressureInfo){let n=s.streamingHandler.getBackpressureInfo();n.isUnderPressure&&!s.senderPaused?(s.senderPaused=!0,this.emit("send-backpressure",{transferId:t,pause:!0,bufferSize:n.overflowSize})):!n.isUnderPressure&&s.senderPaused&&(s.senderPaused=!1,this.emit("send-backpressure",{transferId:t,pause:!1,bufferSize:n.overflowSize}))}try{if(e===s.receivedWaterLevel+1){for(s.receivedWaterLevel=e;s.receivedOutOfOrder.has(s.receivedWaterLevel+1);)s.receivedOutOfOrder.delete(s.receivedWaterLevel+1),s.receivedWaterLevel++;s.lastActivityTime=Date.now()}else if(e>s.receivedWaterLevel+1){if(s.receivedOutOfOrder.size>=s.maxOutOfOrderSize)return{success:!1,error:"Out-of-order buffer full - retry later",retryable:!0,pieceIndex:e};s.receivedOutOfOrder.add(e)}if(s.streamingHandler?s.streamingHandler.writeChunk(t,e,i).then(a=>{a&&!a.success&&!a.duplicate&&(a.error!=="OVERFLOW_TIMEOUT"&&console.warn("[TransferEngine] Write issue for piece",e,":",a.error),a.error==="SW_DOWNLOAD_DEAD"&&a.retryable===!1&&(console.error("[TransferEngine] Fatal write error - aborting transfer:",t),this.emit("transfer-failed",{transferId:t,error:"Download stream cancelled. Please try again.",fatal:!0}),this.cancel(t)))}).catch(a=>{console.error("[TransferEngine] Write error for piece",e,":",a.message)}):this._storePieceInMemory(s,e,i),s.receivedOutOfOrder.size>=100&&s.requestPiece){let a=Date.now(),o=s.lastGapCheckTime||0;if(a-o>5e3){s.lastGapCheckTime=a;let c=s.receivedWaterLevel+1,h=Math.min(c+10,e);for(let l=c;l<h;l++)!s.receivedOutOfOrder.has(l)&&!s.piecesRequested.has(l)&&(s.piecesRequested.add(l),s.requestPiece(l))}}s.piecesReceivedCount++,s.piecesRequested.delete(e),s.pieceTimeouts.delete(e),s.pieceRetries.delete(e),s.bytesTransferred+=r;let n=s.piecesReceivedCount/s.manifest.totalPieces*100;return s.lastProgress=n,this._checkProgressMilestones(t,s,n),this._emitProgress(t,s,n),s.piecesReceivedCount===s.manifest.totalPieces?(s.state="completing",this._stopIdleTimeout(t),{success:!0,complete:!0,progress:100}):(!s.paused&&s.piecesReceivedCount%10===0&&this._requestNextPieces(t),{success:!0})}catch(n){return{success:!1,error:n.message}}}async completeReceive(t){let e=this.transfers.get(t);if(!e||e.type!=="receive")return{success:!1,error:"Transfer not found"};if(e.piecesReceivedCount!==e.manifest.totalPieces)return{success:!1,error:`Incomplete: ${e.piecesReceivedCount}/${e.manifest.totalPieces}`};e.state="completing",this._stopIdleTimeout(t),this._stopBackpressureChecker(t);try{if(e.streamingHandler)return await e.streamingHandler.completeStream(t),e.state="completed",{success:!0,streaming:!0,manifest:e.manifest};{let i=this._assembleFile(e);return e.state="completed",{success:!0,blob:i,manifest:e.manifest,fileName:e.manifest.fileName}}}catch(i){return{success:!1,error:i.message}}}pause(t){let e=this.transfers.get(t);e&&(e.paused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t),this.emit("paused",{transferId:t}))}resume(t){let e=this.transfers.get(t);!e||!e.paused||(e.paused=!1,this._startIdleTimeout(t),e.type==="send"?e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t):this._requestNextPieces(t),this.emit("resumed",{transferId:t}))}pauseForBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused||(e.backpressurePaused=!0,this._stopPacingLoop(t),this._stopIdleTimeout(t))}resumeFromBackpressure(t){let e=this.transfers.get(t);!e||e.type!=="send"||e.backpressurePaused&&(e.backpressurePaused=!1,!e.paused&&(this._startIdleTimeout(t),e.usePacing?this._startPacingLoop(t):this._sendNextPieces(t)))}cancel(t){let e=this.transfers.get(t);e&&(this._cleanupTransfer(t),this.emit("cancelled",{transferId:t,type:e.type}))}cleanup(t){this._cleanupTransfer(t)}getStats(t){let e=this.transfers.get(t);if(!e)return null;let i=e.startTime?Date.now()-e.startTime:0,s=i>0?e.bytesTransferred/i*1e3:0;return{transferId:t,type:e.type,state:e.state,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:this._calculateProgress(e),piecesCompleted:e.type==="send"?e.piecesAckedCount:e.piecesReceivedCount,totalPieces:e.manifest.totalPieces,speed:s,elapsed:i,paused:e.paused}}async _sendNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.retryQueue.length>=this.config.maxRetryQueueSize){this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=this.config.maxConcurrentPieces*e.channels.length*this.config.maxInFlightMultiplier;if(e.inFlightPieces.size>=i)return;let s=e.channel,r=e.channels.length===1;if(r){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return}else{let l=!1;for(let p of e.channels)if(p.readyState==="open"&&p.bufferedAmount<=this.config.highWaterMark){l=!0;break}if(!l)return}let a=this.config.maxConcurrentPieces*e.channels.length-e.inFlightPieces.size;if(a<=0)return;let o=[];for(;e.retryQueue.length>0&&o.length<a;){let l=e.retryQueue.shift();e.inFlightPieces.has(l)||o.push(l)}for(;e.nextPieceToSend<e.manifest.totalPieces&&o.length<a;){let l=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(l)||o.push(l)}if(o.length===0)return;let c=Date.now();for(let l of o)e.inFlightPieces.set(l,{sendTime:c,pieceSize:this._getPieceSize(e.manifest,l)});let h=r?4:4*e.channels.length;try{for(let l=0;l<o.length;l+=h){let g=o.slice(l,l+h).map(async m=>{let f=await this._readPiece(e.file,e.manifest,m);return{pieceIndex:m,data:f}}),d=await Promise.all(g);if(r)for(let{pieceIndex:m,data:f}of d){if(s.readyState!=="open"||s.bufferedAmount>this.config.highWaterMark)return;s.send(this._encodePieceMessage(t,m,f)),this._startPieceTimeout(t,m)}else for(let{pieceIndex:m,data:f}of d){let C=null,_=-1;for(let P=0;P<e.channels.length;P++){let y=(e.channelIndex+P)%e.channels.length,w=e.channels[y];if(w.readyState==="open"&&w.bufferedAmount<=this.config.highWaterMark){C=w,_=y,e.channelIndex=(y+1)%e.channels.length;break}}if(!C)return;C.send(this._encodePieceMessage(t,m,f)),e.inFlightPieces.set(m,{sendTime:Date.now(),channelIndex:_,pieceSize:this._getPieceSize(e.manifest,m)}),this._startPieceTimeout(t,m)}}}catch(l){for(let p of o)e.inFlightPieces.delete(p),e.retryQueue.push(p);this.emit("error",{transferId:t,error:`Failed to send pieces: ${l.message}`})}}_completeSend(t){let e=this.transfers.get(t);e&&(e.state="completed",this._stopPacingLoop(t),e.pieceTimeouts&&e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),this.emit("complete",{transferId:t,type:"send",bytesTransferred:e.bytesTransferred,duration:Date.now()-e.startTime}))}_startPacingLoop(t){let e=this.transfers.get(t);!e||e.pacingIntervalId||(e.pacingIntervalId=setInterval(()=>{this._pacingTick(t)},this.config.pacingIntervalMs),this._pacingTick(t))}_stopPacingLoop(t){let e=this.transfers.get(t);!e||!e.pacingIntervalId||(clearInterval(e.pacingIntervalId),e.pacingIntervalId=null)}_pacingTick(t){let e=this.transfers.get(t);if(!e||e.paused||e.backpressurePaused||e.state!=="transferring")return;if(e.pendingReads===void 0&&(e.pendingReads=0),e.retryQueue.length>=this.config.maxRetryQueueSize){this._stopPacingLoop(t),this.emit("piece-failed",{transferId:t,error:`Network too unstable: ${e.retryQueue.length} pieces pending retry`});return}let i=e.inFlightPieces.size,s=e.channel,r=s?s.bufferedAmount:0,n=this.config.maxInFlightPieces*.95,a=this.config.maxInFlightPieces*.99,o=this.config.maxPendingReads*.95,c=r<2*1024*1024,h=i>=this.config.maxInFlightPieces?"ACK_BACKPRESSURE":e.pendingReads>=this.config.maxPendingReads?"READ_BACKPRESSURE":r>this.config.maxBufferedAmount?"BUFFER_FULL":i>=a||i>=n&&c?"RECEIVER_SLOW":e.pendingReads>=o?"READ_BACKPRESSURE":"NONE";if(e.pacingSampleTick||(e.pacingSampleTick=0),e.pacingSampleTick++,e.pacingSampleTick>=100&&(e.pacingSampleTick=0,this.emit("pacing-stats",{transferId:t,inFlight:i,pendingReads:e.pendingReads,bottleneck:h})),i>=this.config.maxInFlightPieces||!s||s.readyState!=="open")return;let l=0;if(r>this.config.maxBufferedAmount)return;r>this.config.targetBufferedAmount?l=32:r>this.config.minBufferedAmount?l=this.config.maxPiecesPerTick:l=this.config.burstPiecesPerTick;let p=this.config.maxPendingReads-e.pendingReads;if(p<=0)return;l=Math.min(l,p);let g=this.config.maxInFlightPieces-i;l=Math.min(l,g);let d=[];for(;e.retryQueue.length>0&&d.length<l;){let f=e.retryQueue.shift();e.inFlightPieces.has(f)||d.push(f)}for(;e.nextPieceToSend<e.manifest.totalPieces&&d.length<l;){let f=e.nextPieceToSend;e.nextPieceToSend++,e.inFlightPieces.has(f)||d.push(f)}if(d.length===0){e.nextPieceToSend>=e.manifest.totalPieces&&e.retryQueue.length===0&&e.inFlightPieces.size===0&&e.pendingReads===0&&this._stopPacingLoop(t);return}e.pendingReads+=d.length;let m=Date.now();for(let f of d)e.inFlightPieces.set(f,{sendTime:m,pieceSize:this._getPieceSize(e.manifest,f)});this._readAndSendPieces(t,e,s,d)}async _readAndSendPieces(t,e,i,s){try{let r=s.map(a=>this._readPiece(e.file,e.manifest,a).then(o=>({pieceIndex:a,data:o})).catch(o=>({pieceIndex:a,error:o}))),n=await Promise.all(r);for(let a of n){if(a.error){e.inFlightPieces.delete(a.pieceIndex),e.retryQueue.push(a.pieceIndex);continue}let{pieceIndex:o,data:c}=a,h=e.channel;if(!h||h.readyState!=="open"){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}if(h.bufferedAmount>this.config.highWaterMark){e.inFlightPieces.delete(o),e.retryQueue.push(o);continue}try{h.send(this._encodePieceMessage(t,o,c)),this._startPieceTimeout(t,o)}catch{e.inFlightPieces.delete(o),e.retryQueue.push(o)}}}catch{for(let n of s)e.inFlightPieces.has(n)&&(e.inFlightPieces.delete(n),e.retryQueue.push(n))}finally{e.pendingReads-=s.length}}_requestNextPieces(t){let e=this.transfers.get(t);if(!e||e.paused||e.state!=="transferring")return;let i=this._getNextPiecesToRequest(e);for(let s of i)e.piecesRequested.add(s),e.requestPiece(s),this._startPieceTimeout(t,s)}_getNextPiecesToRequest(t){let e=this.config.maxConcurrentPieces-t.piecesRequested.size;if(e<=0)return[];let i=[];for(;t.nextPieceToRequest<t.manifest.totalPieces&&i.length<e;){let s=t.nextPieceToRequest;t.nextPieceToRequest++,!(s<=t.receivedWaterLevel||t.receivedOutOfOrder.has(s))&&!t.piecesRequested.has(s)&&i.push(s)}return i}_initReceiveBuffer(t,e){if(e.fileSize<=104857600)try{t.fileBuffer=new ArrayBuffer(e.fileSize),t.useChunkedBuffer=!1}catch{t.useChunkedBuffer=!0}else t.useChunkedBuffer=!0}_storePieceInMemory(t,e,i){if(t.useChunkedBuffer)t.pieceBuffers.set(e,new Uint8Array(i));else{let s=e*t.manifest.pieceSize;new Uint8Array(t.fileBuffer,s,i.byteLength).set(new Uint8Array(i))}}_assembleFile(t){let{manifest:e,fileBuffer:i,pieceBuffers:s,useChunkedBuffer:r}=t;if(r){let n=[];for(let o=0;o<e.totalPieces;o++){let c=s.get(o);if(!c)throw new Error(`Missing piece ${o}`);n.push(c)}let a=new Blob(n,{type:e.fileType||"application/octet-stream"});return s.clear(),a}else return new Blob([i],{type:e.fileType||"application/octet-stream"})}_createManifest(t){let e=this.config.pieceSize,i=Math.ceil(t.size/e);return{fileName:t.name,fileSize:t.size,fileType:t.type||"application/octet-stream",pieceSize:e,totalPieces:i,createdAt:Date.now()}}async _readPiece(t,e,i){let s=i*e.pieceSize,r=Math.min(s+e.pieceSize,t.size),n=await t.readSlice(s,r),a=new Uint8Array(4+n.byteLength);return a.set(new Uint8Array(n),4),a}_getPieceSize(t,e){return e===t.totalPieces-1?t.fileSize-e*t.pieceSize:t.pieceSize}_calculateProgress(t){return(t.type==="send"?t.piecesAckedCount:t.piecesReceivedCount)/t.manifest.totalPieces*100}_emitProgress(t,e,i){let s=Date.now();if(s-(e.lastProgressEmit||0)<100)return;let r=0,n=e.lastSpeedBytes||0,a=e.lastSpeedTime||e.startTime||s,o=s-a;if(o>=200){let p=e.bytesTransferred-n;r=o>0?p/o*1e3:0,e.lastSpeedBytes=e.bytesTransferred,e.lastSpeedTime=s}else e.smoothedSpeed?r=e.smoothedSpeed:e.startTime&&s>e.startTime&&(r=e.bytesTransferred/(s-e.startTime)*1e3);r=Math.max(0,r),Number.isFinite(r)||(r=0);let c=.15;e.smoothedSpeed===void 0||e.smoothedSpeed===0?e.smoothedSpeed=r:r>0&&(e.smoothedSpeed=c*r+(1-c)*e.smoothedSpeed);let h=Math.round(e.smoothedSpeed);e.lastProgressEmit=s;let l=i!==void 0?i:this._calculateProgress(e);this.emit("progress",{transferId:t,type:e.type,bytesTransferred:e.bytesTransferred,totalBytes:e.manifest.fileSize,progress:l,speed:h})}_encodePieceMessage(t,e,i){return new DataView(i.buffer).setUint32(0,e,!0),i.buffer}decodePieceMessage(t){if(!t||!(t instanceof ArrayBuffer)||t.byteLength<4)return{pieceIndex:-1,data:null,error:"invalid_message"};try{let i=new DataView(t).getUint32(0,!0),s=t.slice(4);return{pieceIndex:i,data:s}}catch{return{pieceIndex:-1,data:null,error:"decode_failed"}}}_checkProgressMilestones(t,e,i){let s=Date.now(),r=36e5,n=0;if(e.lastMilestoneCheck){let a=s-e.lastMilestoneCheck.time,o=e.bytesTransferred-e.lastMilestoneCheck.bytes;if(a>0&&a<r&&o>0)n=o/a*1e3/1e6;else if(a>=r){let c=e.startTime?s-e.startTime:0;n=c>0?e.bytesTransferred/c*1e3/1e6:0}}else{let a=e.startTime?s-e.startTime:0;n=a>0?e.bytesTransferred/a*1e3/1e6:0}e.lastMilestoneCheck={time:s,bytes:e.bytesTransferred},(!Number.isFinite(n)||n<0)&&(n=0),i>=90&&!e.milestone90&&(e.milestone90=!0,e.speedAt90Time=s,this.emit("progress-milestone",{transferId:t,milestone:90,speed:n})),i>=99&&!e.milestone99&&(e.milestone99=!0,this.emit("progress-milestone",{transferId:t,milestone:99,speed:n}))}_startPieceTimeout(t,e){let i=this.transfers.get(t);i&&(i.pieceTimeouts.set(e,Date.now()),this._startPieceTimeoutChecker(t))}_clearPieceTimeout(t,e){let i=this.transfers.get(t);i&&i.pieceTimeouts.delete(e)}_startPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||e.pieceTimeoutCheckerId||(e.pieceTimeoutCheckerId=setInterval(()=>{this._checkPieceTimeouts(t)},5e3))}_stopPieceTimeoutChecker(t){let e=this.transfers.get(t);!e||!e.pieceTimeoutCheckerId||(clearInterval(e.pieceTimeoutCheckerId),e.pieceTimeoutCheckerId=null)}_checkPieceTimeouts(t){let e=this.transfers.get(t);if(!e)return;if(e.state==="completed"||e.state==="completing"){this._stopPieceTimeoutChecker(t);return}let i=Date.now(),s=this.config.pieceTimeout,r=s*.5,n=i-e.lastActivityTime,o=e.manifest.totalPieces-(e.piecesReceivedCount||e.piecesAckedCount||0)<=10,c=n<r&&!o,h=[];for(let[g,d]of e.pieceTimeouts)if(i-d>=s){if(e.type==="receive"&&e.receivedOutOfOrder&&(g<=e.receivedWaterLevel||e.receivedOutOfOrder.has(g))){e.pieceTimeouts.delete(g);continue}if(e.type==="send"&&e.ackedWaterLevel!==void 0&&g<=e.ackedWaterLevel){e.pieceTimeouts.delete(g);continue}h.push(g)}if(h.length===0||c)return;let p=h.slice(0,50);for(let g of p)this._handlePieceTimeout(t,g)}_handlePieceTimeout(t,e){let i=this.transfers.get(t);if(!i)return;i.pieceTimeouts.delete(e);let s=i.pieceRetries.get(e)||0;s<this.config.maxPieceRetries?(i.pieceRetries.set(e,s+1),this.emit("piece-retry",{transferId:t,pieceIndex:e,retryCount:s+1}),i.type==="receive"?(i.piecesRequested.delete(e),i.piecesRequested.add(e),i.requestPiece(e),i.pieceTimeouts.set(e,Date.now())):(i.inFlightPieces.delete(e),i.retryQueue.push(e),i.usePacing||this._sendNextPieces(t))):(i.inFlightPieces.delete(e),i.pieceRetries.delete(e),this.emit("piece-failed",{transferId:t,pieceIndex:e,error:"Max retries exceeded"}))}_startIdleTimeout(t){this._stopIdleTimeout(t);let e=this.transfers.get(t);e&&(e.idleTimeoutId=setTimeout(()=>{let i=this.transfers.get(t);if(!i||i.state==="completing"||i.state==="completed")return;let s=i.lastAckTime||0,r=i.lastActivityTime||0,n=Math.max(s,r),a=Date.now()-n;a>=this.config.idleTimeout?this.emit("idle-timeout",{transferId:t,timeSinceActivity:a}):this._startIdleTimeout(t)},this.config.idleTimeout))}_stopIdleTimeout(t){let e=this.transfers.get(t);!e||!e.idleTimeoutId||(clearTimeout(e.idleTimeoutId),e.idleTimeoutId=null)}_onBufferedAmountLow(t){this.transfers.forEach((e,i)=>{e.type==="send"&&!e.usePacing&&(e.channel===t.target||e.channels?.includes(t.target))&&this._sendNextPieces(i)})}_cleanupTransfer(t){let e=this.transfers.get(t);if(e){if(this._stopPacingLoop(t),this._stopBackpressureChecker(t),e.pieceTimeouts.clear(),this._stopPieceTimeoutChecker(t),this._stopIdleTimeout(t),e.channels)for(let i of e.channels)i.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);else e.channel&&e.channel.removeEventListener("bufferedamountlow",this._onBufferedAmountLow);e.pieceBuffers&&e.pieceBuffers.clear(),e.fileBuffer=null,this.rateLimits.delete(t),this.transfers.delete(t)}}_checkRateLimit(t){let e=Date.now(),i=this.rateLimits.get(t);return i||(i={count:0,windowStart:e},this.rateLimits.set(t,i)),e-i.windowStart>this.config.rateLimitWindowMs&&(i.count=0,i.windowStart=e),i.count++,{allowed:i.count<=this.config.maxPiecesPerSecond,currentRate:i.count,limit:this.config.maxPiecesPerSecond}}};var Y="https://perkoon.com",L=class extends X{constructor(t=Y,e={}){super(),this.serverUrl=t,this.peerTimeout=e.timeout||3e5,this.engine=new A,this.transport=null,this.signaling=null,this.fileSource=null}async send(t,e={}){this.fileSource=await E(t),this.emit("file-ready",{name:this.fileSource.name,size:this.fileSource.size,type:this.fileSource.type});let i=await this._createSession(e.name,e.password);this.emit("session-created",{sessionCode:i.session_code,expiresAt:i.expires_at}),this.signaling=new k(this.serverUrl);let{peers:s}=await this.signaling.connect(i.session_code,i.token,i.peer_id,"sender");this.emit("waiting-for-receiver");let r=s.length>0?s[0]:await this._waitForPeer();this.emit("receiver-connected",{peerId:r}),this.transport=new v,this.transport.initialize(i.peer_id,i.ice_servers);let n=await this.transport.createOffer();this.signaling.sendOffer(r,n),this.transport.on("ice-candidate",({candidate:p,mid:g})=>{this.signaling.sendIceCandidate(r,{candidate:p,mid:g})}),this.signaling.on("ice_candidate",p=>{this.transport.addIceCandidate(p.candidate)});let a=await this._waitForSignal("answer");this.transport.handleAnswer(a.answer);let o=await this.transport.waitForDataChannel();this.emit("connected");let c=Z.randomUUID();this.engine.initSend(c,this.fileSource,o);let h=this.engine.transfers.get(c).manifest;this._setupControlHandler(o,c),o.send(JSON.stringify({type:"manifest",transfer_id:c,manifest:h})),this._setupProgressEvents(c),await this.engine.startSend(c);let l=await this._waitForCompletion(c);return await this.fileSource.close(),this.signaling.disconnect(),this.transport.close(),{sessionCode:i.session_code,speed:l.speed,duration:l.duration}}async receive(t,e,i={}){let s=await this._joinSession(t,i.password);this.emit("session-joined",{sessionCode:t}),this.signaling=new k(this.serverUrl);let{peers:r}=await this.signaling.connect(t,s.token,s.peer_id,"receiver"),n=r.length>0?r[0]:await this._waitForPeer();this.emit("sender-found",{peerId:n}),this.signaling.channel.push("signal",{to:n,type:"ready",data:{role:"receiver"}}),this.transport=new v,this.transport.initialize(s.peer_id,s.ice_servers),this.transport.on("ice-candidate",({candidate:d,mid:m})=>{this.signaling.sendIceCandidate(n,{candidate:d,mid:m})}),this.signaling.on("ice_candidate",d=>{this.transport.addIceCandidate(d.candidate)});let a=await this._waitForSignal("offer"),o=await this.transport.handleOffer(a.offer);this.signaling.sendAnswer(n,o);let c=await this.transport.waitForDataChannel();this.emit("connected");let h=i.stdout?O():B(e,{overwrite:i.overwrite}),l=[],g=await new Promise((d,m)=>{let f=null;c.addEventListener("message",async C=>{let _=C.data;if(typeof _=="string"){try{let P=JSON.parse(_);if(P.type==="manifest"){f=P.transfer_id;let y=P.manifest;this.emit("receiving-file",{name:y.fileName,size:y.fileSize}),await h.startStreamingReceive(f,{fileName:y.fileName,size:y.fileSize,fileSize:y.fileSize,type:y.fileType,pieceSize:y.pieceSize},{}),this.engine.initReceive(f,y,{streamingHandler:h}),this._setupProgressEvents(f),this.engine.startReceive(f,T=>{c.send(JSON.stringify({type:"request",transfer_id:f,piece_index:T}))});let w=-1;this._ackInterval=setInterval(()=>{let T=this.engine.transfers.get(f);if(!T||T.state==="completed"||T.state==="completing"){clearInterval(this._ackInterval);return}let S=T.receivedWaterLevel;if(S>w){w=S;let R=T.receivedOutOfOrder.size>0?Array.from(T.receivedOutOfOrder):void 0;c.send(JSON.stringify({type:"water_level_ack",transfer_id:f,water_level:S,out_of_order:R}))}},100)}}catch{}return}if(f&&(_ instanceof ArrayBuffer||Buffer.isBuffer(_))){let P=_ instanceof ArrayBuffer?_:_.buffer.slice(_.byteOffset,_.byteOffset+_.byteLength),{pieceIndex:y,data:w}=this.engine.decodePieceMessage(P);if(y>=0&&w&&(await this.engine.handlePieceData(f,y,w))?.complete){this._ackInterval&&clearInterval(this._ackInterval);let S=this.engine.transfers.get(f);S&&c.send(JSON.stringify({type:"water_level_ack",transfer_id:f,water_level:S.receivedWaterLevel}));let R=h.getFilePath(f);l.push(R),await this.engine.completeReceive(f),c.send(JSON.stringify({type:"complete",transfer_id:f}));let z=this.engine.transfers.get(f),F=Date.now()-(z?.startTime||Date.now()),M=z?.manifest;d({files:l,speed:M?M.fileSize/(F/1e3):0,duration:F})}}}),setTimeout(()=>{m(new Error("Transfer timed out (10 minutes)"))},6e5)});return this.signaling.disconnect(),this.transport.close(),g}async _createSession(t,e){let i=`${this.serverUrl}/api/v1/sessions`,s={};t&&(s.name=t),e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${n.error||r.statusText}`)}return r.json()}async _joinSession(t,e){let i=`${this.serverUrl}/api/v1/sessions/${t}/join`,s={role:"receiver"};e&&(s.password=e);let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let n=await r.json().catch(()=>({}));throw r.status===404?new Error(`Session ${t} not found. Check the code and try again.`):r.status===410?new Error(`Session ${t} has expired.`):r.status===403?new Error(n.error||"Access denied"):new Error(`Failed to join session: ${n.error||r.statusText}`)}return r.json()}_waitForPeer(){return new Promise((t,e)=>{let i=setTimeout(()=>{e(new Error(`No peer connected within ${Math.round(this.peerTimeout/1e3)}s`))},this.peerTimeout),s=r=>{(r.type==="ready"||r.type==="offer")&&(clearTimeout(i),this.signaling.removeListener("ready",s),this.signaling.removeListener("offer",s),t(r.from))};this.signaling.on("ready",s),this.signaling.on("offer",s)})}_waitForSignal(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error(`Timed out waiting for ${t} signal`))},3e4);this.signaling.once(t,r=>{clearTimeout(s),e(r)})})}_setupProgressEvents(t){let e=setInterval(()=>{let i=this.engine.transfers.get(t);if(!i){clearInterval(e);return}let s=i.manifest,r=s.totalPieces,n;i.type==="send"?n=i.piecesAckedCount||0:n=i.piecesReceivedCount||0;let a=r>0?Math.round(n/r*100):0,o=Date.now()-(i.startTime||Date.now()),c=n*s.pieceSize,h=o>0?c/(o/1e3):0,l=h>0?Math.round((s.fileSize-c)/h):0;this.emit("progress",{transferId:t,percent:Math.min(a,100),speed:h,eta:l,bytesTransferred:c,totalBytes:s.fileSize}),(i.state==="completed"||i.state==="cancelled")&&clearInterval(e)},200)}_setupControlHandler(t,e){t.addEventListener("message",i=>{if(typeof i.data=="string")try{let s=JSON.parse(i.data);switch(s.type){case"ack":this.engine.handlePieceAck(e,s.piece_index);break;case"water_level_ack":this.engine.handleWaterLevelAck(e,s.water_level,s.out_of_order);break;case"request":this.engine.requeuePiece(e,s.piece_index);break;case"complete":{let r=this.engine.transfers.get(e);r&&r.state!=="completed"&&(r.piecesAckedCount=r.manifest.totalPieces,r.bytesTransferred=r.manifest.fileSize,r.inFlightPieces.clear(),r.state="completed")}this.emit("transfer-complete",{transferId:e});break;case"backpressure":s.pause?this.engine.pause(e):this.engine.resume(e);break}}catch{}})}_waitForCompletion(t){return new Promise((e,i)=>{let s=setTimeout(()=>{i(new Error("Transfer timed out (10 minutes)"))},6e5),r=()=>{let a=this.engine.transfers.get(t);if(!a){clearTimeout(s),i(new Error("Transfer context lost"));return}if(a.state==="completed"){clearTimeout(s);let o=Date.now()-a.startTime;e({speed:a.manifest.fileSize/(o/1e3),duration:o});return}if(a.state==="cancelled"||a.state==="failed"){clearTimeout(s),i(new Error("Transfer failed"));return}};this.on("transfer-complete",({transferId:a})=>{a===t&&r()});let n=setInterval(()=>{r();let a=this.engine.transfers.get(t);(!a||a.state==="completed"||a.state==="cancelled")&&clearInterval(n)},500)})}destroy(){this._ackInterval&&clearInterval(this._ackInterval),this.fileSource&&this.fileSource.close().catch(()=>{}),this.signaling&&this.signaling.disconnect(),this.transport&&this.transport.close()}};export{v as NodeTransport,k as SignalingClient,L as TransferManager,B as createNodeFileSink,E as createNodeFileSource};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "perkoon",
3
+ "version": "0.1.0",
4
+ "description": "P2P file transfer CLI — send files directly between devices, no cloud required",
5
+ "type": "module",
6
+ "main": "dist/client.js",
7
+ "bin": {
8
+ "perkoon": "dist/bin/perkoon.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "node build.js",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "node --test src/**/*.test.js"
18
+ },
19
+ "dependencies": {
20
+ "mime-types": "^2.1.35",
21
+ "node-datachannel": "^0.12.0",
22
+ "phoenix": "^1.8.0",
23
+ "ws": "^8.18.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "keywords": [
29
+ "p2p",
30
+ "file-transfer",
31
+ "webrtc",
32
+ "perkoon",
33
+ "send-files",
34
+ "peer-to-peer"
35
+ ],
36
+ "author": "Perkoon",
37
+ "license": "SEE LICENSE IN LICENSE",
38
+ "homepage": "https://perkoon.com",
39
+ "devDependencies": {
40
+ "esbuild": "^0.27.3"
41
+ }
42
+ }