teyyare 0.1.0
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/README.md +180 -0
- package/bench/ablation.bench.js +229 -0
- package/bench/matrix.bench.js +86 -0
- package/bench/run-all.js +19 -0
- package/bin/teyyare.js +124 -0
- package/package.json +27 -0
- package/src/cli/progress.js +125 -0
- package/src/core/protocol.js +163 -0
- package/src/index.js +139 -0
- package/src/manifest/manifest.js +84 -0
- package/src/metrics/collector.js +119 -0
- package/src/receiver/pipeline.js +234 -0
- package/src/resume/state.js +125 -0
- package/src/sender/pipeline.js +288 -0
- package/test/integration/backpressure.test.js +63 -0
- package/test/integration/chaos.test.js +104 -0
- package/test/integration/directory.test.js +75 -0
- package/test/integration/resume.test.js +97 -0
- package/test/integration/single-file.test.js +64 -0
- package/test/unit/ownership.test.js +74 -0
- package/test/unit/protocol.test.js +73 -0
- package/test/unit/state.test.js +49 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { serve, send, FileResumeState } from '../../src/index.js';
|
|
9
|
+
|
|
10
|
+
// Deterministic PRNG (Linear Congruential Generator)
|
|
11
|
+
class PRNG {
|
|
12
|
+
constructor(seed = 0x12345678) {
|
|
13
|
+
this.state = seed >>> 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
next() {
|
|
17
|
+
this.state = (1664525 * this.state + 1013904223) >>> 0;
|
|
18
|
+
return this.state / 0x100000000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
nextInt(min, max) {
|
|
22
|
+
return Math.floor(this.next() * (max - min + 1)) + min;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('Integration - deterministic chaos test: duplicate chunks, interruptions, and recovery', async () => {
|
|
27
|
+
const prng = new PRNG(42);
|
|
28
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-chaos-test-'));
|
|
29
|
+
const sourceFile = path.join(tmpDir, 'chaos-source.bin');
|
|
30
|
+
const destDir = path.join(tmpDir, 'dest');
|
|
31
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
// 4 MB random test data
|
|
34
|
+
const size = 4 * 1024 * 1024;
|
|
35
|
+
const data = crypto.randomBytes(size);
|
|
36
|
+
await fs.writeFile(sourceFile, data);
|
|
37
|
+
const sourceHash = crypto.createHash('sha256').update(data).digest('hex');
|
|
38
|
+
|
|
39
|
+
const destFile = path.join(destDir, 'chaos-source.bin');
|
|
40
|
+
const partPath = `${destFile}.teyyare-part`;
|
|
41
|
+
const statePath = `${destFile}.teyyare-state`;
|
|
42
|
+
|
|
43
|
+
// Chaos Step 1: Simulate interrupted transfer with duplicate chunk writes
|
|
44
|
+
const handle = await fs.open(partPath, 'w+');
|
|
45
|
+
// Write chunks 0 and 1
|
|
46
|
+
await handle.write(data.subarray(0, 1024 * 1024), 0, 1024 * 1024, 0);
|
|
47
|
+
await handle.write(data.subarray(1024 * 1024, 2 * 1024 * 1024), 0, 1024 * 1024, 1024 * 1024);
|
|
48
|
+
// Duplicate chunk 1 write (idempotency simulation)
|
|
49
|
+
await handle.write(data.subarray(1024 * 1024, 2 * 1024 * 1024), 0, 1024 * 1024, 1024 * 1024);
|
|
50
|
+
await handle.sync();
|
|
51
|
+
await handle.close();
|
|
52
|
+
|
|
53
|
+
const partialState = new FileResumeState({
|
|
54
|
+
transferId: 'chaos-session',
|
|
55
|
+
fileId: 1,
|
|
56
|
+
fileSize: size,
|
|
57
|
+
chunkSize: 1024 * 1024,
|
|
58
|
+
sha256: sourceHash,
|
|
59
|
+
verifiedChunks: new Set([0, 1])
|
|
60
|
+
});
|
|
61
|
+
await partialState.save(statePath);
|
|
62
|
+
|
|
63
|
+
// Chaos Step 2: Start server with randomized write delay jitter
|
|
64
|
+
const port = 7526;
|
|
65
|
+
const server = await serve({
|
|
66
|
+
port,
|
|
67
|
+
destinationDir: destDir,
|
|
68
|
+
writeDelayMs: prng.nextInt(5, 15) // Random jitter
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Chaos Step 3: Run resume transfer
|
|
72
|
+
const knownResume = new Map([[1, partialState]]);
|
|
73
|
+
const senderMetrics = await send({
|
|
74
|
+
sourcePath: sourceFile,
|
|
75
|
+
host: '127.0.0.1',
|
|
76
|
+
port,
|
|
77
|
+
pipelineOptions: {
|
|
78
|
+
chunkSize: 1024 * 1024,
|
|
79
|
+
maxEntries: 2
|
|
80
|
+
},
|
|
81
|
+
knownResumeState: knownResume,
|
|
82
|
+
showProgress: false
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await server.receiver.waitForCompletion();
|
|
86
|
+
await server.close();
|
|
87
|
+
|
|
88
|
+
// Verification after chaos recovery
|
|
89
|
+
assert.ok(existsSync(destFile), 'Destination file must exist');
|
|
90
|
+
assert.strictEqual(existsSync(partPath), false, 'Part file must be cleaned up');
|
|
91
|
+
assert.strictEqual(existsSync(statePath), false, 'State file must be cleaned up');
|
|
92
|
+
|
|
93
|
+
const destData = await fs.readFile(destFile);
|
|
94
|
+
assert.strictEqual(destData.length, size, 'Recovered file size must match');
|
|
95
|
+
|
|
96
|
+
const destHash = crypto.createHash('sha256').update(destData).digest('hex');
|
|
97
|
+
assert.strictEqual(destHash, sourceHash, 'Source hash === destination hash must always hold!');
|
|
98
|
+
|
|
99
|
+
const snap = senderMetrics.snapshot();
|
|
100
|
+
assert.strictEqual(snap.resumeHits, 2, 'Must record 2 resume hits');
|
|
101
|
+
assert.strictEqual(snap.chunksCreated, 2, 'Must transmit remaining 2 chunks');
|
|
102
|
+
|
|
103
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
104
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { serve, send } from '../../src/index.js';
|
|
9
|
+
|
|
10
|
+
test('Integration - directory transfer with nested files and small-file packing', async () => {
|
|
11
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-dir-test-'));
|
|
12
|
+
const srcDir = path.join(tmpDir, 'source');
|
|
13
|
+
const destDir = path.join(tmpDir, 'dest');
|
|
14
|
+
|
|
15
|
+
// Create directory structure
|
|
16
|
+
await fs.mkdir(path.join(srcDir, 'assets'), { recursive: true });
|
|
17
|
+
await fs.mkdir(path.join(srcDir, 'src', 'utils'), { recursive: true });
|
|
18
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const filesToCreate = [
|
|
21
|
+
{ rel: 'index.html', size: 512 },
|
|
22
|
+
{ rel: 'style.css', size: 1200 },
|
|
23
|
+
{ rel: 'assets/logo.svg', size: 3400 },
|
|
24
|
+
{ rel: 'src/app.js', size: 15000 },
|
|
25
|
+
{ rel: 'src/utils/math.js', size: 800 },
|
|
26
|
+
{ rel: 'data.bin', size: 2 * 1024 * 1024 } // 2 MB
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const fileHashes = new Map();
|
|
30
|
+
|
|
31
|
+
for (const f of filesToCreate) {
|
|
32
|
+
const fullPath = path.join(srcDir, f.rel);
|
|
33
|
+
const data = crypto.randomBytes(f.size);
|
|
34
|
+
await fs.writeFile(fullPath, data);
|
|
35
|
+
fileHashes.set(f.rel, crypto.createHash('sha256').update(data).digest('hex'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const port = 7522;
|
|
39
|
+
const server = await serve({ port, destinationDir: destDir });
|
|
40
|
+
|
|
41
|
+
const senderMetrics = await send({
|
|
42
|
+
sourcePath: srcDir,
|
|
43
|
+
host: '127.0.0.1',
|
|
44
|
+
port,
|
|
45
|
+
pipelineOptions: {
|
|
46
|
+
chunkSize: 512 * 1024,
|
|
47
|
+
maxEntries: 16
|
|
48
|
+
},
|
|
49
|
+
showProgress: false
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await server.receiver.waitForCompletion();
|
|
53
|
+
await server.close();
|
|
54
|
+
|
|
55
|
+
// Verify all files reconstructed properly
|
|
56
|
+
for (const f of filesToCreate) {
|
|
57
|
+
const destPath = path.join(destDir, f.rel);
|
|
58
|
+
assert.ok(existsSync(destPath), `File ${f.rel} must exist in destination`);
|
|
59
|
+
|
|
60
|
+
const destData = await fs.readFile(destPath);
|
|
61
|
+
assert.strictEqual(destData.length, f.size, `Size must match for ${f.rel}`);
|
|
62
|
+
|
|
63
|
+
const destHash = crypto.createHash('sha256').update(destData).digest('hex');
|
|
64
|
+
assert.strictEqual(destHash, fileHashes.get(f.rel), `Hash must match for ${f.rel}`);
|
|
65
|
+
|
|
66
|
+
// Check no temp files left
|
|
67
|
+
assert.strictEqual(existsSync(`${destPath}.teyyare-part`), false);
|
|
68
|
+
assert.strictEqual(existsSync(`${destPath}.teyyare-state`), false);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stats = senderMetrics.snapshot();
|
|
72
|
+
assert.strictEqual(stats.fileBytesRead, filesToCreate.reduce((a, b) => a + b.size, 0));
|
|
73
|
+
|
|
74
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
75
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { serve, send, FileResumeState } from '../../src/index.js';
|
|
9
|
+
|
|
10
|
+
test('Integration - resume after interruption at 50%, bounded retransmission, SHA-256 match', async () => {
|
|
11
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-resume-test-'));
|
|
12
|
+
const sourceFile = path.join(tmpDir, 'large.bin');
|
|
13
|
+
const destDir = path.join(tmpDir, 'dest');
|
|
14
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
// 8 MB file (8 x 1 MiB chunks)
|
|
17
|
+
const size = 8 * 1024 * 1024;
|
|
18
|
+
const data = crypto.randomBytes(size);
|
|
19
|
+
await fs.writeFile(sourceFile, data);
|
|
20
|
+
const sourceHash = crypto.createHash('sha256').update(data).digest('hex');
|
|
21
|
+
|
|
22
|
+
const destFile = path.join(destDir, 'large.bin');
|
|
23
|
+
const partPath = `${destFile}.teyyare-part`;
|
|
24
|
+
const statePath = `${destFile}.teyyare-state`;
|
|
25
|
+
|
|
26
|
+
// --- Phase 1: Transfer first 4 chunks (50%) ---
|
|
27
|
+
const port1 = 7523;
|
|
28
|
+
const server1 = await serve({ port: port1, destinationDir: destDir });
|
|
29
|
+
|
|
30
|
+
// Pre-seed receiver with first 4 chunks as if interrupted at 50%
|
|
31
|
+
const partialState = new FileResumeState({
|
|
32
|
+
transferId: 't-interrupted',
|
|
33
|
+
fileId: 1,
|
|
34
|
+
fileSize: size,
|
|
35
|
+
chunkSize: 1024 * 1024,
|
|
36
|
+
sha256: sourceHash,
|
|
37
|
+
verifiedChunks: new Set([0, 1, 2, 3])
|
|
38
|
+
});
|
|
39
|
+
await partialState.save(statePath);
|
|
40
|
+
|
|
41
|
+
// Write first 4 MB directly into part file
|
|
42
|
+
const handle = await fs.open(partPath, 'w+');
|
|
43
|
+
await handle.write(data.subarray(0, 4 * 1024 * 1024), 0, 4 * 1024 * 1024, 0);
|
|
44
|
+
await handle.sync();
|
|
45
|
+
await handle.close();
|
|
46
|
+
|
|
47
|
+
await server1.close();
|
|
48
|
+
|
|
49
|
+
assert.ok(existsSync(partPath), 'Part file must exist at 50%');
|
|
50
|
+
assert.ok(existsSync(statePath), 'State file must exist at 50%');
|
|
51
|
+
|
|
52
|
+
// --- Phase 2: Resume transfer ---
|
|
53
|
+
const port2 = 7524;
|
|
54
|
+
const server2 = await serve({ port: port2, destinationDir: destDir });
|
|
55
|
+
|
|
56
|
+
// Sender loads the verified state
|
|
57
|
+
const knownResume = new Map();
|
|
58
|
+
const loadedState = await FileResumeState.load(statePath);
|
|
59
|
+
knownResume.set(1, loadedState);
|
|
60
|
+
|
|
61
|
+
const tResumeStart = Date.now();
|
|
62
|
+
const senderMetrics = await send({
|
|
63
|
+
sourcePath: sourceFile,
|
|
64
|
+
host: '127.0.0.1',
|
|
65
|
+
port: port2,
|
|
66
|
+
pipelineOptions: {
|
|
67
|
+
chunkSize: 1024 * 1024,
|
|
68
|
+
maxEntries: 4
|
|
69
|
+
},
|
|
70
|
+
knownResumeState: knownResume,
|
|
71
|
+
showProgress: false
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await server2.receiver.waitForCompletion();
|
|
75
|
+
const resumeDuration = Date.now() - tResumeStart;
|
|
76
|
+
await server2.close();
|
|
77
|
+
|
|
78
|
+
// --- Verification ---
|
|
79
|
+
assert.ok(existsSync(destFile), 'Destination file must be finalized');
|
|
80
|
+
assert.strictEqual(existsSync(partPath), false, 'Part file must be removed');
|
|
81
|
+
assert.strictEqual(existsSync(statePath), false, 'State file must be cleaned up');
|
|
82
|
+
|
|
83
|
+
const destData = await fs.readFile(destFile);
|
|
84
|
+
assert.strictEqual(destData.length, size, 'Final size must match');
|
|
85
|
+
const destHash = crypto.createHash('sha256').update(destData).digest('hex');
|
|
86
|
+
assert.strictEqual(destHash, sourceHash, 'Final SHA-256 must match source');
|
|
87
|
+
|
|
88
|
+
const snap = senderMetrics.snapshot();
|
|
89
|
+
// Retransmission must be BOUNDED: only 4 chunks sent, 4 chunks skipped as resume hits!
|
|
90
|
+
assert.strictEqual(snap.resumeHits, 4, 'Must have 4 resume hits (50% skipped)');
|
|
91
|
+
assert.strictEqual(snap.chunksCreated, 4, 'Only 4 new chunks should be created/read');
|
|
92
|
+
assert.strictEqual(snap.fileBytesRead, 4 * 1024 * 1024, 'Only 4 MB should be read from disk');
|
|
93
|
+
|
|
94
|
+
console.log(`Resume completed in ${resumeDuration}ms with 0 re-transmitted bytes for verified chunks.`);
|
|
95
|
+
|
|
96
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
97
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { serve, send } from '../../src/index.js';
|
|
9
|
+
|
|
10
|
+
test('Integration - end-to-end 10 MB single file transfer with SHA-256 verification', async () => {
|
|
11
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-test-'));
|
|
12
|
+
const sourceFile = path.join(tmpDir, '10mb.bin');
|
|
13
|
+
const destDir = path.join(tmpDir, 'dest');
|
|
14
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
// Generate 10 MB random binary data
|
|
17
|
+
const size = 10 * 1024 * 1024;
|
|
18
|
+
const data = crypto.randomBytes(size);
|
|
19
|
+
await fs.writeFile(sourceFile, data);
|
|
20
|
+
|
|
21
|
+
const sourceHash = crypto.createHash('sha256').update(data).digest('hex');
|
|
22
|
+
|
|
23
|
+
const port = 7521;
|
|
24
|
+
const server = await serve({ port, destinationDir: destDir });
|
|
25
|
+
|
|
26
|
+
const senderMetrics = await send({
|
|
27
|
+
sourcePath: sourceFile,
|
|
28
|
+
host: '127.0.0.1',
|
|
29
|
+
port,
|
|
30
|
+
pipelineOptions: {
|
|
31
|
+
chunkSize: 1024 * 1024, // 1 MiB
|
|
32
|
+
maxEntries: 4, // 4 MiB generations
|
|
33
|
+
maxBytes: 4 * 1024 * 1024
|
|
34
|
+
},
|
|
35
|
+
showProgress: false
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await server.receiver.waitForCompletion();
|
|
39
|
+
await server.close();
|
|
40
|
+
|
|
41
|
+
const destFile = path.join(destDir, '10mb.bin');
|
|
42
|
+
assert.ok(existsSync(destFile), 'Destination file must exist');
|
|
43
|
+
|
|
44
|
+
const destData = await fs.readFile(destFile);
|
|
45
|
+
assert.strictEqual(destData.length, size, 'File size must match');
|
|
46
|
+
|
|
47
|
+
const destHash = crypto.createHash('sha256').update(destData).digest('hex');
|
|
48
|
+
assert.strictEqual(destHash, sourceHash, 'SHA-256 checksum must match source');
|
|
49
|
+
|
|
50
|
+
// Verify atomic rename and state cleanup
|
|
51
|
+
assert.strictEqual(existsSync(`${destFile}.teyyare-part`), false, 'Part file must not remain');
|
|
52
|
+
assert.strictEqual(existsSync(`${destFile}.teyyare-state`), false, 'State file must be cleaned up');
|
|
53
|
+
|
|
54
|
+
const senderStats = senderMetrics.snapshot();
|
|
55
|
+
assert.strictEqual(senderStats.chunksCreated, 10);
|
|
56
|
+
assert.strictEqual(senderStats.fileBytesRead, size);
|
|
57
|
+
assert.ok(senderStats.throughputMBs > 0, 'Throughput must be greater than 0');
|
|
58
|
+
|
|
59
|
+
const receiverStats = server.metrics.snapshot();
|
|
60
|
+
assert.strictEqual(receiverStats.fileBytesWritten, size);
|
|
61
|
+
assert.strictEqual(receiverStats.chunksVerified, 10);
|
|
62
|
+
|
|
63
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
64
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { MutableBuffer, ArenaStore, IllegalStateTransitionError, LifecycleState } from 'muttafa';
|
|
4
|
+
|
|
5
|
+
test('Ownership - generation lifecycle transitions: MUTABLE -> FROZEN -> FLUSHING -> FLUSHED -> RELEASED', () => {
|
|
6
|
+
const store = new ArenaStore();
|
|
7
|
+
const mutable = new MutableBuffer(1, store);
|
|
8
|
+
|
|
9
|
+
mutable.append(1, 1, new Uint8Array([1]), new Uint8Array([2]));
|
|
10
|
+
assert.strictEqual(mutable.lifecycle.state, LifecycleState.MUTABLE);
|
|
11
|
+
|
|
12
|
+
// Freeze
|
|
13
|
+
const frozen = mutable.freeze();
|
|
14
|
+
assert.strictEqual(frozen.lifecycle.state, LifecycleState.FROZEN);
|
|
15
|
+
|
|
16
|
+
// Mark in-flight (flushing)
|
|
17
|
+
frozen.markFlushing();
|
|
18
|
+
assert.strictEqual(frozen.lifecycle.state, LifecycleState.FLUSHING);
|
|
19
|
+
|
|
20
|
+
// Mark delivered (flushed)
|
|
21
|
+
frozen.markFlushed();
|
|
22
|
+
assert.strictEqual(frozen.lifecycle.state, LifecycleState.FLUSHED);
|
|
23
|
+
|
|
24
|
+
// Safe ownership release
|
|
25
|
+
frozen.release();
|
|
26
|
+
assert.strictEqual(frozen.lifecycle.state, LifecycleState.RELEASED);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('Ownership - assertion detects release while in-flight', () => {
|
|
30
|
+
const store = new ArenaStore();
|
|
31
|
+
const mutable = new MutableBuffer(2, store);
|
|
32
|
+
mutable.append(1, 1, new Uint8Array([1]), new Uint8Array([2]));
|
|
33
|
+
|
|
34
|
+
const frozen = mutable.freeze();
|
|
35
|
+
frozen.markFlushing(); // In-flight
|
|
36
|
+
|
|
37
|
+
// Attempting release while in-flight must be rejected!
|
|
38
|
+
assert.throws(() => {
|
|
39
|
+
frozen.release();
|
|
40
|
+
}, IllegalStateTransitionError);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('Ownership - assertion detects double release', () => {
|
|
44
|
+
const store = new ArenaStore();
|
|
45
|
+
const mutable = new MutableBuffer(3, store);
|
|
46
|
+
mutable.append(1, 1, new Uint8Array([1]), new Uint8Array([2]));
|
|
47
|
+
|
|
48
|
+
const frozen = mutable.freeze();
|
|
49
|
+
frozen.markFlushing();
|
|
50
|
+
frozen.markFlushed();
|
|
51
|
+
frozen.release();
|
|
52
|
+
|
|
53
|
+
// Second release must throw IllegalStateTransitionError
|
|
54
|
+
assert.throws(() => {
|
|
55
|
+
frozen.release();
|
|
56
|
+
}, IllegalStateTransitionError);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('Ownership - assertion detects use after release', () => {
|
|
60
|
+
const store = new ArenaStore();
|
|
61
|
+
const mutable = new MutableBuffer(4, store);
|
|
62
|
+
mutable.append(1, 1, new Uint8Array([1]), new Uint8Array([2]));
|
|
63
|
+
|
|
64
|
+
const frozen = mutable.freeze();
|
|
65
|
+
frozen.markFlushing();
|
|
66
|
+
frozen.markFlushed();
|
|
67
|
+
frozen.release();
|
|
68
|
+
|
|
69
|
+
// Store is emptied and released
|
|
70
|
+
assert.strictEqual(frozen.length, 0);
|
|
71
|
+
const bufs = frozen.buffers();
|
|
72
|
+
assert.strictEqual(bufs[0].byteLength, 0);
|
|
73
|
+
assert.strictEqual(bufs[1].byteLength, 0);
|
|
74
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
Opcodes,
|
|
5
|
+
CHUNK_META_SIZE,
|
|
6
|
+
packChunkMeta,
|
|
7
|
+
unpackChunkMeta,
|
|
8
|
+
packFileBegin,
|
|
9
|
+
unpackFileBegin,
|
|
10
|
+
packTransferBegin,
|
|
11
|
+
unpackTransferBegin,
|
|
12
|
+
packFileEnd,
|
|
13
|
+
unpackFileEnd
|
|
14
|
+
} from '../../src/core/protocol.js';
|
|
15
|
+
|
|
16
|
+
test('Protocol - pack and unpack chunk metadata', () => {
|
|
17
|
+
const meta = {
|
|
18
|
+
fileId: 42,
|
|
19
|
+
chunkIndex: 105,
|
|
20
|
+
offset: 104857600n,
|
|
21
|
+
length: 1048576,
|
|
22
|
+
checksum: 0x1a2b3c4d,
|
|
23
|
+
flags: 1
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const packed = packChunkMeta(meta);
|
|
27
|
+
assert.strictEqual(packed.byteLength, CHUNK_META_SIZE);
|
|
28
|
+
|
|
29
|
+
const unpacked = unpackChunkMeta(packed);
|
|
30
|
+
assert.strictEqual(unpacked.fileId, meta.fileId);
|
|
31
|
+
assert.strictEqual(unpacked.chunkIndex, meta.chunkIndex);
|
|
32
|
+
assert.strictEqual(unpacked.offset, meta.offset);
|
|
33
|
+
assert.strictEqual(unpacked.length, meta.length);
|
|
34
|
+
assert.strictEqual(unpacked.checksum, meta.checksum);
|
|
35
|
+
assert.strictEqual(unpacked.flags, meta.flags);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Protocol - pack and unpack file begin and end', () => {
|
|
39
|
+
const fbegin = packFileBegin({
|
|
40
|
+
fileId: 7,
|
|
41
|
+
fileSize: 10737418240, // 10 GB
|
|
42
|
+
path: 'videos/action/movie.mkv',
|
|
43
|
+
sha256: 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789'
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const unpackedBegin = unpackFileBegin(fbegin);
|
|
47
|
+
assert.strictEqual(unpackedBegin.fileId, 7);
|
|
48
|
+
assert.strictEqual(unpackedBegin.fileSize, 10737418240n);
|
|
49
|
+
assert.strictEqual(unpackedBegin.path, 'videos/action/movie.mkv');
|
|
50
|
+
assert.strictEqual(unpackedBegin.sha256, 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
|
51
|
+
|
|
52
|
+
const fend = packFileEnd({
|
|
53
|
+
fileId: 7,
|
|
54
|
+
sha256: 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const unpackedEnd = unpackFileEnd(fend);
|
|
58
|
+
assert.strictEqual(unpackedEnd.fileId, 7);
|
|
59
|
+
assert.strictEqual(unpackedEnd.sha256, 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('Protocol - pack and unpack transfer begin', () => {
|
|
63
|
+
const tbegin = packTransferBegin({
|
|
64
|
+
transferId: 'session-uuid-12345',
|
|
65
|
+
fileCount: 250,
|
|
66
|
+
totalBytes: 53687091200
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const unpacked = unpackTransferBegin(tbegin);
|
|
70
|
+
assert.strictEqual(unpacked.transferId, 'session-uuid-12345');
|
|
71
|
+
assert.strictEqual(unpacked.fileCount, 250);
|
|
72
|
+
assert.strictEqual(unpacked.totalBytes, 53687091200n);
|
|
73
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { FileResumeState } from '../../src/resume/state.js';
|
|
7
|
+
|
|
8
|
+
test('FileResumeState - tracks verified chunks and missing chunks', async () => {
|
|
9
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-state-test-'));
|
|
10
|
+
const statePath = path.join(tmpDir, 'test.state');
|
|
11
|
+
|
|
12
|
+
const state = new FileResumeState({
|
|
13
|
+
transferId: 't1',
|
|
14
|
+
fileId: 1,
|
|
15
|
+
fileSize: 10 * 1024 * 1024, // 10 MB
|
|
16
|
+
chunkSize: 1024 * 1024 // 1 MiB -> 10 chunks
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
assert.strictEqual(state.totalChunks, 10);
|
|
20
|
+
assert.strictEqual(state.verifiedCount, 0);
|
|
21
|
+
assert.strictEqual(state.isComplete(), false);
|
|
22
|
+
|
|
23
|
+
state.markVerified(0);
|
|
24
|
+
state.markVerified(1);
|
|
25
|
+
state.markVerified(5);
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(state.isVerified(0), true);
|
|
28
|
+
assert.strictEqual(state.isVerified(2), false);
|
|
29
|
+
assert.strictEqual(state.verifiedCount, 3);
|
|
30
|
+
|
|
31
|
+
const missing = state.getMissingChunks();
|
|
32
|
+
assert.deepStrictEqual(missing, [2, 3, 4, 6, 7, 8, 9]);
|
|
33
|
+
|
|
34
|
+
// Atomic save and reload
|
|
35
|
+
await state.save(statePath);
|
|
36
|
+
|
|
37
|
+
const reloaded = await FileResumeState.load(statePath);
|
|
38
|
+
assert.ok(reloaded);
|
|
39
|
+
assert.strictEqual(reloaded.transferId, 't1');
|
|
40
|
+
assert.strictEqual(reloaded.verifiedCount, 3);
|
|
41
|
+
assert.strictEqual(reloaded.isVerified(5), true);
|
|
42
|
+
assert.strictEqual(reloaded.isVerified(3), false);
|
|
43
|
+
|
|
44
|
+
await FileResumeState.cleanup(statePath);
|
|
45
|
+
const afterCleanup = await FileResumeState.load(statePath);
|
|
46
|
+
assert.strictEqual(afterCleanup, null);
|
|
47
|
+
|
|
48
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
49
|
+
});
|