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.
@@ -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
+ });