teyyare 0.2.7 → 0.2.8
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/bin/teyyare.js +33 -0
- package/package.json +6 -2
- package/src/core/protocol.js +39 -6
- package/src/index.js +119 -57
- package/src/receiver/pipeline.js +272 -57
- package/src/sender/pipeline.js +4 -3
- package/test/e2e/e2e.js +276 -0
- package/test/integration/pull.test.js +51 -0
- package/test/integration/resume.test.js +62 -0
- package/test/integration/single-file.test.js +85 -0
- package/test/unit/protocol.test.js +20 -0
package/src/receiver/pipeline.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, createReadStream } from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
11
|
import { crc32 } from '../core/crc32.js';
|
|
12
|
-
import { Opcodes, unpackChunkMeta, unpackFileBegin, unpackTransferBegin, unpackFileEnd, unpackPullError } from '../core/protocol.js';
|
|
12
|
+
import { Opcodes, unpackChunkMeta, unpackFileBegin, unpackTransferBegin, unpackFileEnd, unpackTransferEnd, unpackPullError } from '../core/protocol.js';
|
|
13
13
|
import { unpackGeneration } from '../core/generation.js';
|
|
14
14
|
import { FileResumeState } from '../resume/state.js';
|
|
15
15
|
|
|
@@ -38,11 +38,15 @@ export class ReceiverPipeline {
|
|
|
38
38
|
this.activeFiles = new Map(); // fileId -> { destPath, partPath, statePath, fileHandle, resumeState, ... }
|
|
39
39
|
this.completedFiles = new Set();
|
|
40
40
|
this._pendingChunks = new Map(); // fileId -> Array<{ key, val }>
|
|
41
|
+
this._pendingFileEnds = new Map(); // fileId -> unpacked FileEnd meta
|
|
41
42
|
this._pendingTransferEnd = false;
|
|
42
43
|
this.transferFinished = false;
|
|
43
44
|
this.transferError = null;
|
|
44
45
|
this._finishResolvers = [];
|
|
45
46
|
this._errorRejecters = [];
|
|
47
|
+
this.currentTransferId = null;
|
|
48
|
+
this.expectedFileCount = null;
|
|
49
|
+
this._completedFileCount = 0;
|
|
46
50
|
|
|
47
51
|
// Processing queue to ensure strictly ordered, serialized generation processing
|
|
48
52
|
this._batchQueue = [];
|
|
@@ -126,7 +130,8 @@ export class ReceiverPipeline {
|
|
|
126
130
|
const entries = unpackGeneration(metaBuf, payloadBuf);
|
|
127
131
|
const count = entries.length;
|
|
128
132
|
|
|
129
|
-
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (i < count) {
|
|
130
135
|
const { type, key, val } = entries[i];
|
|
131
136
|
|
|
132
137
|
if (this.metrics) {
|
|
@@ -135,34 +140,59 @@ export class ReceiverPipeline {
|
|
|
135
140
|
|
|
136
141
|
switch (type) {
|
|
137
142
|
case Opcodes.TRANSFER_BEGIN: {
|
|
138
|
-
this.transferFinished = false;
|
|
139
|
-
this.transferError = null;
|
|
140
|
-
this._pendingTransferEnd = false;
|
|
141
143
|
const tinfo = unpackTransferBegin(key);
|
|
144
|
+
if (this.currentTransferId !== tinfo.transferId) {
|
|
145
|
+
if (!this.currentTransferId) {
|
|
146
|
+
this.currentTransferId = tinfo.transferId;
|
|
147
|
+
} else {
|
|
148
|
+
this.currentTransferId = tinfo.transferId;
|
|
149
|
+
this.transferFinished = false;
|
|
150
|
+
this.transferError = null;
|
|
151
|
+
this._pendingTransferEnd = false;
|
|
152
|
+
this.completedFiles.clear();
|
|
153
|
+
this._completedFileCount = 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.expectedFileCount = tinfo.fileCount;
|
|
157
|
+
this.expectedTotalBytes = tinfo.totalBytes;
|
|
158
|
+
this._checkTransferCompletion();
|
|
159
|
+
i++;
|
|
142
160
|
break;
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
case Opcodes.FILE_BEGIN: {
|
|
146
164
|
await this._handleFileBegin(key);
|
|
165
|
+
i++;
|
|
147
166
|
break;
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
case Opcodes.CHUNK: {
|
|
151
|
-
|
|
170
|
+
const chunkBatch = [];
|
|
171
|
+
while (i < count && entries[i].type === Opcodes.CHUNK) {
|
|
172
|
+
chunkBatch.push(entries[i]);
|
|
173
|
+
if (chunkBatch.length > 1 && this.metrics) {
|
|
174
|
+
this.metrics.mutations++;
|
|
175
|
+
}
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
await this._handleChunks(chunkBatch);
|
|
152
179
|
break;
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
case Opcodes.FILE_END: {
|
|
156
183
|
await this._handleFileEnd(key);
|
|
184
|
+
i++;
|
|
157
185
|
break;
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
case Opcodes.TRANSFER_END: {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this._pendingTransferEnd = true;
|
|
189
|
+
const tend = unpackTransferEnd(key);
|
|
190
|
+
if (!this.currentTransferId) {
|
|
191
|
+
this.currentTransferId = tend.transferId;
|
|
165
192
|
}
|
|
193
|
+
this._pendingTransferEnd = true;
|
|
194
|
+
this._checkTransferCompletion();
|
|
195
|
+
i++;
|
|
166
196
|
break;
|
|
167
197
|
}
|
|
168
198
|
|
|
@@ -177,10 +207,24 @@ export class ReceiverPipeline {
|
|
|
177
207
|
this._errorRejecters = [];
|
|
178
208
|
return;
|
|
179
209
|
}
|
|
210
|
+
|
|
211
|
+
default:
|
|
212
|
+
i++;
|
|
213
|
+
break;
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
}
|
|
183
217
|
|
|
218
|
+
_checkTransferCompletion() {
|
|
219
|
+
if (!this._pendingTransferEnd) return;
|
|
220
|
+
if (this.activeFiles.size > 0) return;
|
|
221
|
+
if (this._pendingFileEnds.size > 0) return;
|
|
222
|
+
if (this.expectedFileCount !== null && this._completedFileCount < this.expectedFileCount) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this._finishTransfer();
|
|
226
|
+
}
|
|
227
|
+
|
|
184
228
|
_finishTransfer() {
|
|
185
229
|
this.transferFinished = true;
|
|
186
230
|
for (const resolve of this._finishResolvers) {
|
|
@@ -204,12 +248,22 @@ export class ReceiverPipeline {
|
|
|
204
248
|
resumeState = new FileResumeState({
|
|
205
249
|
fileId: info.fileId,
|
|
206
250
|
fileSize: Number(info.fileSize),
|
|
251
|
+
chunkSize: info.chunkSize || 1048576,
|
|
207
252
|
sha256: info.sha256
|
|
208
253
|
});
|
|
209
254
|
await resumeState.save(statePath);
|
|
255
|
+
} else if (info.chunkSize && resumeState.chunkSize !== info.chunkSize) {
|
|
256
|
+
resumeState.chunkSize = info.chunkSize;
|
|
210
257
|
}
|
|
211
258
|
|
|
212
259
|
const fileHandle = await fs.open(partPath, 'a+');
|
|
260
|
+
const targetSize = Number(info.fileSize);
|
|
261
|
+
if (targetSize > 0) {
|
|
262
|
+
const currentStat = await fileHandle.stat();
|
|
263
|
+
if (currentStat.size < targetSize) {
|
|
264
|
+
await fileHandle.truncate(targetSize);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
213
267
|
|
|
214
268
|
const file = {
|
|
215
269
|
fileId: info.fileId,
|
|
@@ -222,6 +276,9 @@ export class ReceiverPipeline {
|
|
|
222
276
|
fileSize: Number(info.fileSize),
|
|
223
277
|
sha256: info.sha256,
|
|
224
278
|
chunksWrittenSinceSave: 0,
|
|
279
|
+
lastStateSaveTime: Date.now(),
|
|
280
|
+
_stateSaving: false,
|
|
281
|
+
_pendingStateSave: false,
|
|
225
282
|
pendingEnd: false,
|
|
226
283
|
endMeta: null
|
|
227
284
|
};
|
|
@@ -246,9 +303,19 @@ export class ReceiverPipeline {
|
|
|
246
303
|
const pending = this._pendingChunks.get(info.fileId);
|
|
247
304
|
if (pending && pending.length > 0) {
|
|
248
305
|
this._pendingChunks.delete(info.fileId);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
306
|
+
await this._writeChunkRun(file, pending);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if FILE_END arrived out-of-order before FILE_BEGIN
|
|
310
|
+
const pendingEnd = this._pendingFileEnds.get(info.fileId);
|
|
311
|
+
if (pendingEnd) {
|
|
312
|
+
this._pendingFileEnds.delete(info.fileId);
|
|
313
|
+
if (pendingEnd.sha256) file.sha256 = pendingEnd.sha256;
|
|
314
|
+
if (file.fileSize === 0 || file.resumeState.isComplete()) {
|
|
315
|
+
await this._finalizeFile(file);
|
|
316
|
+
} else {
|
|
317
|
+
file.pendingEnd = true;
|
|
318
|
+
file.endMeta = pendingEnd;
|
|
252
319
|
}
|
|
253
320
|
}
|
|
254
321
|
}
|
|
@@ -265,56 +332,164 @@ export class ReceiverPipeline {
|
|
|
265
332
|
pending = [];
|
|
266
333
|
this._pendingChunks.set(meta.fileId, pending);
|
|
267
334
|
}
|
|
268
|
-
pending.push({
|
|
335
|
+
pending.push({ meta, val: new Uint8Array(val) });
|
|
269
336
|
return;
|
|
270
337
|
}
|
|
271
338
|
|
|
272
|
-
await this.
|
|
339
|
+
await this._writeChunkRun(file, [{ meta, val }]);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async _handleChunks(items) {
|
|
343
|
+
if (!items || items.length === 0) return;
|
|
344
|
+
|
|
345
|
+
// Group items by fileId
|
|
346
|
+
const groups = new Map();
|
|
347
|
+
for (const item of items) {
|
|
348
|
+
const meta = item.meta || unpackChunkMeta(item.key);
|
|
349
|
+
let list = groups.get(meta.fileId);
|
|
350
|
+
if (!list) {
|
|
351
|
+
list = [];
|
|
352
|
+
groups.set(meta.fileId, list);
|
|
353
|
+
}
|
|
354
|
+
list.push({ meta, val: item.val });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const [fileId, chunkList] of groups.entries()) {
|
|
358
|
+
const file = this.activeFiles.get(fileId);
|
|
359
|
+
if (!file) {
|
|
360
|
+
if (this.completedFiles.has(fileId)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
let pending = this._pendingChunks.get(fileId);
|
|
364
|
+
if (!pending) {
|
|
365
|
+
pending = [];
|
|
366
|
+
this._pendingChunks.set(fileId, pending);
|
|
367
|
+
}
|
|
368
|
+
for (const c of chunkList) {
|
|
369
|
+
pending.push({ meta: c.meta, val: new Uint8Array(c.val) });
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await this._writeChunkRun(file, chunkList);
|
|
375
|
+
}
|
|
273
376
|
}
|
|
274
377
|
|
|
275
378
|
async _writeChunk(file, meta, val) {
|
|
276
|
-
|
|
277
|
-
|
|
379
|
+
return this._writeChunkRun(file, [{ meta, val }]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async _writeChunkRun(file, items) {
|
|
383
|
+
if (!items || items.length === 0) return;
|
|
384
|
+
|
|
385
|
+
// 1. Unpack, verify CRC, and filter out resume hits
|
|
386
|
+
const toWrite = [];
|
|
387
|
+
for (const item of items) {
|
|
388
|
+
const meta = item.meta || unpackChunkMeta(item.key);
|
|
389
|
+
const val = item.val;
|
|
390
|
+
|
|
391
|
+
// Idempotency / resume hit check
|
|
392
|
+
if (file.resumeState.isVerified(meta.chunkIndex)) {
|
|
393
|
+
if (this.metrics) {
|
|
394
|
+
this.metrics.resumeHits++;
|
|
395
|
+
this.metrics.chunksVerified++;
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Per-chunk corruption detection
|
|
401
|
+
const t0 = Date.now();
|
|
402
|
+
const computedCrc = crc32(val);
|
|
278
403
|
if (this.metrics) {
|
|
279
|
-
this.metrics.
|
|
280
|
-
this.metrics.chunksVerified++;
|
|
404
|
+
this.metrics.checksumTimeMs += (Date.now() - t0);
|
|
281
405
|
}
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
406
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
407
|
+
if (computedCrc !== meta.checksum) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Chunk ${meta.chunkIndex} corruption: CRC 0x${computedCrc.toString(16)} !== 0x${meta.checksum.toString(16)}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
toWrite.push({ meta, val });
|
|
290
414
|
}
|
|
291
415
|
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
416
|
+
if (toWrite.length === 0) {
|
|
417
|
+
if (file.pendingEnd && (file.fileSize === 0 || file.resumeState.isComplete())) {
|
|
418
|
+
await this._finalizeFile(file);
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
296
421
|
}
|
|
297
422
|
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
423
|
+
// 2. Sort by offset to form maximal contiguous runs
|
|
424
|
+
toWrite.sort((a, b) => Number(a.meta.offset - b.meta.offset));
|
|
425
|
+
|
|
426
|
+
let currentRun = [toWrite[0]];
|
|
427
|
+
let currentStartOffset = Number(toWrite[0].meta.offset);
|
|
428
|
+
let expectedNextOffset = currentStartOffset + toWrite[0].meta.length;
|
|
429
|
+
|
|
430
|
+
const flushRun = async (run, startOffset) => {
|
|
431
|
+
// Artificial write delay for slow-receiver backpressure tests
|
|
432
|
+
if (this.writeDelayMs > 0) {
|
|
433
|
+
await new Promise((r) => setTimeout(r, this.writeDelayMs));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const writeStart = Date.now();
|
|
437
|
+
let runBytes = 0;
|
|
438
|
+
for (let i = 0; i < run.length; i++) {
|
|
439
|
+
runBytes += run[i].meta.length;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (run.length === 1) {
|
|
443
|
+
const item = run[0];
|
|
444
|
+
await file.fileHandle.write(item.val, 0, item.meta.length, Number(item.meta.offset));
|
|
445
|
+
} else {
|
|
446
|
+
const bufs = new Array(run.length);
|
|
447
|
+
for (let i = 0; i < run.length; i++) {
|
|
448
|
+
bufs[i] = run[i].val;
|
|
449
|
+
}
|
|
450
|
+
await file.fileHandle.writev(bufs, startOffset);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (this.metrics) {
|
|
454
|
+
this.metrics.diskWriteTimeMs += (Date.now() - writeStart);
|
|
455
|
+
this.metrics.fileBytesWritten += runBytes;
|
|
456
|
+
this.metrics.chunksCreated += run.length;
|
|
457
|
+
this.metrics.chunksVerified += run.length;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (let i = 0; i < run.length; i++) {
|
|
461
|
+
file.resumeState.markVerified(run[i].meta.chunkIndex);
|
|
462
|
+
file.chunksWrittenSinceSave++;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
for (let i = 1; i < toWrite.length; i++) {
|
|
467
|
+
const item = toWrite[i];
|
|
468
|
+
const itemOffset = Number(item.meta.offset);
|
|
469
|
+
|
|
470
|
+
if (itemOffset === expectedNextOffset) {
|
|
471
|
+
currentRun.push(item);
|
|
472
|
+
expectedNextOffset = itemOffset + item.meta.length;
|
|
473
|
+
} else {
|
|
474
|
+
await flushRun(currentRun, currentStartOffset);
|
|
475
|
+
currentRun = [item];
|
|
476
|
+
currentStartOffset = itemOffset;
|
|
477
|
+
expectedNextOffset = itemOffset + item.meta.length;
|
|
478
|
+
}
|
|
301
479
|
}
|
|
302
480
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
await file.fileHandle.write(val, 0, meta.length, Number(meta.offset));
|
|
306
|
-
if (this.metrics) {
|
|
307
|
-
this.metrics.diskWriteTimeMs += (Date.now() - writeStart);
|
|
308
|
-
this.metrics.fileBytesWritten += meta.length;
|
|
309
|
-
this.metrics.chunksCreated++;
|
|
310
|
-
this.metrics.chunksVerified++;
|
|
481
|
+
if (currentRun.length > 0) {
|
|
482
|
+
await flushRun(currentRun, currentStartOffset);
|
|
311
483
|
}
|
|
312
484
|
|
|
485
|
+
// 3. Update progress reporter
|
|
313
486
|
if (this.progress) {
|
|
314
487
|
const diskTimeSec = Math.max(0.001, (this.metrics ? this.metrics.diskWriteTimeMs : 1) / 1000);
|
|
315
488
|
const writtenMB = (this.metrics ? this.metrics.fileBytesWritten : 0) / (1024 * 1024);
|
|
316
489
|
const diskMBs = Number((writtenMB / diskTimeSec).toFixed(1));
|
|
317
|
-
|
|
490
|
+
for (const item of toWrite) {
|
|
491
|
+
file.maxChunkSize = Math.max(file.maxChunkSize || 0, item.meta.length);
|
|
492
|
+
}
|
|
318
493
|
const realTotalChunks = file.maxChunkSize > 0 && file.fileSize > 0 ? Math.ceil(file.fileSize / file.maxChunkSize) : 0;
|
|
319
494
|
this.progress.update({
|
|
320
495
|
fileName: file.relativePath,
|
|
@@ -326,27 +501,56 @@ export class ReceiverPipeline {
|
|
|
326
501
|
});
|
|
327
502
|
}
|
|
328
503
|
|
|
329
|
-
|
|
330
|
-
file
|
|
504
|
+
// 4. Trigger debounced state save
|
|
505
|
+
this._triggerStateSave(file);
|
|
331
506
|
|
|
332
|
-
|
|
333
|
-
file.chunksWrittenSinceSave = 0;
|
|
334
|
-
await file.resumeState.save(file.statePath);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// If FILE_END arrived earlier while chunks were still arriving
|
|
507
|
+
// 5. If FILE_END arrived earlier while chunks were still arriving
|
|
338
508
|
if (file.pendingEnd && (file.fileSize === 0 || file.resumeState.isComplete())) {
|
|
339
509
|
await this._finalizeFile(file);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
_triggerStateSave(file, force = false) {
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
const elapsed = now - file.lastStateSaveTime;
|
|
516
|
+
const shouldSave = force || file.chunksWrittenSinceSave >= 128 || (file.chunksWrittenSinceSave >= 16 && elapsed >= 1500);
|
|
517
|
+
if (!shouldSave) return;
|
|
518
|
+
|
|
519
|
+
if (file._stateSaving) {
|
|
520
|
+
file._pendingStateSave = true;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
file.chunksWrittenSinceSave = 0;
|
|
525
|
+
file.lastStateSaveTime = now;
|
|
526
|
+
file._stateSaving = true;
|
|
527
|
+
|
|
528
|
+
file.resumeState.save(file.statePath)
|
|
529
|
+
.catch(() => {})
|
|
530
|
+
.finally(() => {
|
|
531
|
+
file._stateSaving = false;
|
|
532
|
+
if (file._pendingStateSave) {
|
|
533
|
+
file._pendingStateSave = false;
|
|
534
|
+
this._triggerStateSave(file, true);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async _drainFileStateSave(file) {
|
|
540
|
+
while (file._stateSaving) {
|
|
541
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
343
542
|
}
|
|
344
543
|
}
|
|
345
544
|
|
|
346
545
|
async _handleFileEnd(key) {
|
|
347
546
|
const meta = unpackFileEnd(key);
|
|
348
547
|
const file = this.activeFiles.get(meta.fileId);
|
|
349
|
-
if (!file)
|
|
548
|
+
if (!file) {
|
|
549
|
+
if (!this.completedFiles.has(meta.fileId)) {
|
|
550
|
+
this._pendingFileEnds.set(meta.fileId, meta);
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
350
554
|
|
|
351
555
|
if (meta.sha256) {
|
|
352
556
|
file.sha256 = meta.sha256;
|
|
@@ -354,9 +558,6 @@ export class ReceiverPipeline {
|
|
|
354
558
|
|
|
355
559
|
if (file.fileSize === 0 || file.resumeState.isComplete()) {
|
|
356
560
|
await this._finalizeFile(file);
|
|
357
|
-
if (this._pendingTransferEnd && this.activeFiles.size === 0) {
|
|
358
|
-
this._finishTransfer();
|
|
359
|
-
}
|
|
360
561
|
} else {
|
|
361
562
|
file.pendingEnd = true;
|
|
362
563
|
file.endMeta = meta;
|
|
@@ -364,6 +565,9 @@ export class ReceiverPipeline {
|
|
|
364
565
|
}
|
|
365
566
|
|
|
366
567
|
async _finalizeFile(file) {
|
|
568
|
+
// Await any in-flight background state save before final save
|
|
569
|
+
await this._drainFileStateSave(file);
|
|
570
|
+
|
|
367
571
|
// Save final state before rename
|
|
368
572
|
await file.resumeState.save(file.statePath);
|
|
369
573
|
|
|
@@ -388,10 +592,20 @@ export class ReceiverPipeline {
|
|
|
388
592
|
this.activeFiles.delete(file.fileId);
|
|
389
593
|
this.completedFiles.add(file.fileId);
|
|
390
594
|
this.completedFiles.add(file.destPath);
|
|
595
|
+
this._completedFileCount++;
|
|
596
|
+
|
|
597
|
+
this._checkTransferCompletion();
|
|
391
598
|
}
|
|
392
599
|
|
|
393
600
|
async close() {
|
|
394
601
|
for (const [fileId, file] of this.activeFiles.entries()) {
|
|
602
|
+
await this._drainFileStateSave(file);
|
|
603
|
+
if (file.chunksWrittenSinceSave > 0) {
|
|
604
|
+
try {
|
|
605
|
+
await file.resumeState.save(file.statePath);
|
|
606
|
+
} catch {}
|
|
607
|
+
file.chunksWrittenSinceSave = 0;
|
|
608
|
+
}
|
|
395
609
|
if (file.fileHandle) {
|
|
396
610
|
try {
|
|
397
611
|
await file.fileHandle.close();
|
|
@@ -401,6 +615,7 @@ export class ReceiverPipeline {
|
|
|
401
615
|
}
|
|
402
616
|
this.activeFiles.clear();
|
|
403
617
|
this._pendingChunks.clear();
|
|
618
|
+
this._pendingFileEnds.clear();
|
|
404
619
|
}
|
|
405
620
|
|
|
406
621
|
async _computeFileSha256(filePath) {
|
package/src/sender/pipeline.js
CHANGED
|
@@ -104,8 +104,8 @@ export class SenderPipeline {
|
|
|
104
104
|
this.chunkSize = options.chunkSize || 1024 * 1024;
|
|
105
105
|
this.maxEntries = options.maxEntries || 8;
|
|
106
106
|
this.maxBytes = options.maxBytes || 8 * 1024 * 1024;
|
|
107
|
-
this.maxImmutableBuffers = options.maxImmutableBuffers ||
|
|
108
|
-
this.maxConcurrentFlushes = options.maxConcurrentFlushes || Math.min(this.maxImmutableBuffers,
|
|
107
|
+
this.maxImmutableBuffers = options.maxImmutableBuffers || 32;
|
|
108
|
+
this.maxConcurrentFlushes = options.maxConcurrentFlushes || Math.min(this.maxImmutableBuffers, 32);
|
|
109
109
|
this.metrics = metrics;
|
|
110
110
|
this.progress = progress;
|
|
111
111
|
this.knownResumeState = knownResumeState;
|
|
@@ -233,7 +233,8 @@ export class SenderPipeline {
|
|
|
233
233
|
fileId: file.fileId,
|
|
234
234
|
fileSize: file.size,
|
|
235
235
|
path: file.relativePath,
|
|
236
|
-
sha256: file.sha256 || ''
|
|
236
|
+
sha256: file.sha256 || '',
|
|
237
|
+
chunkSize: this.chunkSize
|
|
237
238
|
});
|
|
238
239
|
await this._append(Opcodes.FILE_BEGIN, fbegin);
|
|
239
240
|
|