pmtiles-swarm 0.98.3 → 0.99.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 +29 -0
- package/package.json +2 -2
- package/src/library.js +76 -7
- package/src/web/index.html +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.99.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **A sidecar crash now names the frame it happened in.** `pmtiles-torrent` 0.11.0 arms
|
|
13
|
+
`faulthandler` in the sidecar, so a segfault writes a Python traceback to stderr on its way out
|
|
14
|
+
and this node forwards it into the log line by line, beside the `sidecar killed by SIGSEGV` that
|
|
15
|
+
used to be the whole story. Nothing to configure; it costs nothing until the process faults.
|
|
16
|
+
|
|
17
|
+
Worth having because that signal is otherwise the only evidence. On the node this came from it
|
|
18
|
+
appeared several times per start for eleven days, and each crash takes the archives handed over
|
|
19
|
+
before it — which is what leaves a library reading `not loaded` after a restart.
|
|
20
|
+
|
|
21
|
+
### 🐞 Bug fixes
|
|
22
|
+
- **An archive the engine took and then did not keep is handed back, rather than only reported.**
|
|
23
|
+
The seeding check has been able to spot this for a while and its own message admitted the rest:
|
|
24
|
+
"nothing will start it before the next restart". It was right, which is why the cure was always
|
|
25
|
+
another restart. The usual cause is a sidecar that dies partway through a restore — the
|
|
26
|
+
replacement holds nothing, so the archives handed over before it died are absent while the ones
|
|
27
|
+
after it are fine, and `add` resolving is no evidence that anything was kept.
|
|
28
|
+
|
|
29
|
+
Those are now handed back once and re-checked, and the log says whether it took. Once, not in a
|
|
30
|
+
loop: an engine that refuses twice will not be talked round by a third try. And only when the
|
|
31
|
+
engine is holding *some* of the library — one holding none of it, or one that could not be listed
|
|
32
|
+
at all, is not suffering a per-archive fault, and re-adding everything on the strength of that
|
|
33
|
+
answer is how a node spends its start hashing what it already had.
|
|
34
|
+
- **A pinned archive in a stack was listed by its infohash.** Forty characters of hex, where the
|
|
35
|
+
picker that offered it had shown a filename. The row leads with the archive's name now and shows
|
|
36
|
+
the infohash as what it resolves to, which for a pinned source is exactly what it means: this
|
|
37
|
+
build and no later one.
|
|
38
|
+
|
|
10
39
|
## 0.98.3
|
|
11
40
|
### ✨ Features and improvements
|
|
12
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"maplibre-gl": "^6.2.0",
|
|
50
50
|
"parse-torrent": "^11.0.24",
|
|
51
51
|
"pmtiles": "^4.4.1",
|
|
52
|
-
"pmtiles-torrent": "^0.
|
|
52
|
+
"pmtiles-torrent": "^0.11.0",
|
|
53
53
|
"webtorrent": "^3.0.21"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
package/src/library.js
CHANGED
|
@@ -2154,16 +2154,46 @@ export class Library {
|
|
|
2154
2154
|
* @param {object[]} entries - The entries restore worked through.
|
|
2155
2155
|
* @returns {Promise<void>} - Resolves once every claim has been checked.
|
|
2156
2156
|
*/
|
|
2157
|
+
/**
|
|
2158
|
+
* Whether the engine is holding this archive right now.
|
|
2159
|
+
*
|
|
2160
|
+
* Asked of one archive rather than by re-listing the library: this runs only
|
|
2161
|
+
* for an archive that has just been handed back, which on a healthy node is
|
|
2162
|
+
* never.
|
|
2163
|
+
* @param {string} infoHash - The archive.
|
|
2164
|
+
* @returns {Promise<boolean>} - Whether the engine has it.
|
|
2165
|
+
*/
|
|
2166
|
+
async #engineHolds(infoHash) {
|
|
2167
|
+
const status = await this.#engine.get?.(infoHash).catch(() => null);
|
|
2168
|
+
return Boolean(status);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2157
2171
|
async #verifySeeding(entries) {
|
|
2158
2172
|
if (entries.length === 0) return;
|
|
2159
2173
|
|
|
2160
2174
|
// One listing rather than a status call each: this runs over the whole
|
|
2161
2175
|
// library on every start, and a round trip per archive is a cost paid by
|
|
2162
2176
|
// every node to catch a fault most of them do not have.
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2177
|
+
// Whether the listing worked is a separate fact from what it contained,
|
|
2178
|
+
// and collapsing the two into an empty map is what made this only ever
|
|
2179
|
+
// able to report. A repair has to know the difference: an engine that
|
|
2180
|
+
// could not be asked is not an engine holding nothing.
|
|
2181
|
+
const listing = await this.#engine.list().then(
|
|
2182
|
+
(all) => ({ answered: true, all }),
|
|
2183
|
+
() => ({ answered: false, all: [] }),
|
|
2184
|
+
);
|
|
2185
|
+
const held = new Map(
|
|
2186
|
+
listing.all.map((status) => [status.infoHash, status]),
|
|
2187
|
+
);
|
|
2188
|
+
|
|
2189
|
+
// Handing archives back one at a time only makes sense against an engine
|
|
2190
|
+
// that is holding some of them. One holding none of what it was just given
|
|
2191
|
+
// is not suffering a per-archive fault -- it is a replacement that came up
|
|
2192
|
+
// empty, or a listing that cannot be trusted -- and re-adding the whole
|
|
2193
|
+
// library on the strength of a bad answer is how a node spends its start
|
|
2194
|
+
// hashing everything it already had. That case is the reconnect handler's,
|
|
2195
|
+
// and it is reported here rather than acted on.
|
|
2196
|
+
const repairable = listing.answered && held.size > 0;
|
|
2167
2197
|
|
|
2168
2198
|
let wrong = 0;
|
|
2169
2199
|
for (const entry of entries) {
|
|
@@ -2178,11 +2208,50 @@ export class Library {
|
|
|
2178
2208
|
// it absent from the engine and unreported by the very check meant to
|
|
2179
2209
|
// notice. Absent is absent — it is neither seeding nor downloading.
|
|
2180
2210
|
if (!status) {
|
|
2211
|
+
// Handed back rather than only reported. This is the one fault here
|
|
2212
|
+
// with an obvious remedy -- the archive was restorable a moment ago,
|
|
2213
|
+
// since restore did it without complaint -- and the usual cause is a
|
|
2214
|
+
// sidecar that died partway through and was replaced by one holding
|
|
2215
|
+
// nothing. The replacement is up by the time this runs, so the second
|
|
2216
|
+
// attempt is against a working engine.
|
|
2217
|
+
//
|
|
2218
|
+
// Once, not in a loop. An engine that refuses twice is not going to be
|
|
2219
|
+
// talked round by a third try, and a restore that retried for ever
|
|
2220
|
+
// would keep a node busy instead of letting it say what is wrong.
|
|
2221
|
+
if (!repairable) {
|
|
2222
|
+
wrong += 1;
|
|
2223
|
+
console.error(
|
|
2224
|
+
`${label}: restore handed this to the engine and the engine is ` +
|
|
2225
|
+
'not holding it. It is neither seeding nor downloading, and ' +
|
|
2226
|
+
'nothing will start it before the next restart.',
|
|
2227
|
+
);
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
const again = await this.#readd(entry).then(
|
|
2232
|
+
() => true,
|
|
2233
|
+
(error) => {
|
|
2234
|
+
console.error(
|
|
2235
|
+
`${label}: could not be handed back: ${error.message}`,
|
|
2236
|
+
);
|
|
2237
|
+
return false;
|
|
2238
|
+
},
|
|
2239
|
+
);
|
|
2240
|
+
const recovered = again && (await this.#engineHolds(entry.infoHash));
|
|
2241
|
+
if (recovered) {
|
|
2242
|
+
console.warn(
|
|
2243
|
+
`${label}: the engine was not holding this after restore, so it ` +
|
|
2244
|
+
'was handed back. It is loaded now.',
|
|
2245
|
+
);
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2181
2249
|
wrong += 1;
|
|
2182
2250
|
console.error(
|
|
2183
|
-
`${label}: restore handed this to the engine
|
|
2184
|
-
'holding it
|
|
2185
|
-
'
|
|
2251
|
+
`${label}: restore handed this to the engine, the engine is not ` +
|
|
2252
|
+
'holding it, and handing it back did not take either. It is ' +
|
|
2253
|
+
'neither seeding nor downloading. The log above this says what ' +
|
|
2254
|
+
'the engine has been doing; a restart is the next thing to try.',
|
|
2186
2255
|
);
|
|
2187
2256
|
continue;
|
|
2188
2257
|
}
|
package/src/web/index.html
CHANGED
|
@@ -7963,6 +7963,17 @@ Every piece is hashed against the ` +
|
|
|
7963
7963
|
? source.bounds.map((n) => Number(n).toFixed(2)).join(', ')
|
|
7964
7964
|
: null;
|
|
7965
7965
|
|
|
7966
|
+
// A pinned archive is named in the recipe by its infohash, which is
|
|
7967
|
+
// forty characters of hex and tells nobody which archive it is. The
|
|
7968
|
+
// filename is what the picker offered and what an operator recognises,
|
|
7969
|
+
// so lead with that and let the infohash be what it resolves to --
|
|
7970
|
+
// which for a pinned source is exactly what it means: this build, and
|
|
7971
|
+
// no later one. Falls back to the hash for an archive that has been
|
|
7972
|
+
// removed, where the name is all that is gone.
|
|
7973
|
+
const pinnedArchive = kind === 'archive';
|
|
7974
|
+
const label =
|
|
7975
|
+
pinnedArchive && source.archiveName ? source.archiveName : source.name;
|
|
7976
|
+
|
|
7966
7977
|
const notes = [];
|
|
7967
7978
|
if (index === all.length - 1 && all.length > 1) {
|
|
7968
7979
|
notes.push('<span class="pill">wins</span>');
|
|
@@ -7975,7 +7986,7 @@ Every piece is hashed against the ` +
|
|
|
7975
7986
|
return `
|
|
7976
7987
|
<tr${source.resolved ? '' : ' class="bad"'}>
|
|
7977
7988
|
<td class="muted">${index}</td>
|
|
7978
|
-
<td><code>${escapeHtml(
|
|
7989
|
+
<td><code>${escapeHtml(label)}</code>
|
|
7979
7990
|
<span class="muted">${kind}</span></td>
|
|
7980
7991
|
<td>${
|
|
7981
7992
|
source.resolved
|
|
@@ -7985,7 +7996,13 @@ Every piece is hashed against the ` +
|
|
|
7985
7996
|
? `<span class="muted" title="Another recipe, evaluated for this tile and merged as one layer. It follows every later change to that stack.">recipe of ${
|
|
7986
7997
|
source.nested ?? 0
|
|
7987
7998
|
} source${source.nested === 1 ? '' : 's'}</span>`
|
|
7988
|
-
:
|
|
7999
|
+
: pinnedArchive
|
|
8000
|
+
? `<code class="muted" title="${escapeHtml(
|
|
8001
|
+
source.infohash ?? source.name,
|
|
8002
|
+
)}">${escapeHtml(
|
|
8003
|
+
(source.infohash ?? source.name).slice(0, 12),
|
|
8004
|
+
)}…</code>`
|
|
8005
|
+
: `<code class="muted">${escapeHtml(source.archiveName ?? '')}</code>`
|
|
7989
8006
|
: '<span class="bad">does not resolve</span>'
|
|
7990
8007
|
}</td>
|
|
7991
8008
|
<td>${zooms}${
|