teyyare 0.2.4 → 0.2.5
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 +2 -2
- package/package.json +2 -2
- package/src/core/protocol.js +25 -4
- package/src/index.js +47 -8
- package/src/receiver/pipeline.js +7 -2
- package/src/sender/pipeline.js +18 -0
package/bin/teyyare.js
CHANGED
|
@@ -51,7 +51,7 @@ async function main() {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
54
|
-
console.log('0.2.
|
|
54
|
+
console.log('0.2.5');
|
|
55
55
|
process.exit(0);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -143,7 +143,7 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
|
143
143
|
`);
|
|
144
144
|
process.exit(0);
|
|
145
145
|
} else if (command === 'status') {
|
|
146
|
-
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.
|
|
146
|
+
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.5 status: ready');
|
|
147
147
|
} else {
|
|
148
148
|
console.error(`Unknown command: ${command}`);
|
|
149
149
|
printUsage();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teyyare",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"teyyare": "
|
|
8
|
+
"teyyare": "bin/teyyare.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test test/**/*.test.js",
|
package/src/core/protocol.js
CHANGED
|
@@ -165,14 +165,23 @@ export function unpackTransferEnd(buf) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* Pack PULL_REQUEST metadata.
|
|
168
|
+
* Pack PULL_REQUEST metadata with optional verifiedChunks bitmap/list for resume.
|
|
169
169
|
*/
|
|
170
|
-
export function packPullRequest({ remotePath }) {
|
|
170
|
+
export function packPullRequest({ remotePath, verifiedChunks = [] }) {
|
|
171
171
|
const pathBytes = encoder.encode(remotePath);
|
|
172
|
-
const
|
|
172
|
+
const count = verifiedChunks.length;
|
|
173
|
+
const buf = new Uint8Array(2 + pathBytes.byteLength + 4 + count * 4);
|
|
173
174
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
174
175
|
view.setUint16(0, pathBytes.byteLength, false);
|
|
175
176
|
buf.set(pathBytes, 2);
|
|
177
|
+
|
|
178
|
+
let off = 2 + pathBytes.byteLength;
|
|
179
|
+
view.setUint32(off, count, false);
|
|
180
|
+
off += 4;
|
|
181
|
+
for (let i = 0; i < count; i++) {
|
|
182
|
+
view.setUint32(off, verifiedChunks[i], false);
|
|
183
|
+
off += 4;
|
|
184
|
+
}
|
|
176
185
|
return buf;
|
|
177
186
|
}
|
|
178
187
|
|
|
@@ -180,7 +189,19 @@ export function unpackPullRequest(buf) {
|
|
|
180
189
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
181
190
|
const pathLen = view.getUint16(0, false);
|
|
182
191
|
const remotePath = decoder.decode(buf.subarray(2, 2 + pathLen));
|
|
183
|
-
|
|
192
|
+
|
|
193
|
+
const verifiedChunks = [];
|
|
194
|
+
let off = 2 + pathLen;
|
|
195
|
+
if (buf.byteLength >= off + 4) {
|
|
196
|
+
const count = view.getUint32(off, false);
|
|
197
|
+
off += 4;
|
|
198
|
+
for (let i = 0; i < count && off + 4 <= buf.byteLength; i++) {
|
|
199
|
+
verifiedChunks.push(view.getUint32(off, false));
|
|
200
|
+
off += 4;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { remotePath, verifiedChunks };
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
/**
|
package/src/index.js
CHANGED
|
@@ -302,12 +302,14 @@ export async function serve({
|
|
|
302
302
|
autoHookBatch: false
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
+
let activeSender = null;
|
|
306
|
+
|
|
305
307
|
replication.onBatch(async (msg) => {
|
|
306
308
|
if (!msg.buffers || msg.buffers.length < 2) return;
|
|
307
309
|
const [metaBuf, payloadBuf] = msg.buffers;
|
|
308
310
|
const entries = unpackGeneration(metaBuf, payloadBuf);
|
|
309
311
|
if (entries.length > 0 && entries[0].type === Opcodes.PULL_REQUEST) {
|
|
310
|
-
const { remotePath } = unpackPullRequest(entries[0].key);
|
|
312
|
+
const { remotePath, verifiedChunks = [] } = unpackPullRequest(entries[0].key);
|
|
311
313
|
let resolved = path.isAbsolute(remotePath) ? remotePath : path.resolve(destinationDir, remotePath);
|
|
312
314
|
if (!existsSync(resolved)) {
|
|
313
315
|
const alt = path.resolve(destinationDir, remotePath);
|
|
@@ -323,22 +325,47 @@ export async function serve({
|
|
|
323
325
|
return;
|
|
324
326
|
}
|
|
325
327
|
|
|
328
|
+
// Abort any existing running sender before starting new transfer
|
|
329
|
+
if (activeSender) {
|
|
330
|
+
activeSender.abort();
|
|
331
|
+
activeSender = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
326
334
|
const manifest = await TransferManifest.build(resolved);
|
|
327
335
|
replication.reset();
|
|
336
|
+
|
|
337
|
+
// Seed sender with verified chunks sent by pull client for resume
|
|
338
|
+
const knownResumeState = new Map();
|
|
339
|
+
if (verifiedChunks && verifiedChunks.length > 0) {
|
|
340
|
+
knownResumeState.set(1, new FileResumeState({
|
|
341
|
+
fileId: 1,
|
|
342
|
+
verifiedChunks: new Set(verifiedChunks)
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
328
346
|
const sender = new SenderPipeline({
|
|
329
347
|
replication,
|
|
330
348
|
manifest,
|
|
349
|
+
knownResumeState,
|
|
331
350
|
metrics
|
|
332
351
|
});
|
|
352
|
+
activeSender = sender;
|
|
353
|
+
|
|
333
354
|
setImmediate(async () => {
|
|
334
355
|
try {
|
|
335
356
|
await sender.transfer();
|
|
336
357
|
} catch (err) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
358
|
+
if (!sender.aborted) {
|
|
359
|
+
await submitSingleMutation(
|
|
360
|
+
replication,
|
|
361
|
+
Opcodes.PULL_ERROR,
|
|
362
|
+
packPullError({ errorCode: 500, message: err.message })
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
} finally {
|
|
366
|
+
if (activeSender === sender) {
|
|
367
|
+
activeSender = null;
|
|
368
|
+
}
|
|
342
369
|
}
|
|
343
370
|
});
|
|
344
371
|
return;
|
|
@@ -470,11 +497,23 @@ export async function pull({
|
|
|
470
497
|
});
|
|
471
498
|
|
|
472
499
|
try {
|
|
473
|
-
//
|
|
500
|
+
// Check if resume state exists for this file
|
|
501
|
+
const fileName = path.basename(sourceRemotePath);
|
|
502
|
+
const targetFile = path.resolve(destinationDir, fileName);
|
|
503
|
+
const statePath = `${targetFile}.teyyare-state`;
|
|
504
|
+
let verifiedChunks = [];
|
|
505
|
+
try {
|
|
506
|
+
const existingState = await FileResumeState.load(statePath);
|
|
507
|
+
if (existingState && existingState.verifiedChunks) {
|
|
508
|
+
verifiedChunks = Array.from(existingState.verifiedChunks);
|
|
509
|
+
}
|
|
510
|
+
} catch {}
|
|
511
|
+
|
|
512
|
+
// Submit PULL_REQUEST batch with verified chunks for resume
|
|
474
513
|
await submitSingleMutation(
|
|
475
514
|
replication,
|
|
476
515
|
Opcodes.PULL_REQUEST,
|
|
477
|
-
packPullRequest({ remotePath: sourceRemotePath })
|
|
516
|
+
packPullRequest({ remotePath: sourceRemotePath, verifiedChunks })
|
|
478
517
|
);
|
|
479
518
|
|
|
480
519
|
// Wait for receiver pipeline to receive and write all files
|
package/src/receiver/pipeline.js
CHANGED
|
@@ -216,10 +216,14 @@ export class ReceiverPipeline {
|
|
|
216
216
|
if (this.progress) {
|
|
217
217
|
const fileSize = Number(info.fileSize);
|
|
218
218
|
const estimatedChunks = Math.max(1, Math.ceil(fileSize / (1024 * 1024)));
|
|
219
|
+
const verifiedBytes = resumeState.verifiedCount * (resumeState.chunkSize || 1024 * 1024);
|
|
219
220
|
this.progress.update({
|
|
220
221
|
fileName: info.path,
|
|
221
222
|
totalBytes: fileSize,
|
|
222
|
-
totalChunks: estimatedChunks
|
|
223
|
+
totalChunks: estimatedChunks,
|
|
224
|
+
bytesDone: verifiedBytes,
|
|
225
|
+
chunksDone: resumeState.verifiedCount,
|
|
226
|
+
resumeHits: resumeState.verifiedCount
|
|
223
227
|
});
|
|
224
228
|
}
|
|
225
229
|
}
|
|
@@ -228,7 +232,8 @@ export class ReceiverPipeline {
|
|
|
228
232
|
const meta = unpackChunkMeta(key);
|
|
229
233
|
const file = this.activeFiles.get(meta.fileId);
|
|
230
234
|
if (!file) {
|
|
231
|
-
|
|
235
|
+
// Stale or out-of-order chunk from an interrupted previous session - ignore safely
|
|
236
|
+
return;
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
// Idempotency / resume hit check
|
package/src/sender/pipeline.js
CHANGED
|
@@ -141,13 +141,26 @@ export class SenderPipeline {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
|
+
this.aborted = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
abort() {
|
|
148
|
+
this.aborted = true;
|
|
149
|
+
if (this.engine) {
|
|
150
|
+
try {
|
|
151
|
+
this.engine.halt();
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
async _append(opcode, key, value = undefined, keyBytes = 0, valueBytes = 0) {
|
|
157
|
+
if (this.aborted) return;
|
|
147
158
|
while (this.engine.isBackpressured()) {
|
|
159
|
+
if (this.aborted) return;
|
|
148
160
|
await this.engine.ready();
|
|
149
161
|
await new Promise((r) => setImmediate(r));
|
|
150
162
|
}
|
|
163
|
+
if (this.aborted) return;
|
|
151
164
|
const kLen = keyBytes || (key ? key.byteLength : 0);
|
|
152
165
|
const vLen = valueBytes || (value ? value.byteLength : 0);
|
|
153
166
|
return this.engine.custom(opcode, key, value, kLen, vLen);
|
|
@@ -205,6 +218,7 @@ export class SenderPipeline {
|
|
|
205
218
|
const handle = await fs.open(filePath, 'r');
|
|
206
219
|
try {
|
|
207
220
|
for (let i = 0; i < totalFileChunks; i++) {
|
|
221
|
+
if (this.aborted) break;
|
|
208
222
|
const offset = BigInt(i * this.chunkSize);
|
|
209
223
|
const remaining = file.size - (i * this.chunkSize);
|
|
210
224
|
const chunkLen = Math.min(this.chunkSize, remaining);
|
|
@@ -272,11 +286,15 @@ export class SenderPipeline {
|
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
if (this.aborted) return;
|
|
290
|
+
|
|
275
291
|
// OP_FILE_END
|
|
276
292
|
const fend = packFileEnd({ fileId: file.fileId, sha256: file.sha256 || '' });
|
|
277
293
|
await this._append(Opcodes.FILE_END, fend);
|
|
278
294
|
}
|
|
279
295
|
|
|
296
|
+
if (this.aborted) return;
|
|
297
|
+
|
|
280
298
|
// 3. OP_TRANSFER_END
|
|
281
299
|
const tend = packTransferEnd({ transferId: this.transferId, status: 0 });
|
|
282
300
|
await this._append(Opcodes.TRANSFER_END, tend);
|