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 +35 -2
- package/package.json +6 -2
- package/src/core/protocol.js +39 -6
- package/src/index.js +144 -58
- package/src/receiver/pipeline.js +290 -58
- package/src/sender/pipeline.js +37 -7
- 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/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.
|
|
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.
|
|
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.
|
|
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",
|
package/src/core/protocol.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
chunks[
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
dstOff
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
121
|
+
rawFrame.set(curr.subarray(chunkOffset, chunkOffset + remaining), dstOff);
|
|
103
122
|
dstOff += remaining;
|
|
104
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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:
|
|
448
|
-
maxInflightBytes:
|
|
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 {
|
|
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
|
|
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 ||
|
|
586
|
-
maxInflightBytes: pipelineOptions.maxInflightBytes ||
|
|
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 ||
|
|
656
|
-
maxInflightBytes: pipelineOptions.maxInflightBytes ||
|
|
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
|
|
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({
|
|
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
|
}
|