UNPKG

@trap_stevo/filetide

Version:

Revolutionizing real-time file transfer with seamless, instant communication across any device. Deliver files instantly, regardless of platform, and experience unparalleled speed and control in managing transfers. Elevate your file-sharing capabilities wi

355 lines (325 loc) 14.9 kB
"use strict"; function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } const fs = require("fs"); const FileTransferRecoveryManager = require("./FileTransferRecoveryManager.js"); var _FileTransferManager_brand = /*#__PURE__*/new WeakSet(); class FileTransferManager { constructor({ chunkSize = 512 * 1024, parallelChunks = 5, maxRetries = 3 } = {}) { _classPrivateMethodInitSpec(this, _FileTransferManager_brand); this.parallelChunks = parallelChunks; this.activeTransfers = new Map(); this.maxRetries = maxRetries; this.chunkSize = chunkSize; } async sendFile(file, { fileDetails, originDetails, onSendChunk, onComplete, onProgress, onStart, onTiding }, clientTide) { const transferId = _assertClassBrand(_FileTransferManager_brand, this, _generateTransferId).call(this, fileDetails); return await _assertClassBrand(_FileTransferManager_brand, this, _startTransfer).call(this, file, transferId, fileDetails, originDetails, onSendChunk, onComplete, onProgress, onStart, onTiding, clientTide); } async sendLargeFile(fileHandle, { fileDetails, originDetails, recipientID, onSendChunk, onComplete, onProgress, onStart, onTiding }, clientTide) { const transferId = _assertClassBrand(_FileTransferManager_brand, this, _generateTransferId).call(this, fileDetails); const recoveredTransferState = (await clientTide.emitEventWithResponse(originDetails.senderID, "get-client-transfer-state", "sent-current-transfer-state", { recipientID, senderID: originDetails.senderID, filePath: fileDetails.destinationPath, fileName: fileDetails.name }, 30000)) || { transferredSize: 0, lastChunkSent: 0 }; const totalChunks = Math.ceil(fileDetails.size / this.chunkSize); this.activeTransfers.set(transferId, { totalChunks, transferredSize: recoveredTransferState.transferredSize, completedChunks: recoveredTransferState.lastChunkSent, activeChunks: 0, failedChunks: new Map(), currentChunk: recoveredTransferState.lastChunkSent, size: fileDetails.size }); const updateProgress = () => { const transferState = this.activeTransfers.get(transferId); if (onProgress) { onProgress(transferState.completedChunks / transferState.totalChunks * 100, this.chunkSize); } }; const exponentialBackoff = retryCount => Math.min(1000 * 2 ** retryCount, 10000); let nextExpectedChunk = recoveredTransferState.lastChunkSent; const chunkBuffer = new Map(); const sendChunk = async chunkIndex => { const start = chunkIndex * this.chunkSize; const end = Math.min(start + this.chunkSize, fileDetails.size); const { buffer } = await fileHandle.read(Buffer.alloc(end - start), 0, end - start, start); const transferState = this.activeTransfers.get(transferId); chunkBuffer.set(chunkIndex, buffer); try { while (chunkBuffer.has(nextExpectedChunk)) { const nextChunkBuffer = chunkBuffer.get(nextExpectedChunk); await onSendChunk(transferId, nextExpectedChunk, this.chunkSize, nextChunkBuffer, totalChunks, this.activeTransfers.get(transferId).completedChunks + 1); chunkBuffer.delete(nextExpectedChunk); nextExpectedChunk++; transferState.transferredSize += this.chunkSize; transferState.completedChunks++; updateProgress(); } if (transferState.completedChunks === transferState.totalChunks) { return onComplete(transferId).then(() => { this.activeTransfers.delete(transferId); }); } } catch (error) { console.error(`[FileTide ~ Transporter] ~ Did not send chunk ${chunkIndex} ~ ${error.message}`); return onComplete(transferId).then(() => { this.activeTransfers.delete(transferId); }); } }; const sendChunkBatch = async (startChunkIndex, batchSize = 100, miniBatchSize = 10) => { for (let i = 0; i < batchSize && startChunkIndex + i < totalChunks; i += miniBatchSize) { const miniBatchPromises = []; for (let j = 0; j < miniBatchSize && startChunkIndex + i + j < totalChunks; j++) { const chunkIndex = startChunkIndex + i + j; miniBatchPromises.push(sendChunk(chunkIndex)); } await Promise.all(miniBatchPromises); await new Promise(resolve => setTimeout(resolve, 5)); } }; const transferState = this.activeTransfers.get(transferId); let currentBatchStart = transferState.currentChunk; const keepAliveInterval = setInterval(() => { if (onTiding) onTiding(); }, 5000); return new Promise(async (resolve, reject) => { try { let adaptiveBatchSize = 100; while (currentBatchStart < totalChunks) { if (transferState.activeChunks < this.parallelChunks) { adaptiveBatchSize = Math.min(adaptiveBatchSize + 10, 100); } else { adaptiveBatchSize = Math.max(adaptiveBatchSize - 10, 10); } await sendChunkBatch(currentBatchStart, adaptiveBatchSize); currentBatchStart += adaptiveBatchSize; await new Promise(resolve => setTimeout(resolve, 30)); } } finally { clearInterval(keepAliveInterval); fileHandle.on("close", resolve); fileHandle.on("error", error => { this.activeTransfers.delete(transferId); reject(error); }); try { await fileHandle.close(); } catch (error) { console.error(`[FileTide ~ Transporter] ~ Error closing file handle ~ ${error.message}`); } } }); } } async function _startTransfer(file, transferId, fileDetails, originDetails, onSendChunk, onComplete, onProgress, onStart, onTiding, clientTide) { const recoveredTransferState = (await clientTide.emitEventWithResponse(originDetails.senderID, "get-client-transfer-state", "sent-current-transfer-state", { recipientID: originDetails.recipientID, senderID: originDetails.senderID, filePath: fileDetails.destinationPath, fileName: fileDetails.name }, 30000)) || { transferredSize: 0, lastChunkSent: 0 }; const totalChunks = Math.ceil(file.length / this.chunkSize); let nextExpectedChunk = recoveredTransferState.lastChunkSent; const chunkBuffer = new Map(); this.activeTransfers.set(transferId, { totalChunks, transferredSize: recoveredTransferState.transferredSize, completedChunks: recoveredTransferState.lastChunkSent, activeChunks: 0, failedChunks: new Map(), currentChunk: recoveredTransferState.lastChunkSent, size: fileDetails.size }); const transferState = this.activeTransfers.get(transferId); const updateProgress = () => { if (onProgress) { onProgress(transferState.completedChunks / transferState.totalChunks * 100, this.chunkSize); } }; const exponentialBackoff = retryCount => Math.min(1000 * 2 ** retryCount, 10000); const sendChunk = async chunkIndex => { const start = chunkIndex * this.chunkSize; const end = Math.min(start + this.chunkSize, file.length); const chunk = file.slice(start, end); transferState.activeChunks++; try { await onSendChunk(transferId, chunkIndex, this.chunkSize, chunk, totalChunks, transferState.completedChunks + 1); chunkBuffer.set(chunkIndex, chunk); while (chunkBuffer.has(nextExpectedChunk)) { chunkBuffer.delete(nextExpectedChunk); nextExpectedChunk++; transferState.completedChunks++; updateProgress(); } if (transferState.completedChunks === transferState.totalChunks) { clearInterval(keepAliveInterval); await onComplete(transferId); this.activeTransfers.delete(transferId); } } catch (error) { console.error(`[FileTide ~ Transporter] ~ Did not send chunk ${chunkIndex} ~ ${error.message}`); const retryCount = (transferState.failedChunks.get(chunkIndex) || 0) + 1; if (retryCount > this.maxRetries) { clearInterval(keepAliveInterval); this.activeTransfers.delete(transferId); throw new Error(`[FileTide ~ Transporter] ~ Did not send chunk ${chunkIndex} after ${this.maxRetries} retries.`); } transferState.failedChunks.set(chunkIndex, retryCount); setTimeout(() => sendChunk(chunkIndex), exponentialBackoff(retryCount)); } finally { transferState.activeChunks--; } }; const sendChunkBatch = async (startChunkIndex, batchSize = 100, miniBatchSize = 10) => { for (let i = 0; i < batchSize && startChunkIndex + i < totalChunks; i += miniBatchSize) { const miniBatchPromises = []; for (let j = 0; j < miniBatchSize && startChunkIndex + i + j < totalChunks; j++) { const chunkIndex = startChunkIndex + i + j; miniBatchPromises.push(sendChunk(chunkIndex)); } await Promise.all(miniBatchPromises); await new Promise(resolve => setTimeout(resolve, 5)); } }; const keepAliveInterval = setInterval(() => { if (onTiding) onTiding(); }, 5000); let currentBatchStart = transferState.currentChunk; let adaptiveBatchSize = 100; try { while (currentBatchStart < totalChunks) { adaptiveBatchSize = transferState.activeChunks < this.parallelChunks ? Math.min(adaptiveBatchSize + 10, 100) : Math.max(adaptiveBatchSize - 10, 10); await sendChunkBatch(currentBatchStart, adaptiveBatchSize); currentBatchStart += adaptiveBatchSize; await new Promise(resolve => setTimeout(resolve, 30)); } } finally { clearInterval(keepAliveInterval); } } /*async #startTransfer(file, transferId, fileDetails, originDetails, onSendChunk, onComplete, onProgress, onStart, onTiding, clientTide) { const recoveredTransferState = await clientTide.emitEventWithResponse(originDetails.senderID, "get-client-transfer-state", "sent-current-transfer-state", { recipientID : originDetails.recipientID, senderID : originDetails.senderID, filePath : fileDetails.destinationPath, fileName : fileDetails.name }, 30000) || { transferredSize : 0, lastChunkSent : 0 }; const totalChunks = Math.ceil(file.length / this.chunkSize); this.activeTransfers.set(transferId, { totalChunks, transferredSize : recoveredTransferState.transferredSize, completedChunks : recoveredTransferState.lastChunkSent, activeChunks : 0, failedChunks : new Map(), currentChunk : recoveredTransferState.lastChunkSent, size : fileDetails.size }); const updateProgress = () => { const transferState = this.activeTransfers.get(transferId); if (onProgress) { onProgress((transferState.completedChunks / transferState.totalChunks) * 100, this.chunkSize); } }; const keepAliveInterval = setInterval(() => { if (onTiding) onTiding(); }, 5000); const exponentialBackoff = (retryCount) => Math.min(1000 * (2 ** retryCount), 10000); return new Promise((resolve, reject) => { const sendChunk = async (chunkIndex) => { const transferState = this.activeTransfers.get(transferId); if (chunkIndex >= transferState.totalChunks) { return; } const start = chunkIndex * this.chunkSize; const end = Math.min(start + this.chunkSize, file.length); const chunk = file.slice(start, end); if (onStart && chunkIndex === 0) { onStart({ name : fileDetails.name, path : fileDetails.path, size : fileDetails.size, totalChunks : transferState.totalChunks }); } transferState.activeChunks++; try { await onSendChunk(transferId, chunkIndex, this.chunkSize, chunk, transferState.totalChunks, transferState.completedChunks + 1); transferState.transferredSize += this.chunkSize; transferState.completedChunks++; transferState.activeChunks--; updateProgress(); if (transferState.activeChunks < this.parallelChunks && transferState.currentChunk < transferState.totalChunks) { sendChunk(transferState.currentChunk++); } if (transferState.completedChunks === transferState.totalChunks) { await onComplete(transferId); this.activeTransfers.delete(transferId); clearInterval(keepAliveInterval); resolve(); } } catch (error) { console.error(`[FileTide ~ File Messager] ~ Did not send chunk ${chunkIndex} ~ ${error.message}`); transferState.activeChunks--; const retryCount = (transferState.failedChunks.get(chunkIndex) || 0) + 1; if (retryCount > this.maxRetries) { this.activeTransfers.delete(transferId); clearInterval(keepAliveInterval); reject(new Error(`Did not send chunk ${chunkIndex} after ${this.maxRetries} retries.`)); } else { transferState.failedChunks.set(chunkIndex, retryCount); setTimeout(() => sendChunk(chunkIndex), exponentialBackoff(retryCount)); } } }; const transferState = this.activeTransfers.get(transferId); for (let i = 0; i < Math.min(this.parallelChunks, transferState.totalChunks); i++) { sendChunk(transferState.currentChunk++); } }); }*/ function _generateTransferId(file) { return `${file.name}-${Date.now()}`; } module.exports = { FileTransferManager };