@trap_stevo/filetide 0.0.85 → 0.0.86
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/SCL-1.0-Universal.md +228 -0
- package/dist/cjs/FileMessagerClient.js +134 -70
- package/dist/cjs/FileTide.js +163 -82
- package/dist/cjs/{FileMessager.js → FileTideAnchor.js} +444 -264
- package/dist/cjs/HUDManagers/EndpointManager.js +157 -0
- package/dist/cjs/HUDManagers/{FileNetClientManager.js → FileTideAnchorClientManager.js} +3 -2
- package/dist/cjs/HUDManagers/{FileNetUtilityManager.js → FileTideAnchorUtilityManager.js} +2 -2
- package/dist/cjs/HUDManagers/FileTideDebugUtilityManager.js +6 -0
- package/dist/cjs/HUDManagers/FileTideNetworkTrafficManager.js +419 -0
- package/dist/cjs/HUDManagers/FileTransferManager.js +90 -157
- package/dist/cjs/HUDManagers/FileTransferReceiverManager.js +169 -9
- package/dist/cjs/HUDManagers/FileTransferStatsManager.js +138 -0
- package/dist/cjs/HUDManagers/FileTransferTrackingManager.js +22 -0
- package/package.json +4 -5
- package/LICENSE.md +0 -79
- package/dist/cjs/HUDComponents/ConsoleProgressBarManager.js +0 -288
|
@@ -4,18 +4,43 @@ function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a
|
|
|
4
4
|
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
|
|
5
5
|
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"); }
|
|
6
6
|
const fs = require("fs");
|
|
7
|
+
const {
|
|
8
|
+
FileTideNetworkTrafficManager
|
|
9
|
+
} = require("./FileTideNetworkTrafficManager.js");
|
|
7
10
|
var _FileTransferManager_brand = /*#__PURE__*/new WeakSet();
|
|
8
11
|
class FileTransferManager {
|
|
9
12
|
constructor({
|
|
10
|
-
chunkSize = 512 * 1024,
|
|
11
|
-
parallelChunks =
|
|
12
|
-
maxRetries = 3
|
|
13
|
+
chunkSize: _chunkSize = 512 * 1024,
|
|
14
|
+
parallelChunks = 3,
|
|
15
|
+
maxRetries = 3,
|
|
16
|
+
networkTraffic = {}
|
|
13
17
|
} = {}) {
|
|
14
18
|
_classPrivateMethodInitSpec(this, _FileTransferManager_brand);
|
|
15
|
-
this.parallelChunks = parallelChunks;
|
|
19
|
+
this.parallelChunks = Math.max(1, parallelChunks);
|
|
16
20
|
this.activeTransfers = new Map();
|
|
17
21
|
this.maxRetries = maxRetries;
|
|
18
|
-
this.chunkSize =
|
|
22
|
+
this.chunkSize = _chunkSize;
|
|
23
|
+
this.networkTraffic = new FileTideNetworkTrafficManager({
|
|
24
|
+
defaultBandwidthBps: 32 * 1024 * 1024,
|
|
25
|
+
minimumBandwidthBps: 512 * 1024,
|
|
26
|
+
targetUtilization: 0.90,
|
|
27
|
+
congestedUtilizationFactor: 0.60,
|
|
28
|
+
targetChunkDurationMs: 250,
|
|
29
|
+
minChunkSize: _chunkSize,
|
|
30
|
+
maxChunkSize: _chunkSize,
|
|
31
|
+
burstSeconds: 0.20,
|
|
32
|
+
congestedLatencyMs: 250,
|
|
33
|
+
unstableLatencyMs: 1000,
|
|
34
|
+
congestedFailureRate: 0.10,
|
|
35
|
+
unstableFailureRate: 0.30,
|
|
36
|
+
sampleWindowSize: 20,
|
|
37
|
+
smoothingFactor: 0.25,
|
|
38
|
+
throughputProbeIntervalMs: 750,
|
|
39
|
+
throughputProbeGrowthFactor: 1.18,
|
|
40
|
+
healthyThroughputRatio: 0.90,
|
|
41
|
+
degradedThroughputRatio: 0.72,
|
|
42
|
+
...networkTraffic
|
|
43
|
+
});
|
|
19
44
|
}
|
|
20
45
|
async sendFile(file, {
|
|
21
46
|
fileDetails,
|
|
@@ -35,6 +60,7 @@ class FileTransferManager {
|
|
|
35
60
|
recipientID,
|
|
36
61
|
onSendChunk,
|
|
37
62
|
onComplete,
|
|
63
|
+
onFailure,
|
|
38
64
|
onProgress,
|
|
39
65
|
onStart,
|
|
40
66
|
onTiding
|
|
@@ -50,8 +76,8 @@ class FileTransferManager {
|
|
|
50
76
|
currentChunk: 0,
|
|
51
77
|
size: fileDetails.size
|
|
52
78
|
});
|
|
79
|
+
const transferState = this.activeTransfers.get(transferId);
|
|
53
80
|
const updateProgress = () => {
|
|
54
|
-
const transferState = this.activeTransfers.get(transferId);
|
|
55
81
|
if (onProgress) {
|
|
56
82
|
onProgress(transferState.completedChunks / transferState.totalChunks * 100, this.chunkSize);
|
|
57
83
|
}
|
|
@@ -62,31 +88,34 @@ class FileTransferManager {
|
|
|
62
88
|
const sendChunk = async chunkIndex => {
|
|
63
89
|
const start = chunkIndex * this.chunkSize;
|
|
64
90
|
const end = Math.min(start + this.chunkSize, fileDetails.size);
|
|
91
|
+
const actualChunkSize = end - start;
|
|
92
|
+
await _assertClassBrand(_FileTransferManager_brand, this, _waitForChunkAdmission).call(this, actualChunkSize);
|
|
65
93
|
const {
|
|
66
94
|
buffer
|
|
67
|
-
} = await fileHandle.read(Buffer.alloc(
|
|
68
|
-
|
|
95
|
+
} = await fileHandle.read(Buffer.alloc(actualChunkSize), 0, actualChunkSize, start);
|
|
96
|
+
transferState.activeChunks++;
|
|
69
97
|
chunkBuffer.set(chunkIndex, buffer);
|
|
70
98
|
try {
|
|
71
99
|
while (chunkBuffer.has(nextExpectedChunk)) {
|
|
72
100
|
const nextChunkBuffer = chunkBuffer.get(nextExpectedChunk);
|
|
73
|
-
|
|
101
|
+
const nextChunkSize = nextChunkBuffer.length;
|
|
102
|
+
await onSendChunk(transferId, nextExpectedChunk, nextChunkSize, nextChunkBuffer, totalChunks, transferState.completedChunks + 1);
|
|
74
103
|
chunkBuffer.delete(nextExpectedChunk);
|
|
75
104
|
nextExpectedChunk++;
|
|
76
|
-
transferState.transferredSize +=
|
|
105
|
+
transferState.transferredSize += nextChunkSize;
|
|
77
106
|
transferState.completedChunks++;
|
|
78
107
|
updateProgress();
|
|
79
108
|
}
|
|
80
109
|
if (transferState.completedChunks === transferState.totalChunks) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
110
|
+
if (onComplete) {
|
|
111
|
+
await onComplete(transferId);
|
|
112
|
+
}
|
|
84
113
|
}
|
|
85
114
|
} catch (error) {
|
|
86
115
|
console.error(`[FileTide ~ Transporter] ~ Did not send chunk ${chunkIndex} ~ ${error.message}`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
throw error;
|
|
117
|
+
} finally {
|
|
118
|
+
transferState.activeChunks--;
|
|
90
119
|
}
|
|
91
120
|
};
|
|
92
121
|
const sendChunkBatch = async (startChunkIndex, batchSize = 100, miniBatchSize = 10) => {
|
|
@@ -97,41 +126,51 @@ class FileTransferManager {
|
|
|
97
126
|
miniBatchPromises.push(sendChunk(chunkIndex));
|
|
98
127
|
}
|
|
99
128
|
await Promise.all(miniBatchPromises);
|
|
100
|
-
await new Promise(resolve =>
|
|
129
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
101
130
|
}
|
|
102
131
|
};
|
|
103
|
-
const transferState = this.activeTransfers.get(transferId);
|
|
104
132
|
let currentBatchStart = transferState.currentChunk;
|
|
105
133
|
const keepAliveInterval = setInterval(() => {
|
|
106
|
-
if (onTiding)
|
|
134
|
+
if (onTiding) {
|
|
135
|
+
onTiding();
|
|
136
|
+
}
|
|
107
137
|
}, 5000);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
adaptiveBatchSize = Math.max(adaptiveBatchSize - 10, 10);
|
|
116
|
-
}
|
|
117
|
-
await sendChunkBatch(currentBatchStart, adaptiveBatchSize);
|
|
118
|
-
currentBatchStart += adaptiveBatchSize;
|
|
119
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
120
|
-
}
|
|
121
|
-
} finally {
|
|
122
|
-
clearInterval(keepAliveInterval);
|
|
123
|
-
fileHandle.on("close", resolve);
|
|
124
|
-
fileHandle.on("error", error => {
|
|
125
|
-
this.activeTransfers.delete(transferId);
|
|
126
|
-
reject(error);
|
|
127
|
-
});
|
|
128
|
-
try {
|
|
129
|
-
await fileHandle.close();
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error(`[FileTide ~ Transporter] ~ Error closing file handle ~ ${error.message}`);
|
|
138
|
+
try {
|
|
139
|
+
let adaptiveBatchSize = 100;
|
|
140
|
+
while (currentBatchStart < totalChunks) {
|
|
141
|
+
if (transferState.activeChunks < this.parallelChunks) {
|
|
142
|
+
adaptiveBatchSize = Math.min(adaptiveBatchSize + 10, 100);
|
|
143
|
+
} else {
|
|
144
|
+
adaptiveBatchSize = Math.max(adaptiveBatchSize - 10, 10);
|
|
132
145
|
}
|
|
146
|
+
await sendChunkBatch(currentBatchStart, adaptiveBatchSize);
|
|
147
|
+
currentBatchStart += adaptiveBatchSize;
|
|
148
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
133
149
|
}
|
|
134
|
-
})
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (onFailure) {
|
|
152
|
+
await onFailure(transferId, error);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
} finally {
|
|
156
|
+
clearInterval(keepAliveInterval);
|
|
157
|
+
this.activeTransfers.delete(transferId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
getNetworkTrafficStatus() {
|
|
161
|
+
return this.networkTraffic.getStatus();
|
|
162
|
+
}
|
|
163
|
+
updateNetworkTraffic(sample = {}) {
|
|
164
|
+
return this.networkTraffic.updateNetworkSample(sample);
|
|
165
|
+
}
|
|
166
|
+
recordNetworkThroughput(throughputBps) {
|
|
167
|
+
return this.networkTraffic.recordDeliveredThroughput(throughputBps);
|
|
168
|
+
}
|
|
169
|
+
pauseTransfers(reason = "requested") {
|
|
170
|
+
return this.networkTraffic.pause(reason);
|
|
171
|
+
}
|
|
172
|
+
resumeTransfers(reason = "requested") {
|
|
173
|
+
return this.networkTraffic.resume(reason);
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
176
|
async function _startTransfer(file, transferId, fileDetails, originDetails, onSendChunk, onComplete, onProgress, onStart, onTiding, clientTide) {
|
|
@@ -158,9 +197,11 @@ async function _startTransfer(file, transferId, fileDetails, originDetails, onSe
|
|
|
158
197
|
const start = chunkIndex * this.chunkSize;
|
|
159
198
|
const end = Math.min(start + this.chunkSize, file.length);
|
|
160
199
|
const chunk = file.slice(start, end);
|
|
200
|
+
const actualChunkSize = chunk.length;
|
|
201
|
+
await _assertClassBrand(_FileTransferManager_brand, this, _waitForChunkAdmission).call(this, actualChunkSize);
|
|
161
202
|
transferState.activeChunks++;
|
|
162
203
|
try {
|
|
163
|
-
await onSendChunk(transferId, chunkIndex,
|
|
204
|
+
await onSendChunk(transferId, chunkIndex, actualChunkSize, chunk, totalChunks, transferState.completedChunks + 1);
|
|
164
205
|
chunkBuffer.set(chunkIndex, chunk);
|
|
165
206
|
while (chunkBuffer.has(nextExpectedChunk)) {
|
|
166
207
|
chunkBuffer.delete(nextExpectedChunk);
|
|
@@ -214,121 +255,13 @@ async function _startTransfer(file, transferId, fileDetails, originDetails, onSe
|
|
|
214
255
|
clearInterval(keepAliveInterval);
|
|
215
256
|
}
|
|
216
257
|
}
|
|
217
|
-
/*async #startTransfer(file, transferId, fileDetails, originDetails, onSendChunk, onComplete, onProgress, onStart, onTiding, clientTide)
|
|
218
|
-
{
|
|
219
|
-
const totalChunks = Math.ceil(file.length / this.chunkSize);
|
|
220
|
-
|
|
221
|
-
this.activeTransfers.set(transferId, {
|
|
222
|
-
totalChunks,
|
|
223
|
-
transferredSize : 0,
|
|
224
|
-
completedChunks : 0,
|
|
225
|
-
activeChunks : 0,
|
|
226
|
-
failedChunks : new Map(),
|
|
227
|
-
currentChunk : 0,
|
|
228
|
-
size : fileDetails.size
|
|
229
|
-
});
|
|
230
|
-
const updateProgress = () => {
|
|
231
|
-
const transferState = this.activeTransfers.get(transferId);
|
|
232
|
-
|
|
233
|
-
if (onProgress)
|
|
234
|
-
{
|
|
235
|
-
onProgress((transferState.completedChunks / transferState.totalChunks) * 100, this.chunkSize);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const keepAliveInterval = setInterval(() => {
|
|
240
|
-
if (onTiding) onTiding();
|
|
241
|
-
}, 5000);
|
|
242
|
-
|
|
243
|
-
const exponentialBackoff = (retryCount) => Math.min(1000 * (2 ** retryCount), 10000);
|
|
244
|
-
|
|
245
|
-
return new Promise((resolve, reject) => {
|
|
246
|
-
const sendChunk = async (chunkIndex) => {
|
|
247
|
-
const transferState = this.activeTransfers.get(transferId);
|
|
248
|
-
|
|
249
|
-
if (chunkIndex >= transferState.totalChunks) { return; }
|
|
250
|
-
const start = chunkIndex * this.chunkSize;
|
|
251
|
-
|
|
252
|
-
const end = Math.min(start + this.chunkSize, file.length);
|
|
253
|
-
|
|
254
|
-
const chunk = file.slice(start, end);
|
|
255
|
-
|
|
256
|
-
if (onStart && chunkIndex === 0)
|
|
257
|
-
{
|
|
258
|
-
onStart({
|
|
259
|
-
name : fileDetails.name,
|
|
260
|
-
path : fileDetails.path,
|
|
261
|
-
size : fileDetails.size,
|
|
262
|
-
totalChunks : transferState.totalChunks
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
transferState.activeChunks++;
|
|
267
|
-
|
|
268
|
-
try
|
|
269
|
-
{
|
|
270
|
-
await onSendChunk(transferId, chunkIndex, this.chunkSize, chunk, transferState.totalChunks, transferState.completedChunks + 1);
|
|
271
|
-
|
|
272
|
-
transferState.transferredSize += this.chunkSize;
|
|
273
|
-
|
|
274
|
-
transferState.completedChunks++;
|
|
275
|
-
|
|
276
|
-
transferState.activeChunks--;
|
|
277
|
-
|
|
278
|
-
updateProgress();
|
|
279
|
-
|
|
280
|
-
if (transferState.activeChunks < this.parallelChunks && transferState.currentChunk < transferState.totalChunks)
|
|
281
|
-
{
|
|
282
|
-
sendChunk(transferState.currentChunk++);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (transferState.completedChunks === transferState.totalChunks)
|
|
286
|
-
{
|
|
287
|
-
await onComplete(transferId);
|
|
288
|
-
|
|
289
|
-
this.activeTransfers.delete(transferId);
|
|
290
|
-
|
|
291
|
-
clearInterval(keepAliveInterval);
|
|
292
|
-
|
|
293
|
-
resolve();
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch (error)
|
|
297
|
-
{
|
|
298
|
-
console.error(`[FileTide ~ File Messager] ~ Did not send chunk ${chunkIndex} ~ ${error.message}`);
|
|
299
|
-
|
|
300
|
-
transferState.activeChunks--;
|
|
301
|
-
|
|
302
|
-
const retryCount = (transferState.failedChunks.get(chunkIndex) || 0) + 1;
|
|
303
|
-
|
|
304
|
-
if (retryCount > this.maxRetries)
|
|
305
|
-
{
|
|
306
|
-
this.activeTransfers.delete(transferId);
|
|
307
|
-
|
|
308
|
-
clearInterval(keepAliveInterval);
|
|
309
|
-
|
|
310
|
-
reject(new Error(`Did not send chunk ${chunkIndex} after ${this.maxRetries} retries.`));
|
|
311
|
-
}
|
|
312
|
-
else
|
|
313
|
-
{
|
|
314
|
-
transferState.failedChunks.set(chunkIndex, retryCount);
|
|
315
|
-
|
|
316
|
-
setTimeout(() => sendChunk(chunkIndex), exponentialBackoff(retryCount));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const transferState = this.activeTransfers.get(transferId);
|
|
322
|
-
|
|
323
|
-
for (let i = 0; i < Math.min(this.parallelChunks, transferState.totalChunks); i++)
|
|
324
|
-
{
|
|
325
|
-
sendChunk(transferState.currentChunk++);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}*/
|
|
329
258
|
function _generateTransferId(file) {
|
|
330
259
|
return `${file.name}-${Date.now()}`;
|
|
331
260
|
}
|
|
261
|
+
function _waitForChunkAdmission(chunkSize) {
|
|
262
|
+
return this.networkTraffic.waitForChunkAdmission(chunkSize);
|
|
263
|
+
}
|
|
264
|
+
;
|
|
332
265
|
module.exports = {
|
|
333
266
|
FileTransferManager
|
|
334
267
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
|
|
4
|
+
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
|
|
5
|
+
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"); }
|
|
3
6
|
const path = require("path");
|
|
4
7
|
const fs = require("fs");
|
|
5
8
|
const {
|
|
@@ -11,14 +14,18 @@ const {
|
|
|
11
14
|
const {
|
|
12
15
|
FileUtilityManager
|
|
13
16
|
} = require("./FileUtilityManager.js");
|
|
17
|
+
var _FileTransferReceiverManager_brand = /*#__PURE__*/new WeakSet();
|
|
14
18
|
class FileTransferReceiverManager {
|
|
15
|
-
constructor() {
|
|
19
|
+
constructor() {
|
|
20
|
+
_classPrivateMethodInitSpec(this, _FileTransferReceiverManager_brand);
|
|
21
|
+
this.tideQueues = new Map();
|
|
22
|
+
}
|
|
16
23
|
async initializeTransfer(data, onError) {
|
|
17
24
|
const savePath = FileUtilityManager.getTidePath(path.join(data.path || data.filePath, data.fileName));
|
|
18
25
|
FileTideDebugUtilityManager.log("notice", true, `Initializing transfer ~ ${data.fileName} | ${savePath}...`);
|
|
19
26
|
if (FileTransferTrackingManager.containsTransfer(data.fileName)) {
|
|
20
27
|
FileTideDebugUtilityManager.log("notice", true, `Transfer ~ ${data.fileName} | ${savePath} already initialized.`);
|
|
21
|
-
return;
|
|
28
|
+
return FileTransferTrackingManager.getTransfer(data.fileName);
|
|
22
29
|
}
|
|
23
30
|
try {
|
|
24
31
|
const dir = path.dirname(savePath);
|
|
@@ -31,29 +38,70 @@ class FileTransferReceiverManager {
|
|
|
31
38
|
flags: "w"
|
|
32
39
|
});
|
|
33
40
|
FileTransferTrackingManager.trackTransfer(data, fileStream);
|
|
41
|
+
const transferState = FileTransferTrackingManager.getTransfer(data.fileName);
|
|
42
|
+
if (transferState && transferState.highestContiguousChunk === undefined) {
|
|
43
|
+
transferState.highestContiguousChunk = -1;
|
|
44
|
+
}
|
|
34
45
|
FileTideDebugUtilityManager.log("notice", true, `Initialized transfer ~ ${data.fileName} | ${savePath}!`);
|
|
46
|
+
return transferState;
|
|
35
47
|
} catch (error) {
|
|
36
48
|
FileTideDebugUtilityManager.log("error", true, `Did not initialize transfer ~ ${data.fileName} | ${savePath}.\n\t\t${error}`);
|
|
37
49
|
if (onError) {
|
|
38
50
|
onError(`[${data.fileName}] Did not initialize transfer ~ ${error.message}`);
|
|
39
51
|
}
|
|
52
|
+
throw error;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
async handleTransferProgress(data, onError) {
|
|
43
56
|
let transferState = FileTransferTrackingManager.getTransfer(data.fileName);
|
|
44
57
|
if (!transferState) {
|
|
45
|
-
await this.initializeTransfer(data);
|
|
46
|
-
|
|
58
|
+
transferState = await this.initializeTransfer(data, onError);
|
|
59
|
+
}
|
|
60
|
+
if (transferState.highestContiguousChunk === undefined) {
|
|
61
|
+
transferState.highestContiguousChunk = -1;
|
|
47
62
|
}
|
|
48
|
-
const chunkBuffer = Buffer.from(data.fileChunk);
|
|
49
63
|
try {
|
|
50
|
-
transferState.
|
|
51
|
-
|
|
64
|
+
const expectedChunkIndex = transferState.highestContiguousChunk + 1;
|
|
65
|
+
if (data.chunkIndex !== expectedChunkIndex) {
|
|
66
|
+
throw new Error(`[${data.fileName}] Out-of-order chunk. Expected ${expectedChunkIndex}, received ${data.chunkIndex}.`);
|
|
67
|
+
}
|
|
68
|
+
await _assertClassBrand(_FileTransferReceiverManager_brand, this, _writeChunk).call(this, transferState, data);
|
|
69
|
+
return {
|
|
70
|
+
highestContiguousChunk: transferState.highestContiguousChunk,
|
|
71
|
+
receivedChunks: transferState.receivedChunks.size,
|
|
72
|
+
totalChunks: transferState.totalChunks
|
|
73
|
+
};
|
|
52
74
|
} catch (error) {
|
|
53
75
|
FileTideDebugUtilityManager.log("error", true, `Did not write chunk ${data.chunkIndex} ~ ${data.fileName}.\n\t\t${error}`);
|
|
54
76
|
if (onError) {
|
|
55
77
|
onError(`[${data.fileName}] Did not write chunk ${data.chunkIndex} ~ ${error.message}`);
|
|
56
78
|
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async handleTransferTide(tide, onError) {
|
|
83
|
+
const transferKey = tide?.transferId || tide?.fileName;
|
|
84
|
+
if (!transferKey) {
|
|
85
|
+
throw new Error("Received Tide without a transfer identifier.");
|
|
86
|
+
}
|
|
87
|
+
const previousTide = this.tideQueues.get(transferKey) || Promise.resolve();
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* Serialize Tides for the same transfer, but do not allow one
|
|
91
|
+
* rejected Tide promise to poison the queue chain itself. The
|
|
92
|
+
* next Tide still runs its own validation against the authoritative
|
|
93
|
+
* contiguous receiver state and will fail explicitly if a gap remains.
|
|
94
|
+
*/
|
|
95
|
+
const currentTide = previousTide.catch(() => undefined).then(() => {
|
|
96
|
+
return _assertClassBrand(_FileTransferReceiverManager_brand, this, _processTransferTide).call(this, tide, onError);
|
|
97
|
+
});
|
|
98
|
+
this.tideQueues.set(transferKey, currentTide);
|
|
99
|
+
try {
|
|
100
|
+
return await currentTide;
|
|
101
|
+
} finally {
|
|
102
|
+
if (this.tideQueues.get(transferKey) === currentTide) {
|
|
103
|
+
this.tideQueues.delete(transferKey);
|
|
104
|
+
}
|
|
57
105
|
}
|
|
58
106
|
}
|
|
59
107
|
async completeTransfer(data, onComplete) {
|
|
@@ -67,8 +115,18 @@ class FileTransferReceiverManager {
|
|
|
67
115
|
try {
|
|
68
116
|
FileTideDebugUtilityManager.log("notice", true, `Verifying transfer state ~ ${data.fileName} completion (${transferState.receivedChunks.size}/${transferState.totalChunks})...`);
|
|
69
117
|
FileTideDebugUtilityManager.log("notice", true, `Transfer state ~ ${data.fileName} completed ~ (${transferState.receivedChunks.size === transferState.totalChunks}).`);
|
|
70
|
-
if (transferState.receivedChunks.size === transferState.totalChunks) {
|
|
71
|
-
|
|
118
|
+
if (transferState.receivedChunks.size === transferState.totalChunks && transferState.highestContiguousChunk === transferState.totalChunks - 1) {
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
const onError = error => {
|
|
121
|
+
transferState.fileStream.removeListener("error", onError);
|
|
122
|
+
reject(error);
|
|
123
|
+
};
|
|
124
|
+
transferState.fileStream.once("error", onError);
|
|
125
|
+
transferState.fileStream.end(() => {
|
|
126
|
+
transferState.fileStream.removeListener("error", onError);
|
|
127
|
+
resolve();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
72
130
|
console.log(`[${data.fileName}] Transfer complete. All chunks received and file written.`);
|
|
73
131
|
FileTransferTrackingManager.untrackTransfer(data.fileName);
|
|
74
132
|
FileTideDebugUtilityManager.log("notice", true, `Cleared transfer state ~ ${data.fileName}.`);
|
|
@@ -86,6 +144,108 @@ class FileTransferReceiverManager {
|
|
|
86
144
|
}
|
|
87
145
|
}
|
|
88
146
|
}
|
|
147
|
+
async function _waitForDrain(fileStream) {
|
|
148
|
+
return await new Promise((resolve, reject) => {
|
|
149
|
+
const onDrain = () => {
|
|
150
|
+
cleanup();
|
|
151
|
+
resolve();
|
|
152
|
+
};
|
|
153
|
+
const onError = error => {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(error);
|
|
156
|
+
};
|
|
157
|
+
const cleanup = () => {
|
|
158
|
+
fileStream.removeListener("drain", onDrain);
|
|
159
|
+
fileStream.removeListener("error", onError);
|
|
160
|
+
};
|
|
161
|
+
fileStream.once("drain", onDrain);
|
|
162
|
+
fileStream.once("error", onError);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function _writeChunk(transferState, data) {
|
|
166
|
+
const chunkBuffer = Buffer.isBuffer(data.fileChunk) ? data.fileChunk : Buffer.from(data.fileChunk);
|
|
167
|
+
const canContinue = transferState.fileStream.write(chunkBuffer);
|
|
168
|
+
if (!canContinue) {
|
|
169
|
+
await _assertClassBrand(_FileTransferReceiverManager_brand, this, _waitForDrain).call(this, transferState.fileStream);
|
|
170
|
+
}
|
|
171
|
+
transferState.receivedChunks.add(data.chunkIndex);
|
|
172
|
+
while (transferState.receivedChunks.has(transferState.highestContiguousChunk + 1)) {
|
|
173
|
+
transferState.highestContiguousChunk++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function _processTransferTide(tide, onError) {
|
|
177
|
+
const {
|
|
178
|
+
transferId,
|
|
179
|
+
senderID,
|
|
180
|
+
recipientId,
|
|
181
|
+
fileName,
|
|
182
|
+
filePath,
|
|
183
|
+
fileSize,
|
|
184
|
+
totalChunks,
|
|
185
|
+
firstChunkIndex,
|
|
186
|
+
lastChunkIndex,
|
|
187
|
+
chunks = [],
|
|
188
|
+
metadata = {}
|
|
189
|
+
} = tide;
|
|
190
|
+
if (!Array.isArray(chunks) || chunks.length <= 0) {
|
|
191
|
+
throw new Error(`[${fileName}] Received an empty Tide.`);
|
|
192
|
+
}
|
|
193
|
+
if (firstChunkIndex !== chunks[0].chunkIndex || lastChunkIndex !== chunks[chunks.length - 1].chunkIndex) {
|
|
194
|
+
throw new Error(`[${fileName}] Tide boundary metadata does not match its chunks.`);
|
|
195
|
+
}
|
|
196
|
+
let expectedChunkIndex = firstChunkIndex;
|
|
197
|
+
for (const chunk of chunks) {
|
|
198
|
+
if (chunk.chunkIndex !== expectedChunkIndex) {
|
|
199
|
+
throw new Error(`[${fileName}] Tide ordering violation. Expected chunk ${expectedChunkIndex}, received ${chunk.chunkIndex}.`);
|
|
200
|
+
}
|
|
201
|
+
expectedChunkIndex++;
|
|
202
|
+
}
|
|
203
|
+
let transferState = FileTransferTrackingManager.getTransfer(fileName);
|
|
204
|
+
if (!transferState) {
|
|
205
|
+
transferState = await this.initializeTransfer({
|
|
206
|
+
transferId,
|
|
207
|
+
senderID,
|
|
208
|
+
recipientId,
|
|
209
|
+
fileName,
|
|
210
|
+
filePath,
|
|
211
|
+
path: filePath,
|
|
212
|
+
fileSize,
|
|
213
|
+
totalChunks,
|
|
214
|
+
chunkSize: chunks[0].chunkSize,
|
|
215
|
+
metadata
|
|
216
|
+
}, onError);
|
|
217
|
+
}
|
|
218
|
+
const expectedTideStart = transferState.highestContiguousChunk + 1;
|
|
219
|
+
if (firstChunkIndex !== expectedTideStart) {
|
|
220
|
+
throw new Error(`[${fileName}] Tide ordering violation. Expected Tide to begin at chunk ${expectedTideStart}, received ${firstChunkIndex}.`);
|
|
221
|
+
}
|
|
222
|
+
let tideBytes = 0;
|
|
223
|
+
for (const chunk of chunks) {
|
|
224
|
+
await _assertClassBrand(_FileTransferReceiverManager_brand, this, _writeChunk).call(this, transferState, {
|
|
225
|
+
fileName,
|
|
226
|
+
fileChunk: chunk.fileChunk,
|
|
227
|
+
chunkIndex: chunk.chunkIndex
|
|
228
|
+
});
|
|
229
|
+
tideBytes += chunk.chunkSize;
|
|
230
|
+
}
|
|
231
|
+
const missingChunks = [];
|
|
232
|
+
for (let index = firstChunkIndex; index <= lastChunkIndex; index++) {
|
|
233
|
+
if (!transferState.receivedChunks.has(index)) {
|
|
234
|
+
missingChunks.push(index);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
transferId,
|
|
239
|
+
firstChunkIndex,
|
|
240
|
+
lastChunkIndex,
|
|
241
|
+
highestContiguousChunk: transferState.highestContiguousChunk,
|
|
242
|
+
tideChunkCount: chunks.length,
|
|
243
|
+
tideBytes,
|
|
244
|
+
completedChunks: transferState.highestContiguousChunk + 1,
|
|
245
|
+
totalChunks: transferState.totalChunks,
|
|
246
|
+
missingChunks
|
|
247
|
+
};
|
|
248
|
+
}
|
|
89
249
|
module.exports = {
|
|
90
250
|
FileTransferReceiverManager
|
|
91
251
|
};
|