@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
JavaScript
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
};
;