pmtiles-swarm 0.98.2 โ 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 +45 -0
- package/package.json +2 -2
- package/src/library.js +76 -7
- package/src/web/index.html +48 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@
|
|
|
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
|
+
|
|
39
|
+
## 0.98.3
|
|
40
|
+
### โจ Features and improvements
|
|
41
|
+
|
|
42
|
+
### ๐ Bug fixes
|
|
43
|
+
- **A stack could not be pointed at a completed MBTiles archive.** The source picker offered only
|
|
44
|
+
PMTiles, so an MBTiles the node was perfectly able to serve tiles from was simply absent from the
|
|
45
|
+
menu. It offers a complete one now, by the same rule the tile route enforces โ and still hides one
|
|
46
|
+
that is arriving, since a stack naming that would answer 503 for every tile until the download
|
|
47
|
+
finished.
|
|
48
|
+
- **A planet dump was offered as a stack source.** The same filter read an archive with no recorded
|
|
49
|
+
kind as PMTiles, which is not a harmless default: a mirrored `.osm.pbf` appeared in the menu, and
|
|
50
|
+
naming it would have produced a recipe that failed at its first tile. The kind now falls back to
|
|
51
|
+
the filename before anything is assumed, which is how `prewarm.js` resolves it after the identical
|
|
52
|
+
mistake sent every mirrored dump round the header-read backoff for ever. Such an archive is also
|
|
53
|
+
labelled `unknown ยท not servable` in the list, where it previously said nothing at all.
|
|
54
|
+
|
|
10
55
|
## 0.98.2
|
|
11
56
|
### โจ Features and improvements
|
|
12
57
|
|
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
|
@@ -1836,11 +1836,31 @@
|
|
|
1836
1836
|
* @returns {boolean} - Whether to offer the tile endpoints.
|
|
1837
1837
|
*/
|
|
1838
1838
|
const isServable = (entry) => {
|
|
1839
|
-
const kind = entry
|
|
1839
|
+
const kind = kindOf(entry);
|
|
1840
1840
|
if (kind === 'pmtiles') return true;
|
|
1841
1841
|
return kind === 'mbtiles' && entry.complete === true;
|
|
1842
1842
|
};
|
|
1843
1843
|
|
|
1844
|
+
/**
|
|
1845
|
+
* What kind of archive this is, from the record or failing that the name.
|
|
1846
|
+
*
|
|
1847
|
+
* An entry from before the kind was recorded carries none, and reading
|
|
1848
|
+
* that as PMTiles is not a harmless default: it is how a planet dump ends
|
|
1849
|
+
* up labelled servable and offered as a stack source, where naming it
|
|
1850
|
+
* would produce a recipe that fails at its first tile. `prewarm.js`
|
|
1851
|
+
* resolves it exactly this way, after the same mistake sent every
|
|
1852
|
+
* mirrored .osm.pbf round the header-read backoff for ever.
|
|
1853
|
+
* @param {object} entry - A catalog entry.
|
|
1854
|
+
* @returns {string} - 'pmtiles', 'mbtiles', or 'unknown'.
|
|
1855
|
+
*/
|
|
1856
|
+
const kindOf = (entry) => {
|
|
1857
|
+
if (entry.kind) return entry.kind;
|
|
1858
|
+
const name = entry.name ?? '';
|
|
1859
|
+
if (/\.pmtiles$/i.test(name)) return 'pmtiles';
|
|
1860
|
+
if (/\.mbtiles$/i.test(name)) return 'mbtiles';
|
|
1861
|
+
return 'unknown';
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1844
1864
|
/**
|
|
1845
1865
|
* What to say under the size about an archive that is not PMTiles.
|
|
1846
1866
|
*
|
|
@@ -1852,7 +1872,7 @@
|
|
|
1852
1872
|
* @returns {string} - The note, or empty for a PMTiles archive.
|
|
1853
1873
|
*/
|
|
1854
1874
|
const kindNote = (entry) => {
|
|
1855
|
-
const kind = entry
|
|
1875
|
+
const kind = kindOf(entry);
|
|
1856
1876
|
if (kind === 'pmtiles') return '';
|
|
1857
1877
|
if (kind !== 'mbtiles') return `${escapeHtml(kind)} ยท not servable`;
|
|
1858
1878
|
return entry.complete === true
|
|
@@ -2563,7 +2583,7 @@
|
|
|
2563
2583
|
// anything for an archive read out of the swarm. An MBTiles is read
|
|
2564
2584
|
// whole or not at all โ once it is here there is nothing left to
|
|
2565
2585
|
// fetch, and before that there is no tile to warm.
|
|
2566
|
-
const warmable = (entry
|
|
2586
|
+
const warmable = kindOf(entry) === 'pmtiles';
|
|
2567
2587
|
const torrentUrl = `${base}/archives/${entry.infoHash}/archive.torrent`;
|
|
2568
2588
|
const summary = entry.pmtiles ?? {};
|
|
2569
2589
|
const mode = entry.mode ?? 'mirror';
|
|
@@ -2648,7 +2668,7 @@
|
|
|
2648
2668
|
${
|
|
2649
2669
|
servable
|
|
2650
2670
|
? `<div class="sub" style="margin-bottom:1rem"><code>${escapeHtml(tileJson)}</code></div>`
|
|
2651
|
-
: (entry
|
|
2671
|
+
: kindOf(entry) === 'mbtiles'
|
|
2652
2672
|
? `<div class="sub" style="margin-bottom:1rem">
|
|
2653
2673
|
This MBTiles archive is still arriving, so it has no tile endpoint yet. It is
|
|
2654
2674
|
SQLite, which cannot be read a byte range at a time out of the swarm the way
|
|
@@ -7943,6 +7963,17 @@ Every piece is hashed against the ` +
|
|
|
7943
7963
|
? source.bounds.map((n) => Number(n).toFixed(2)).join(', ')
|
|
7944
7964
|
: null;
|
|
7945
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
|
+
|
|
7946
7977
|
const notes = [];
|
|
7947
7978
|
if (index === all.length - 1 && all.length > 1) {
|
|
7948
7979
|
notes.push('<span class="pill">wins</span>');
|
|
@@ -7955,7 +7986,7 @@ Every piece is hashed against the ` +
|
|
|
7955
7986
|
return `
|
|
7956
7987
|
<tr${source.resolved ? '' : ' class="bad"'}>
|
|
7957
7988
|
<td class="muted">${index}</td>
|
|
7958
|
-
<td><code>${escapeHtml(
|
|
7989
|
+
<td><code>${escapeHtml(label)}</code>
|
|
7959
7990
|
<span class="muted">${kind}</span></td>
|
|
7960
7991
|
<td>${
|
|
7961
7992
|
source.resolved
|
|
@@ -7965,7 +7996,13 @@ Every piece is hashed against the ` +
|
|
|
7965
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 ${
|
|
7966
7997
|
source.nested ?? 0
|
|
7967
7998
|
} source${source.nested === 1 ? '' : 's'}</span>`
|
|
7968
|
-
:
|
|
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>`
|
|
7969
8006
|
: '<span class="bad">does not resolve</span>'
|
|
7970
8007
|
}</td>
|
|
7971
8008
|
<td>${zooms}${
|
|
@@ -8175,10 +8212,12 @@ Every piece is hashed against the ` +
|
|
|
8175
8212
|
// missing from a menu the operator is looking at. This is the same
|
|
8176
8213
|
// endpoint the Archives view reads, and it answers a bare array.
|
|
8177
8214
|
const held = await api('/api/torrents');
|
|
8215
|
+
// The same rule the tile route enforces, because a stack source is
|
|
8216
|
+
// read through exactly that path: a complete MBTiles belongs here,
|
|
8217
|
+
// and one still arriving does not -- a stack naming it would answer
|
|
8218
|
+
// 503 for every tile until the download finished.
|
|
8178
8219
|
const free = (held ?? []).filter(
|
|
8179
|
-
(entry) =>
|
|
8180
|
-
!taken.has(entry.infoHash) &&
|
|
8181
|
-
(entry.kind ?? 'pmtiles') === 'pmtiles',
|
|
8220
|
+
(entry) => !taken.has(entry.infoHash) && isServable(entry),
|
|
8182
8221
|
);
|
|
8183
8222
|
if (free.length) {
|
|
8184
8223
|
groups.push(
|