teyyare 0.2.6 → 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 CHANGED
@@ -19,6 +19,8 @@ function printUsage() {
19
19
 
20
20
  \x1b[1mOPTIONS:\x1b[0m
21
21
  -c, --channels <n> Parallel TCP connections (default: 4 for remote, 1 for local)
22
+ --chunk-size <size> Chunk size, e.g. 512K, 1M, 2M, 4M (default: 2M remote, 1M local)
23
+ --batch-size <size> Generation batch size, e.g. 8M, 16M (default: 8x chunk size)
22
24
  --port <port> Port number (default: 7421)
23
25
  --dir <path> Serve root directory (default: current dir)
24
26
 
@@ -30,6 +32,15 @@ function printUsage() {
30
32
  `);
31
33
  }
32
34
 
35
+ function parseByteSize(str) {
36
+ if (!str) return undefined;
37
+ const s = String(str).trim().toLowerCase();
38
+ if (s.endsWith('k') || s.endsWith('kb')) return Math.round(parseFloat(s) * 1024);
39
+ if (s.endsWith('m') || s.endsWith('mb')) return Math.round(parseFloat(s) * 1024 * 1024);
40
+ if (s.endsWith('g') || s.endsWith('gb')) return Math.round(parseFloat(s) * 1024 * 1024 * 1024);
41
+ return Number(str);
42
+ }
43
+
33
44
  function parseDestination(target) {
34
45
  // Matches host:port:path or host:path or local path
35
46
  if (!target.includes(':')) {
@@ -56,7 +67,7 @@ async function main() {
56
67
  }
57
68
 
58
69
  if (command === '--version' || command === '-v' || command === 'version') {
59
- console.log('0.2.6');
70
+ console.log('0.2.7');
60
71
  process.exit(0);
61
72
  }
62
73
 
@@ -88,10 +99,16 @@ async function main() {
88
99
  await new Promise(() => {});
89
100
  } else if (command === 'send') {
90
101
  let channels = undefined;
102
+ let chunkSize = undefined;
103
+ let batchSize = undefined;
91
104
  const positional = [];
92
105
  for (let i = 1; i < args.length; i++) {
93
106
  if ((args[i] === '-c' || args[i] === '--channels') && args[i + 1]) {
94
107
  channels = Number(args[++i]);
108
+ } else if ((args[i] === '--chunk-size' || args[i] === '--chunk') && args[i + 1]) {
109
+ chunkSize = parseByteSize(args[++i]);
110
+ } else if ((args[i] === '--batch-size' || args[i] === '--gen-size') && args[i + 1]) {
111
+ batchSize = parseByteSize(args[++i]);
95
112
  } else {
96
113
  positional.push(args[i]);
97
114
  }
@@ -106,12 +123,17 @@ async function main() {
106
123
  process.exit(1);
107
124
  }
108
125
 
126
+ const pipelineOptions = {};
127
+ if (chunkSize) pipelineOptions.chunkSize = chunkSize;
128
+ if (batchSize) pipelineOptions.maxBytes = batchSize;
129
+
109
130
  const { host, port } = parseDestination(target);
110
131
  const metrics = await send({
111
132
  sourcePath: source,
112
133
  host,
113
134
  port,
114
135
  channels,
136
+ pipelineOptions,
115
137
  showProgress: true
116
138
  });
117
139
 
@@ -129,10 +151,16 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
129
151
  process.exit(0);
130
152
  } else if (command === 'pull') {
131
153
  let channels = undefined;
154
+ let chunkSize = undefined;
155
+ let batchSize = undefined;
132
156
  const positional = [];
133
157
  for (let i = 1; i < args.length; i++) {
134
158
  if ((args[i] === '-c' || args[i] === '--channels') && args[i + 1]) {
135
159
  channels = Number(args[++i]);
160
+ } else if ((args[i] === '--chunk-size' || args[i] === '--chunk') && args[i + 1]) {
161
+ chunkSize = parseByteSize(args[++i]);
162
+ } else if ((args[i] === '--batch-size' || args[i] === '--gen-size') && args[i + 1]) {
163
+ batchSize = parseByteSize(args[++i]);
136
164
  } else {
137
165
  positional.push(args[i]);
138
166
  }
@@ -147,6 +175,10 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
147
175
  process.exit(1);
148
176
  }
149
177
 
178
+ const pipelineOptions = {};
179
+ if (chunkSize) pipelineOptions.chunkSize = chunkSize;
180
+ if (batchSize) pipelineOptions.maxBytes = batchSize;
181
+
150
182
  const { host, port, destPath: remotePath } = parseDestination(source);
151
183
  const metrics = await pull({
152
184
  sourceRemotePath: remotePath,
@@ -154,6 +186,7 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
154
186
  port,
155
187
  channels,
156
188
  destinationDir: target,
189
+ pipelineOptions,
157
190
  showProgress: true
158
191
  });
159
192
 
@@ -170,7 +203,7 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
170
203
  `);
171
204
  process.exit(0);
172
205
  } else if (command === 'status') {
173
- console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.6 status: ready');
206
+ console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.7 status: ready');
174
207
  } else {
175
208
  console.error(`Unknown command: ${command}`);
176
209
  printUsage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teyyare",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "High-performance resumable file transfer engine and CLI built on muttafa and raptiye",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -9,7 +9,11 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/**/*.test.js",
12
- "bench": "node bench/run-all.js"
12
+ "bench": "node bench/run-all.js",
13
+ "e2e": "node test/e2e/e2e.js",
14
+ "e2e:profile": "softscope test/e2e/e2e.js",
15
+ "e2e:live": "softscope --live test/e2e/e2e.js",
16
+ "e2e:md": "softscope --md test/e2e/e2e.js"
13
17
  },
14
18
  "keywords": [
15
19
  "file-transfer",
@@ -76,10 +76,10 @@ export function unpackChunkMeta(buf, offset = 0) {
76
76
  * 14..45: sha256 hex (32 bytes raw or 64 hex chars)
77
77
  * 46..: path utf8
78
78
  */
79
- export function packFileBegin({ fileId, fileSize, path, sha256 = '' }) {
79
+ export function packFileBegin({ fileId, fileSize, path, sha256 = '', chunkSize = 1048576 }) {
80
80
  const pathBytes = encoder.encode(path);
81
81
  const shaBytes = encoder.encode(sha256.padEnd(64, '0').slice(0, 64));
82
- const total = 14 + 64 + pathBytes.byteLength;
82
+ const total = 14 + 64 + pathBytes.byteLength + 4;
83
83
  const buf = new Uint8Array(total);
84
84
  const view = new DataView(buf.buffer, buf.byteOffset, total);
85
85
 
@@ -88,6 +88,7 @@ export function packFileBegin({ fileId, fileSize, path, sha256 = '' }) {
88
88
  view.setUint16(12, pathBytes.byteLength, false);
89
89
  buf.set(shaBytes, 14);
90
90
  buf.set(pathBytes, 14 + 64);
91
+ view.setUint32(14 + 64 + pathBytes.byteLength, chunkSize >>> 0, false);
91
92
  return buf;
92
93
  }
93
94
 
@@ -98,7 +99,12 @@ export function unpackFileBegin(buf) {
98
99
  const pathLen = view.getUint16(12, false);
99
100
  const sha256 = decoder.decode(buf.subarray(14, 14 + 64)).replace(/0+$/, '');
100
101
  const path = decoder.decode(buf.subarray(14 + 64, 14 + 64 + pathLen));
101
- return { fileId, fileSize, path, sha256 };
102
+ let chunkSize = 1048576;
103
+ if (buf.byteLength >= 14 + 64 + pathLen + 4) {
104
+ const cs = view.getUint32(14 + 64 + pathLen, false);
105
+ if (cs > 0) chunkSize = cs;
106
+ }
107
+ return { fileId, fileSize, path, sha256, chunkSize };
102
108
  }
103
109
 
104
110
  /**
@@ -167,10 +173,18 @@ export function unpackTransferEnd(buf) {
167
173
  /**
168
174
  * Pack PULL_REQUEST metadata with optional verifiedChunks bitmap/list for resume.
169
175
  */
170
- export function packPullRequest({ remotePath, verifiedChunks = [] }) {
176
+ export function packPullRequest({
177
+ remotePath,
178
+ verifiedChunks = [],
179
+ chunkSize = 0,
180
+ maxEntries = 0,
181
+ maxBytes = 0
182
+ }) {
171
183
  const pathBytes = encoder.encode(remotePath);
172
184
  const count = verifiedChunks.length;
173
- const buf = new Uint8Array(2 + pathBytes.byteLength + 4 + count * 4);
185
+ const hasTuning = (chunkSize > 0 || maxEntries > 0 || maxBytes > 0);
186
+ const tuningLen = hasTuning ? 10 : 0;
187
+ const buf = new Uint8Array(2 + pathBytes.byteLength + 4 + count * 4 + tuningLen);
174
188
  const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
175
189
  view.setUint16(0, pathBytes.byteLength, false);
176
190
  buf.set(pathBytes, 2);
@@ -182,6 +196,13 @@ export function packPullRequest({ remotePath, verifiedChunks = [] }) {
182
196
  view.setUint32(off, verifiedChunks[i], false);
183
197
  off += 4;
184
198
  }
199
+
200
+ if (hasTuning) {
201
+ view.setUint32(off, chunkSize >>> 0, false);
202
+ view.setUint16(off + 4, maxEntries & 0xffff, false);
203
+ view.setUint32(off + 6, maxBytes >>> 0, false);
204
+ }
205
+
185
206
  return buf;
186
207
  }
187
208
 
@@ -201,7 +222,19 @@ export function unpackPullRequest(buf) {
201
222
  }
202
223
  }
203
224
 
204
- return { remotePath, verifiedChunks };
225
+ let chunkSize = undefined;
226
+ let maxEntries = undefined;
227
+ let maxBytes = undefined;
228
+ if (buf.byteLength >= off + 10) {
229
+ const cs = view.getUint32(off, false);
230
+ const me = view.getUint16(off + 4, false);
231
+ const mb = view.getUint32(off + 6, false);
232
+ if (cs > 0) chunkSize = cs;
233
+ if (me > 0) maxEntries = me;
234
+ if (mb > 0) maxBytes = mb;
235
+ }
236
+
237
+ return { remotePath, verifiedChunks, chunkSize, maxEntries, maxBytes };
205
238
  }
206
239
 
207
240
  /**
package/src/index.js CHANGED
@@ -45,32 +45,45 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
45
45
  if (socket._writableState) socket._writableState.highWaterMark = 4 * 1024 * 1024;
46
46
 
47
47
  const chunks = [];
48
+ let chunkHead = 0;
49
+ let chunkOffset = 0;
48
50
  let bufferedLength = 0;
51
+ const headerScratch = Buffer.allocUnsafe(HEADER_SIZE);
49
52
 
50
53
  socket.on('data', (chunk) => {
51
54
  chunks.push(chunk);
52
55
  bufferedLength += chunk.length;
53
56
 
54
57
  while (bufferedLength >= HEADER_SIZE) {
55
- let headerBuf = chunks[0];
56
- if (headerBuf.length < HEADER_SIZE) {
57
- const tempHeader = Buffer.allocUnsafe(HEADER_SIZE);
58
+ const head = chunks[chunkHead];
59
+ const headAvail = head.length - chunkOffset;
60
+ let header;
61
+
62
+ if (headAvail >= HEADER_SIZE) {
63
+ try {
64
+ header = readHeader(head, chunkOffset);
65
+ } catch (err) {
66
+ socket.destroy(err);
67
+ return;
68
+ }
69
+ } else {
58
70
  let off = 0;
59
- for (let i = 0; off < HEADER_SIZE && i < chunks.length; i++) {
60
- const c = chunks[i];
61
- const toCopy = Math.min(c.length, HEADER_SIZE - off);
62
- tempHeader.set(c.subarray(0, toCopy), off);
71
+ let cIdx = chunkHead;
72
+ let cOff = chunkOffset;
73
+ while (off < HEADER_SIZE && cIdx < chunks.length) {
74
+ const c = chunks[cIdx];
75
+ const toCopy = Math.min(c.length - cOff, HEADER_SIZE - off);
76
+ headerScratch.set(c.subarray(cOff, cOff + toCopy), off);
63
77
  off += toCopy;
78
+ cIdx++;
79
+ cOff = 0;
80
+ }
81
+ try {
82
+ header = readHeader(headerScratch, 0);
83
+ } catch (err) {
84
+ socket.destroy(err);
85
+ return;
64
86
  }
65
- headerBuf = tempHeader;
66
- }
67
-
68
- let header;
69
- try {
70
- header = readHeader(headerBuf, 0);
71
- } catch (err) {
72
- socket.destroy(err);
73
- return;
74
87
  }
75
88
 
76
89
  const totalLength = HEADER_SIZE + header.payloadLength;
@@ -79,29 +92,35 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
79
92
  }
80
93
 
81
94
  let rawFrame;
82
- if (chunks[0].length >= totalLength) {
83
- rawFrame = chunks[0].subarray(0, totalLength);
84
- if (chunks[0].length === totalLength) {
85
- chunks.shift();
86
- } else {
87
- chunks[0] = chunks[0].subarray(totalLength);
95
+ if (headAvail >= totalLength) {
96
+ // Fast-path: single chunk zero-copy slice
97
+ rawFrame = head.subarray(chunkOffset, chunkOffset + totalLength);
98
+ chunkOffset += totalLength;
99
+ if (chunkOffset === head.length) {
100
+ chunks[chunkHead] = null;
101
+ chunkHead++;
102
+ chunkOffset = 0;
88
103
  }
89
104
  bufferedLength -= totalLength;
90
105
  } else {
106
+ // Multi-chunk frame assembly
91
107
  rawFrame = Buffer.allocUnsafe(totalLength);
92
108
  let dstOff = 0;
93
109
  let remaining = totalLength;
94
110
  while (remaining > 0) {
95
- const head = chunks[0];
96
- if (head.length <= remaining) {
97
- rawFrame.set(head, dstOff);
98
- dstOff += head.length;
99
- remaining -= head.length;
100
- chunks.shift();
111
+ const curr = chunks[chunkHead];
112
+ const avail = curr.length - chunkOffset;
113
+ if (avail <= remaining) {
114
+ rawFrame.set(curr.subarray(chunkOffset), dstOff);
115
+ dstOff += avail;
116
+ remaining -= avail;
117
+ chunks[chunkHead] = null;
118
+ chunkHead++;
119
+ chunkOffset = 0;
101
120
  } else {
102
- rawFrame.set(head.subarray(0, remaining), dstOff);
121
+ rawFrame.set(curr.subarray(chunkOffset, chunkOffset + remaining), dstOff);
103
122
  dstOff += remaining;
104
- chunks[0] = head.subarray(remaining);
123
+ chunkOffset += remaining;
105
124
  remaining = 0;
106
125
  }
107
126
  }
@@ -126,6 +145,16 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
126
145
  console.error(`[TCPTransport ${this.id}] Error decoding frame:`, err.message);
127
146
  }
128
147
  }
148
+
149
+ // Amortized array compaction to prevent unbounded array growth without O(N) shift()
150
+ if (bufferedLength === 0) {
151
+ chunks.length = 0;
152
+ chunkHead = 0;
153
+ chunkOffset = 0;
154
+ } else if (chunkHead >= 128 && chunkHead > (chunks.length >> 1)) {
155
+ chunks.splice(0, chunkHead);
156
+ chunkHead = 0;
157
+ }
129
158
  });
130
159
 
131
160
  socket.on('close', () => {
@@ -141,8 +170,14 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
141
170
  if (idx !== -1) {
142
171
  pool.splice(idx, 1);
143
172
  }
173
+ if (pool.length === 0 && this.onPeerDisconnect) {
174
+ this.onPeerDisconnect(peer);
175
+ }
144
176
  }
145
177
  }
178
+ if (this._inboundSockets.size === 0 && this.onPeerDisconnect) {
179
+ this.onPeerDisconnect(1);
180
+ }
146
181
  });
147
182
 
148
183
  socket.on('error', () => {
@@ -213,18 +248,19 @@ TCPTransport.prototype.sendv = function(peer, buffers) {
213
248
 
214
249
  const pool = this._peerSocketPool ? this._peerSocketPool.get(peer) : null;
215
250
  if (pool && pool.length > 0) {
216
- const active = pool.filter((s) => !s.destroyed && s.writable);
217
- if (active.length > 0) {
218
- let idx = this._channelIndex.get(peer) || 0;
219
- const socket = active[idx % active.length];
220
- this._channelIndex.set(peer, idx + 1);
221
-
222
- socket.cork();
223
- for (let i = 0; i < buffers.length; i++) {
224
- socket.write(buffers[i]);
251
+ let idx = this._channelIndex.get(peer) || 0;
252
+ const poolLen = pool.length;
253
+ for (let attempts = 0; attempts < poolLen; attempts++) {
254
+ const s = pool[(idx + attempts) % poolLen];
255
+ if (!s.destroyed && s.writable) {
256
+ this._channelIndex.set(peer, idx + attempts + 1);
257
+ s.cork();
258
+ for (let i = 0; i < buffers.length; i++) {
259
+ s.write(buffers[i]);
260
+ }
261
+ s.uncork();
262
+ return;
225
263
  }
226
- socket.uncork();
227
- return;
228
264
  }
229
265
  }
230
266
 
@@ -246,13 +282,15 @@ TCPTransport.prototype.send = function(peer, buffer) {
246
282
 
247
283
  const pool = this._peerSocketPool ? this._peerSocketPool.get(peer) : null;
248
284
  if (pool && pool.length > 0) {
249
- const active = pool.filter((s) => !s.destroyed && s.writable);
250
- if (active.length > 0) {
251
- let idx = this._channelIndex.get(peer) || 0;
252
- const socket = active[idx % active.length];
253
- this._channelIndex.set(peer, idx + 1);
254
- socket.write(buffer);
255
- return;
285
+ let idx = this._channelIndex.get(peer) || 0;
286
+ const poolLen = pool.length;
287
+ for (let attempts = 0; attempts < poolLen; attempts++) {
288
+ const s = pool[(idx + attempts) % poolLen];
289
+ if (!s.destroyed && s.writable) {
290
+ this._channelIndex.set(peer, idx + attempts + 1);
291
+ s.write(buffer);
292
+ return;
293
+ }
256
294
  }
257
295
  }
258
296
 
@@ -444,8 +482,8 @@ export async function serve({
444
482
  transport,
445
483
  localNode: 2,
446
484
  remoteNode: 1,
447
- maxInflightBatches: 16,
448
- maxInflightBytes: 64 * 1024 * 1024
485
+ maxInflightBatches: 32,
486
+ maxInflightBytes: 128 * 1024 * 1024
449
487
  });
450
488
 
451
489
  const receiver = new ReceiverPipeline({
@@ -458,12 +496,27 @@ export async function serve({
458
496
 
459
497
  let activeSender = null;
460
498
 
499
+ transport.onPeerDisconnect = async (peer) => {
500
+ if (activeSender) {
501
+ const s = activeSender;
502
+ activeSender = null;
503
+ await s.abort();
504
+ }
505
+ replication.reset();
506
+ };
507
+
461
508
  replication.onBatch(async (msg) => {
462
509
  if (!msg.buffers || msg.buffers.length < 2) return;
463
510
  const [metaBuf, payloadBuf] = msg.buffers;
464
511
  const entries = unpackGeneration(metaBuf, payloadBuf);
465
512
  if (entries.length > 0 && entries[0].type === Opcodes.PULL_REQUEST) {
466
- const { remotePath, verifiedChunks = [] } = unpackPullRequest(entries[0].key);
513
+ const {
514
+ remotePath,
515
+ verifiedChunks = [],
516
+ chunkSize,
517
+ maxEntries,
518
+ maxBytes
519
+ } = unpackPullRequest(entries[0].key);
467
520
  let resolved = path.isAbsolute(remotePath) ? remotePath : path.resolve(destinationDir, remotePath);
468
521
  if (!existsSync(resolved)) {
469
522
  const alt = path.resolve(destinationDir, remotePath);
@@ -481,8 +534,9 @@ export async function serve({
481
534
 
482
535
  // Abort any existing running sender before starting new transfer
483
536
  if (activeSender) {
484
- activeSender.abort();
537
+ const s = activeSender;
485
538
  activeSender = null;
539
+ await s.abort();
486
540
  }
487
541
 
488
542
  const manifest = await TransferManifest.build(resolved);
@@ -497,9 +551,18 @@ export async function serve({
497
551
  }));
498
552
  }
499
553
 
554
+ const senderOpts = {
555
+ maxImmutableBuffers: 32,
556
+ maxConcurrentFlushes: 32
557
+ };
558
+ if (chunkSize) senderOpts.chunkSize = chunkSize;
559
+ if (maxEntries) senderOpts.maxEntries = maxEntries;
560
+ if (maxBytes) senderOpts.maxBytes = maxBytes;
561
+
500
562
  const sender = new SenderPipeline({
501
563
  replication,
502
564
  manifest,
565
+ options: senderOpts,
503
566
  knownResumeState,
504
567
  metrics
505
568
  });
@@ -534,6 +597,12 @@ export async function serve({
534
597
  receiver,
535
598
  metrics,
536
599
  async close() {
600
+ if (activeSender) {
601
+ const s = activeSender;
602
+ activeSender = null;
603
+ await s.abort();
604
+ }
605
+ await receiver.close();
537
606
  metrics.stop();
538
607
  await transport.close();
539
608
  }
@@ -582,8 +651,8 @@ export async function send({
582
651
  transport,
583
652
  localNode: 1,
584
653
  remoteNode: 2,
585
- maxInflightBatches: pipelineOptions.maxInflightBatches || 16,
586
- maxInflightBytes: pipelineOptions.maxInflightBytes || 64 * 1024 * 1024
654
+ maxInflightBatches: pipelineOptions.maxInflightBatches || 32,
655
+ maxInflightBytes: pipelineOptions.maxInflightBytes || 128 * 1024 * 1024
587
656
  });
588
657
 
589
658
  const manifest = await TransferManifest.build(sourcePath);
@@ -600,6 +669,7 @@ export async function send({
600
669
  try {
601
670
  await sender.transfer();
602
671
  } finally {
672
+ await sender.abort();
603
673
  metrics.stop();
604
674
  await transport.close();
605
675
  }
@@ -628,6 +698,15 @@ export async function pull({
628
698
  pipelineOptions = {},
629
699
  showProgress = true
630
700
  }) {
701
+ const isRemote = host !== '127.0.0.1' && host !== 'localhost';
702
+ const defaultChunkSize = isRemote ? 2 * 1024 * 1024 : 1024 * 1024;
703
+ const defaultMaxEntries = 8;
704
+ const defaultMaxBytes = defaultChunkSize * defaultMaxEntries;
705
+
706
+ const effectiveChunkSize = pipelineOptions.chunkSize || defaultChunkSize;
707
+ const effectiveMaxEntries = pipelineOptions.maxEntries || defaultMaxEntries;
708
+ const effectiveMaxBytes = pipelineOptions.maxBytes || defaultMaxBytes;
709
+
631
710
  const metrics = new MetricsCollector();
632
711
  const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
633
712
  if (progress) {
@@ -652,8 +731,8 @@ export async function pull({
652
731
  transport,
653
732
  localNode: 1,
654
733
  remoteNode: 2,
655
- maxInflightBatches: pipelineOptions.maxInflightBatches || 16,
656
- maxInflightBytes: pipelineOptions.maxInflightBytes || 64 * 1024 * 1024
734
+ maxInflightBatches: pipelineOptions.maxInflightBatches || 32,
735
+ maxInflightBytes: pipelineOptions.maxInflightBytes || 128 * 1024 * 1024
657
736
  });
658
737
 
659
738
  const receiver = new ReceiverPipeline({
@@ -676,17 +755,24 @@ export async function pull({
676
755
  }
677
756
  } catch {}
678
757
 
679
- // Submit PULL_REQUEST batch with verified chunks for resume
758
+ // Submit PULL_REQUEST batch with verified chunks and WAN tuning
680
759
  await submitSingleMutation(
681
760
  replication,
682
761
  Opcodes.PULL_REQUEST,
683
- packPullRequest({ remotePath: sourceRemotePath, verifiedChunks })
762
+ packPullRequest({
763
+ remotePath: sourceRemotePath,
764
+ verifiedChunks,
765
+ chunkSize: effectiveChunkSize,
766
+ maxEntries: effectiveMaxEntries,
767
+ maxBytes: effectiveMaxBytes
768
+ })
684
769
  );
685
770
 
686
771
  // Wait for receiver pipeline to receive and write all files
687
772
  await receiver.waitForCompletion();
688
773
  } finally {
689
774
  if (progress) progress.stop();
775
+ await receiver.close();
690
776
  metrics.stop();
691
777
  await transport.close();
692
778
  }