pmtiles-swarm 0.35.4 โ 0.36.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/CHANGELOG.md +42 -0
- package/package.json +2 -2
- package/src/engines/libtorrent.js +130 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,48 @@
|
|
|
7
7
|
### ๐ Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.36.0
|
|
11
|
+
### โจ Features and improvements
|
|
12
|
+
- **Hashing an archive now happens in a process of its own.** Building the torrent for a 698 GiB
|
|
13
|
+
archive ran inside the libtorrent sidecar, competing with the session for the disk and for
|
|
14
|
+
Python's interpreter lock while every archive on the node was being served from that same disk.
|
|
15
|
+
It also could not be stopped: libtorrent's hashing never checks for interruption, and the sidecar
|
|
16
|
+
cannot be ended to end a hash because it holds the session and every torrent seeding from it. A
|
|
17
|
+
build started by a misclick ran its full six hours.
|
|
18
|
+
|
|
19
|
+
It is now `libtorrent_sidecar.py --create`, started per hash, holding no session and no port.
|
|
20
|
+
Killing it costs the hash and nothing else, and hashing only ever reads, so the archive is
|
|
21
|
+
untouched. It reports the piece it has reached as it goes, so a caller can draw a real fraction
|
|
22
|
+
rather than "hashing 698 GiB ยท 3m".
|
|
23
|
+
|
|
24
|
+
Requires pmtiles-torrent 0.8.0. Also picks up 0.7.5, which stops an archive that is hashing its
|
|
25
|
+
store from reporting itself as "paused" โ libtorrent hashes one store at a time and flags every
|
|
26
|
+
torrent queued behind it as paused, so a library busy verifying itself read as one somebody had
|
|
27
|
+
stopped.
|
|
28
|
+
|
|
29
|
+
### ๐ Bug fixes
|
|
30
|
+
|
|
31
|
+
## 0.35.5
|
|
32
|
+
### โจ Features and improvements
|
|
33
|
+
|
|
34
|
+
### ๐ Bug fixes
|
|
35
|
+
- **Requires pmtiles-torrent 0.7.4, which stops archives dropping out of the engine a few more with
|
|
36
|
+
every restart.** Seen here as `[restore] <archive>: mismatching info-hash`, beginning with one
|
|
37
|
+
archive and reaching eighteen of twenty. An archive that failed this way was never handed to the
|
|
38
|
+
engine at all, so the console showed it at 0% with no state, a recheck answered `no such
|
|
39
|
+
torrent`, and its data sat complete on the disk the whole time โ the preview rendered from it
|
|
40
|
+
perfectly well.
|
|
41
|
+
|
|
42
|
+
The sidecar was writing resume data under the wrong torrent's name: saving it was the last thing
|
|
43
|
+
still popping libtorrent's alert queue on its own thread while the alert pump popped on another,
|
|
44
|
+
and the pump's next pop freed the batch that loop was reading. `add` then refuses such a file
|
|
45
|
+
with "mismatching info-hash". 0.7.2 did not introduce it but made the pump pop far more often,
|
|
46
|
+
which is why it appeared immediately after that upgrade.
|
|
47
|
+
|
|
48
|
+
Restarting on this version is the whole recovery: an add refused over resume data is retried
|
|
49
|
+
without it, and the recheck finds every byte already on disk. Nothing is downloaded again,
|
|
50
|
+
though rechecking a large archive is not quick.
|
|
51
|
+
|
|
10
52
|
## 0.35.4
|
|
11
53
|
### โจ Features and improvements
|
|
12
54
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"maplibre-gl": "^6.2.0",
|
|
47
47
|
"parse-torrent": "^11.0.24",
|
|
48
48
|
"pmtiles": "^4.4.1",
|
|
49
|
-
"pmtiles-torrent": "^0.
|
|
49
|
+
"pmtiles-torrent": "^0.8.0",
|
|
50
50
|
"webtorrent": "^3.0.21"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
@@ -491,13 +491,23 @@ export class LibtorrentEngine {
|
|
|
491
491
|
|
|
492
492
|
/**
|
|
493
493
|
* Creates a torrent from a local file.
|
|
494
|
+
*
|
|
495
|
+
* In a process of its own rather than over the pipe. libtorrent's hashing
|
|
496
|
+
* never checks for interruption, so a hash running inside the sidecar could
|
|
497
|
+
* not be stopped -- and the sidecar itself cannot be ended to stop one,
|
|
498
|
+
* because it holds the session and every torrent seeding from it. A 698 GiB
|
|
499
|
+
* build started by a misclick therefore ran its full six hours, saturating
|
|
500
|
+
* the disk the rest of the library was being served from.
|
|
501
|
+
*
|
|
502
|
+
* A process started for one hash can simply be killed. Hashing only reads,
|
|
503
|
+
* so nothing is left half-written, and the archive is untouched.
|
|
494
504
|
* @param {string} filePath - The file to hash.
|
|
495
|
-
* @param {object} [options] - Piece length, trackers, web seeds, format
|
|
505
|
+
* @param {object} [options] - Piece length, trackers, web seeds, format,
|
|
506
|
+
* `signal` to cancel with, and `onProgress({piece, pieces})`.
|
|
496
507
|
* @returns {Promise<object>} - The torrent file and what it describes.
|
|
497
508
|
*/
|
|
498
509
|
async createTorrent(filePath, options = {}) {
|
|
499
|
-
const result = await this.#
|
|
500
|
-
'create',
|
|
510
|
+
const result = await this.#hashApart(
|
|
501
511
|
{
|
|
502
512
|
path: filePath,
|
|
503
513
|
pieceLength: options.pieceLength,
|
|
@@ -508,8 +518,7 @@ export class LibtorrentEngine {
|
|
|
508
518
|
createdBy: options.createdBy,
|
|
509
519
|
format: options.format ?? 'hybrid',
|
|
510
520
|
},
|
|
511
|
-
|
|
512
|
-
options.timeoutMs ?? 6 * 60 * 60 * 1000,
|
|
521
|
+
options,
|
|
513
522
|
);
|
|
514
523
|
return {
|
|
515
524
|
...result,
|
|
@@ -517,6 +526,122 @@ export class LibtorrentEngine {
|
|
|
517
526
|
};
|
|
518
527
|
}
|
|
519
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Runs one `--create` to completion, or until it is no longer wanted.
|
|
531
|
+
* @param {object} params - What to hash and how.
|
|
532
|
+
* @param {object} options - signal, onProgress, timeoutMs.
|
|
533
|
+
* @returns {Promise<object>} - The sidecar's result object.
|
|
534
|
+
*/
|
|
535
|
+
#hashApart(params, options) {
|
|
536
|
+
return new Promise((resolve, reject) => {
|
|
537
|
+
const script = this.#options.script ?? resolveSidecar();
|
|
538
|
+
const child = spawn(this.#options.python, [script, '--create'], {
|
|
539
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
let settled = false;
|
|
543
|
+
let result;
|
|
544
|
+
let failure;
|
|
545
|
+
let pending = '';
|
|
546
|
+
let stderr = '';
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Ends this hash once, whatever ends it.
|
|
550
|
+
* @param {Error} [error] - Why, if it failed.
|
|
551
|
+
*/
|
|
552
|
+
const finish = (error) => {
|
|
553
|
+
if (settled) return;
|
|
554
|
+
settled = true;
|
|
555
|
+
clearTimeout(timer);
|
|
556
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
557
|
+
if (error) reject(error);
|
|
558
|
+
else resolve(result);
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const cancel = () => {
|
|
562
|
+
// The hash cannot be asked to stop, so it is ended. Nothing is lost:
|
|
563
|
+
// hashing reads and this process holds nothing else.
|
|
564
|
+
child.kill();
|
|
565
|
+
// Said the same way however it was cancelled. An AbortController with
|
|
566
|
+
// no reason gives "This operation was aborted", which in a log next to
|
|
567
|
+
// an archive name explains nothing; callers that care which kind of
|
|
568
|
+
// stop this was read `signal.reason`, which is carried as the cause.
|
|
569
|
+
finish(
|
|
570
|
+
new Error('hashing was cancelled', { cause: options.signal?.reason }),
|
|
571
|
+
);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// Hashing a large archive takes as long as it takes, but not forever:
|
|
575
|
+
// a hash that has stopped reporting is stuck, and holding the add open
|
|
576
|
+
// for six hours to discover that helps nobody.
|
|
577
|
+
const timer = setTimeout(
|
|
578
|
+
() => {
|
|
579
|
+
child.kill();
|
|
580
|
+
finish(new Error('hashing timed out'));
|
|
581
|
+
},
|
|
582
|
+
options.timeoutMs ?? 6 * 60 * 60 * 1000,
|
|
583
|
+
);
|
|
584
|
+
timer.unref?.();
|
|
585
|
+
|
|
586
|
+
if (options.signal?.aborted) return cancel();
|
|
587
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
588
|
+
|
|
589
|
+
child.stdout.setEncoding('utf8');
|
|
590
|
+
child.stdout.on('data', (chunk) => {
|
|
591
|
+
pending += chunk;
|
|
592
|
+
const lines = pending.split('\n');
|
|
593
|
+
pending = lines.pop() ?? '';
|
|
594
|
+
for (const line of lines) {
|
|
595
|
+
if (!line.trim()) continue;
|
|
596
|
+
let message;
|
|
597
|
+
try {
|
|
598
|
+
message = JSON.parse(line);
|
|
599
|
+
} catch {
|
|
600
|
+
// A line that is not ours. Python's own output on the way to a
|
|
601
|
+
// crash arrives here, and is worth keeping for the error.
|
|
602
|
+
stderr += `${line}\n`;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (message.event === 'progress') {
|
|
606
|
+
options.onProgress?.({
|
|
607
|
+
piece: message.piece,
|
|
608
|
+
pieces: message.pieces,
|
|
609
|
+
});
|
|
610
|
+
} else if (message.ok) {
|
|
611
|
+
result = message.result;
|
|
612
|
+
} else if (message.ok === false) {
|
|
613
|
+
failure = new Error(message.error ?? 'hashing failed');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
child.stderr.setEncoding('utf8');
|
|
619
|
+
child.stderr.on('data', (chunk) => {
|
|
620
|
+
stderr += chunk;
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
child.on('error', (error) =>
|
|
624
|
+
finish(new Error(`could not start the hasher: ${error.message}`)),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
child.on('close', (code, signal) => {
|
|
628
|
+
if (failure) return finish(failure);
|
|
629
|
+
if (result) return finish();
|
|
630
|
+
const why = signal
|
|
631
|
+
? `killed by ${signal}`
|
|
632
|
+
: `exited with code ${code}`;
|
|
633
|
+
finish(
|
|
634
|
+
new Error(
|
|
635
|
+
`hashing produced nothing (${why})${stderr ? `: ${stderr.trim()}` : ''}`,
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
child.stdin.on('error', () => {});
|
|
641
|
+
child.stdin.end(JSON.stringify(params));
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
520
645
|
/**
|
|
521
646
|
* Reads one piece, promoted ahead of the normal picker.
|
|
522
647
|
*
|