pmtiles-swarm 0.37.0 → 0.37.2
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 +34 -0
- package/package.json +2 -2
- package/src/engines/composite.js +20 -6
- package/src/engines/libtorrent.js +51 -0
- package/src/library.js +25 -7
- package/src/web/index.html +28 -16
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.37.2
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
|
|
13
|
+
### 🐞 Bug fixes
|
|
14
|
+
- **Pausing an archive now stops it.** Reported from the field: an archive was paused, the row read
|
|
15
|
+
`paused`, and it went on downloading at 8.4 MiB/s. Nothing in the chain refused — the pause was
|
|
16
|
+
asked for, reported as done, and never happened.
|
|
17
|
+
|
|
18
|
+
Three faults in a line. `LibtorrentEngine` had no `pause` or `resume` at all, so there was no way
|
|
19
|
+
to stop a libtorrent torrent from here. `CompositeEngine.pause` answered for its primary alone,
|
|
20
|
+
so an archive held by a secondary reported as not stopped when it was. And `Library.pause` tested
|
|
21
|
+
only that the engine *had* a pause method and threw away what it answered — a composite has one
|
|
22
|
+
whatever its engines can do, so the `false` went into a void, the fallback never ran, and
|
|
23
|
+
`paused: true` went into the catalog regardless. The console prefers that flag to the engine's
|
|
24
|
+
live state, which is why the row said `paused` while Down and Up kept moving.
|
|
25
|
+
|
|
26
|
+
Requires pmtiles-torrent 0.9.0, which adds the `pause` and `resume` the sidecar never had — and
|
|
27
|
+
makes them stick. `handle.pause()` alone is not a stop: libtorrent's auto-manager clears the
|
|
28
|
+
paused flag again within about a second, so pausing that way produces a torrent that describes
|
|
29
|
+
itself as paused while it transfers. That would have reproduced this exact symptom one layer
|
|
30
|
+
deeper.
|
|
31
|
+
|
|
32
|
+
Nothing was left in a bad state by this: because the pause never took effect, no archive was
|
|
33
|
+
half-stopped and no resume data is wrong. They were seeding and downloading throughout.
|
|
34
|
+
|
|
35
|
+
## 0.37.1
|
|
36
|
+
### ✨ Features and improvements
|
|
37
|
+
- **Cancel now sits in the row it cancels.** Collected into a bar underneath the list, each button
|
|
38
|
+
had to repeat the whole filename to say which add it stopped — two of those filled a line, and
|
|
39
|
+
pressing the right one meant matching a long name against the list above it. The rows carry their
|
|
40
|
+
own, and an add that cannot be cancelled keeps an empty cell so the columns stay lined up.
|
|
41
|
+
|
|
42
|
+
### 🐞 Bug fixes
|
|
43
|
+
|
|
10
44
|
## 0.37.0
|
|
11
45
|
### ✨ Features and improvements
|
|
12
46
|
- **An archive being hashed can now be cancelled, and says how far through it is.** 0.36.0 moved
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
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.9.0",
|
|
50
50
|
"webtorrent": "^3.0.21"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
package/src/engines/composite.js
CHANGED
|
@@ -543,26 +543,40 @@ export class CompositeEngine {
|
|
|
543
543
|
|
|
544
544
|
/**
|
|
545
545
|
* Stops offering an archive, everywhere.
|
|
546
|
+
*
|
|
547
|
+
* Answers for the whole composite rather than for the primary alone. An
|
|
548
|
+
* archive can be held by a secondary and not by the primary, and reporting
|
|
549
|
+
* the primary's "no" for it told the caller nothing was stopped when
|
|
550
|
+
* something was -- which is a false negative that costs data, since the
|
|
551
|
+
* caller's answer to a pause it cannot get is to remove the torrent instead.
|
|
546
552
|
* @param {string} infoHash - The archive.
|
|
547
|
-
* @returns {Promise<boolean>} - Whether
|
|
553
|
+
* @returns {Promise<boolean>} - Whether any engine stopped it.
|
|
548
554
|
*/
|
|
549
555
|
async pause(infoHash) {
|
|
556
|
+
let stopped = false;
|
|
550
557
|
for (const engine of this.#secondaries) {
|
|
551
|
-
await engine.pause?.(infoHash).catch(() =>
|
|
558
|
+
stopped = (await engine.pause?.(infoHash).catch(() => false)) || stopped;
|
|
552
559
|
}
|
|
553
|
-
|
|
560
|
+
const primary = this.#primary.pause
|
|
561
|
+
? await this.#primary.pause(infoHash)
|
|
562
|
+
: false;
|
|
563
|
+
return primary || stopped;
|
|
554
564
|
}
|
|
555
565
|
|
|
556
566
|
/**
|
|
557
567
|
* Offers it again, everywhere.
|
|
558
568
|
* @param {string} infoHash - The archive.
|
|
559
|
-
* @returns {Promise<boolean>} - Whether
|
|
569
|
+
* @returns {Promise<boolean>} - Whether any engine started it.
|
|
560
570
|
*/
|
|
561
571
|
async resume(infoHash) {
|
|
572
|
+
let started = false;
|
|
562
573
|
for (const engine of this.#secondaries) {
|
|
563
|
-
await engine.resume?.(infoHash).catch(() =>
|
|
574
|
+
started = (await engine.resume?.(infoHash).catch(() => false)) || started;
|
|
564
575
|
}
|
|
565
|
-
|
|
576
|
+
const primary = this.#primary.resume
|
|
577
|
+
? await this.#primary.resume(infoHash)
|
|
578
|
+
: false;
|
|
579
|
+
return primary || started;
|
|
566
580
|
}
|
|
567
581
|
|
|
568
582
|
/**
|
|
@@ -415,6 +415,57 @@ export class LibtorrentEngine {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Stops a torrent, leaving its data and its place in the session alone.
|
|
420
|
+
*
|
|
421
|
+
* Not removal. The bytes stay, the resume data stays, and starting it again
|
|
422
|
+
* costs nothing -- which is the whole reason this exists rather than the
|
|
423
|
+
* remove-and-re-add a missing pause used to fall back to. Re-adding a
|
|
424
|
+
* 698 GiB archive means hashing the store again to arrive where it already
|
|
425
|
+
* was.
|
|
426
|
+
* @param {string} infoHash - The archive to stop.
|
|
427
|
+
* @returns {Promise<boolean>} - Whether it was stopped.
|
|
428
|
+
*/
|
|
429
|
+
async pause(infoHash) {
|
|
430
|
+
await this.#stopStart('pause', infoHash);
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Offers a stopped torrent again.
|
|
436
|
+
* @param {string} infoHash - The archive to start.
|
|
437
|
+
* @returns {Promise<boolean>} - Whether it was started.
|
|
438
|
+
*/
|
|
439
|
+
async resume(infoHash) {
|
|
440
|
+
await this.#stopStart('resume', infoHash);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Pause and resume differ only in the word, including how they fail.
|
|
446
|
+
* @param {string} op - 'pause' or 'resume'.
|
|
447
|
+
* @param {string} infoHash - The archive.
|
|
448
|
+
* @returns {Promise<object>} - The sidecar's answer.
|
|
449
|
+
*/
|
|
450
|
+
async #stopStart(op, infoHash) {
|
|
451
|
+
try {
|
|
452
|
+
return await this.#call(op, { infoHash });
|
|
453
|
+
} catch (error) {
|
|
454
|
+
// An older sidecar answers "unknown op", which is true and useless: it
|
|
455
|
+
// reads as a bug in the request rather than as a package that needs
|
|
456
|
+
// updating. Worth saying plainly, because before 0.9.0 there was no
|
|
457
|
+
// pause here at all -- the request reached the catalog and stopped, so
|
|
458
|
+
// the console showed `paused` beside an archive still transferring.
|
|
459
|
+
if (/unknown op/i.test(error.message)) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`this sidecar cannot ${op}; pmtiles-torrent 0.9.0 or newer is needed`,
|
|
462
|
+
{ cause: error },
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
418
469
|
async list() {
|
|
419
470
|
// A node that is shutting down still has a console polling it and a sweep
|
|
420
471
|
// or two in flight. Answering "the sidecar exited" to each of them fills
|
package/src/library.js
CHANGED
|
@@ -1866,11 +1866,24 @@ export class Library {
|
|
|
1866
1866
|
throw error;
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1869
|
+
// Whether it actually stopped, not whether something was asked.
|
|
1870
|
+
//
|
|
1871
|
+
// This tested only that the engine *had* a pause method and threw the
|
|
1872
|
+
// answer away. A composite has one whatever its engines can do, so a
|
|
1873
|
+
// primary with no pause of its own returned false into a void: the
|
|
1874
|
+
// fallback below never ran, `paused: true` went into the catalog, and the
|
|
1875
|
+
// console -- which prefers that flag to the engine's live state -- showed
|
|
1876
|
+
// `paused` beside an archive still transferring at 8 MiB/s. The button did
|
|
1877
|
+
// nothing and said it had worked.
|
|
1878
|
+
const stopped = this.#engine.pause
|
|
1879
|
+
? await this.#engine.pause(infoHash)
|
|
1880
|
+
: false;
|
|
1881
|
+
if (!stopped) {
|
|
1872
1882
|
// Removing without its data is a pause an engine cannot refuse; resume
|
|
1873
|
-
// adds it back and it rechecks what is already on disk.
|
|
1883
|
+
// adds it back and it rechecks what is already on disk. A last resort,
|
|
1884
|
+
// because that recheck is the whole store -- tens of minutes for a
|
|
1885
|
+
// planet archive -- which is why an engine that can really pause is
|
|
1886
|
+
// worth the two operations it takes.
|
|
1874
1887
|
await this.#engine
|
|
1875
1888
|
.remove(infoHash, { deleteData: false })
|
|
1876
1889
|
.catch(() => {});
|
|
@@ -1954,9 +1967,14 @@ export class Library {
|
|
|
1954
1967
|
throw error;
|
|
1955
1968
|
}
|
|
1956
1969
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1970
|
+
// Same as pause: the answer decides, not the presence of a method. An
|
|
1971
|
+
// archive stopped by the fallback above is not in the engine at all, so a
|
|
1972
|
+
// resume it merely claimed would leave the catalog saying the archive was
|
|
1973
|
+
// running while nothing held it.
|
|
1974
|
+
const started = this.#engine.resume
|
|
1975
|
+
? await this.#engine.resume(infoHash)
|
|
1976
|
+
: false;
|
|
1977
|
+
if (!started) {
|
|
1960
1978
|
await this.#readd({ ...entry, paused: false });
|
|
1961
1979
|
}
|
|
1962
1980
|
await this.#tiles?.invalidate(infoHash).catch(() => {});
|
package/src/web/index.html
CHANGED
|
@@ -309,6 +309,16 @@
|
|
|
309
309
|
.piecerow { display: grid; grid-template-columns: 7rem 1fr 4rem; gap: 0.6rem; align-items: center; margin-bottom: 0.5rem; }
|
|
310
310
|
.piecerow > .label { font-size: 0.8rem; color: var(--muted); }
|
|
311
311
|
.piecerow > .value { font-size: 0.8rem; color: var(--muted); text-align: right; }
|
|
312
|
+
/* Like a piecerow, but the label is a filename rather than a fixed word,
|
|
313
|
+
and each row carries its own Cancel. A separate class because the
|
|
314
|
+
piecerow's 7rem label and 4rem value are sized for "downloaded" and
|
|
315
|
+
"41%": an archive name in 7rem wraps onto three lines, and the naming
|
|
316
|
+
of the buttons had to repeat the whole filename to say which was
|
|
317
|
+
which. */
|
|
318
|
+
.addrow { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 0.6rem; align-items: center; margin-bottom: 0.5rem; }
|
|
319
|
+
.addrow > .label { font-size: 0.8rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
320
|
+
.addrow > .value { font-size: 0.8rem; color: var(--muted); text-align: right; white-space: nowrap; }
|
|
321
|
+
.addrow > button { padding: 0.15rem 0.55rem; font-size: 0.75rem; }
|
|
312
322
|
|
|
313
323
|
button.speed { border-color: var(--line); color: var(--muted); }
|
|
314
324
|
button.speed.on { border-color: var(--warn); color: var(--warn); }
|
|
@@ -923,7 +933,6 @@
|
|
|
923
933
|
box.innerHTML = '';
|
|
924
934
|
return;
|
|
925
935
|
}
|
|
926
|
-
const cancellable = running.filter((add) => add.cancellable !== false);
|
|
927
936
|
box.innerHTML = `
|
|
928
937
|
<h3 style="margin:1.2rem 0 0.5rem">Being added, before a torrent exists</h3>
|
|
929
938
|
<div class="sub" style="margin-bottom:0.6rem">
|
|
@@ -958,26 +967,29 @@
|
|
|
958
967
|
: pct == null
|
|
959
968
|
? bytes(add.received)
|
|
960
969
|
: `${pct.toFixed(1)}%`;
|
|
970
|
+
const name = add.name ?? add.url.split('/').pop() ?? add.url;
|
|
971
|
+
// Cancel sits in the row it cancels, so the button does not have
|
|
972
|
+
// to name the archive to say which one it is. Named, they were as
|
|
973
|
+
// wide as the filename — two of them filled a line, and reading
|
|
974
|
+
// one meant matching a long name against the list above it.
|
|
975
|
+
//
|
|
976
|
+
// An add with no controller keeps an empty cell rather than
|
|
977
|
+
// losing one, so the rows above and below it stay lined up.
|
|
978
|
+
const stop =
|
|
979
|
+
add.cancellable === false
|
|
980
|
+
? '<span></span>'
|
|
981
|
+
: `<button data-cancel="${escapeHtml(add.url)}" title="Cancel ${escapeHtml(
|
|
982
|
+
name,
|
|
983
|
+
)}" aria-label="Cancel ${escapeHtml(name)}">Cancel</button>`;
|
|
961
984
|
return `
|
|
962
|
-
<div class="
|
|
963
|
-
<span class="label" title="${escapeHtml(add.url)}">${escapeHtml(
|
|
964
|
-
add.name ?? add.url.split('/').pop() ?? add.url,
|
|
965
|
-
)}</span>
|
|
985
|
+
<div class="addrow">
|
|
986
|
+
<span class="label" title="${escapeHtml(add.url)}">${escapeHtml(name)}</span>
|
|
966
987
|
<span class="track"><i style="width:${pct == null ? 0 : pct.toFixed(1)}%"></i></span>
|
|
967
988
|
<span class="value">${value}</span>
|
|
989
|
+
${stop}
|
|
968
990
|
</div>`;
|
|
969
991
|
})
|
|
970
|
-
.join('')}
|
|
971
|
-
<div class="bar" style="margin-top:0.5rem">
|
|
972
|
-
${cancellable
|
|
973
|
-
.map(
|
|
974
|
-
(add) =>
|
|
975
|
-
`<button data-cancel="${escapeHtml(add.url)}">Cancel ${escapeHtml(
|
|
976
|
-
add.name ?? add.url.split('/').pop() ?? '',
|
|
977
|
-
)}</button>`,
|
|
978
|
-
)
|
|
979
|
-
.join('')}
|
|
980
|
-
</div>`;
|
|
992
|
+
.join('')}`;
|
|
981
993
|
|
|
982
994
|
for (const button of box.querySelectorAll('[data-cancel]')) {
|
|
983
995
|
button.onclick = async () => {
|