pmtiles-swarm 0.55.3 → 0.58.1

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 CHANGED
@@ -7,6 +7,122 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.58.1
11
+ ### 🐞 Bug fixes
12
+ - **The sample configuration put every piece of state under the config file.** `"dataDir": "./data"`,
13
+ `"savePath": "./data/torrents-data"` and `"resumeDir": "./data/resume"` all resolve against the
14
+ config file — and the service guide puts that file in `/etc`. Anyone following both documents ended
15
+ up with a catalog, a resume directory and their archives on the partition meant for configuration,
16
+ having done nothing wrong. The sample now uses absolute paths, a test enforces it, and the node
17
+ warns at startup if state resolves under `/etc` anyway.
18
+
19
+ It is also shorter. A first config should get a node running, not demonstrate the whole surface —
20
+ `docs/configuration.md` is where the rest lives.
21
+
22
+ - **Moving state out of `/etc` had a trap in the instructions.** `mv OLD/data NEW/data` nests when the
23
+ destination exists, which it does after the documented setup — so the real directory ends up one
24
+ level too deep, the node writes a fresh empty catalog beside it, and an intact library reads as
25
+ lost. `docs/running-as-a-service.md` now gives a form that cannot nest, says to repoint
26
+ `libtorrent.resumeDir` as well as `dataDir`, and has you count catalog entries before and after.
27
+
28
+ ## 0.58.0
29
+ ### ✨ Features and improvements
30
+ - **A stable name for every kind of import, not just watched folders.** `latestLink` and
31
+ `latestLinkType` are now offered on watched web locations, RSS feeds and remote nodes as well — one
32
+ path a consumer can hold while the build behind it changes.
33
+
34
+ A scheduled source had the feature all along and no way to ask for it: the console never showed the
35
+ column. It was also ignoring `latestLinkType`, so a source asking for a hard link quietly got a
36
+ symlink. A subscription could not ask at all.
37
+
38
+ For a subscription the name is pointed at the archive **when the download finishes**. Until then
39
+ there is a marker file, or a sparse one still filling in, and a name resolving to either is worse
40
+ than no name: whatever opens it reads zeroes rather than failing.
41
+
42
+ - **The node says something when its state has landed in `/etc`.** Nobody chooses that — the
43
+ documented service layout puts the config file there, every path resolves relative to that file,
44
+ and the sample reads `"dataDir": "./data"`. So the catalog and the resume directory end up on the
45
+ partition meant for configuration. Warned rather than corrected: it is a real path that works, and
46
+ moving a running node's data would be worse than saying so.
47
+
48
+ ### 🐞 Bug fixes
49
+
50
+ ## 0.57.0
51
+ ### ✨ Features and improvements
52
+ - **The three publishing switches are one **Local file** column on the import tables.** Three
53
+ dropdowns per row cost more width than the choice is worth, on tables that already scroll
54
+ sideways — and as an import default these are almost always decided together:
55
+
56
+ | option | serves the file | web seed | on the public page |
57
+ | --- | --- | --- | --- |
58
+ | `node` | — | — | — |
59
+ | `off` | no | — | — |
60
+ | `http` | yes | no | no |
61
+ | `http + catalog` | yes | no | yes |
62
+ | `http + web seed` | yes | yes | no |
63
+ | `http + web seed + catalog` | yes | yes | yes |
64
+
65
+ `http + catalog` is there rather than left out to keep a tidier ladder: a dropdown that could not
66
+ say it would round that state up to the nearest option it had, turning a web seed on — and
67
+ publishing the node to the swarm — because somebody re-saved an unrelated row. Every combination
68
+ survives being written and read back, which is what the round-trip test checks.
69
+
70
+ The archive details panel still offers the three separately. A row here sets a policy for what
71
+ arrives; the panel is where one archive gets picked over.
72
+
73
+ ## 0.56.2
74
+ ### 🐞 Bug fixes
75
+ - **Adding a save location in the console did nothing.** The row was read correctly and then thrown
76
+ away: the settings pane below renders every config key it does not explicitly skip as a raw-JSON
77
+ textarea, the skip list named `watch`, `sources` and `subscriptions` but not `locations`, and in
78
+ `saveSettings` the textarea loop runs after the row editors — so a copy of the list as it was when
79
+ the pane was drawn overwrote the one with the new row in it.
80
+
81
+ The skip is now derived from the registered row editors rather than listed by hand, so this cannot
82
+ happen again to the next editor somebody adds. The list was the bug; keeping a list and adding one
83
+ more name to it would have been the same bug waiting.
84
+
85
+ ## 0.56.1
86
+ ### ✨ Features and improvements
87
+ - **Comments trimmed back to house style, and the reasoning moved into the docs where it belongs.**
88
+ Recent work had been leaving 15–25 line explanatory blocks in the source; measured across
89
+ `catalog.js` that was 85 new comment lines against 76 new code lines. The argument for a decision
90
+ drifts out of date faster in a comment than in prose, and buries the code while it does it.
91
+
92
+ Two new sections in [docs/internals.md](docs/internals.md) give the displaced reasoning a home:
93
+ **Re-reading a summary an older prober wrote**, and **A validator for a URL that stays put** —
94
+ which is where the ETag story now lives, including why the infohash was the wrong choice and what
95
+ a PMTiles reader needs to see for `If-Range` and cross-origin `ETag` to work.
96
+
97
+ No behaviour change.
98
+
99
+ ## 0.56.0
100
+ ### ✨ Features and improvements
101
+ - **MapLibre Tiles are recognised.** PMTiles tile type `6` is MLT, and the tile-type table stopped at
102
+ `5` — so an MLT archive probed as `unknown` and was refused a tile endpoint it could have served,
103
+ even though the extension map had known about `.mlt` for a while.
104
+
105
+ No new TileJSON key was needed for it. MapLibre spells the tile encoding of a vector source
106
+ `encoding`, exactly as it does the elevation packing of a raster-dem one, and applies TileJSON
107
+ members to both after the source is constructed — so `encoding: "mlt"` reaches a vector source the
108
+ same way `encoding: "terrarium"` reaches a raster one. One key, two meanings, told apart by the
109
+ source type. The MLT value is read from the header rather than the metadata, because an archive
110
+ whose tile type says MapLibre Tiles is MLT-encoded and nothing needs to say so twice.
111
+
112
+ - **A resume-data shortfall is now reported.** The sidecar has been answering with how many torrents
113
+ were asked to write and how many managed it before the deadline, and both numbers were discarded —
114
+ by the engine wrapper, and again by the timer that called it. A torrent that does not write is one
115
+ that gets re-hashed on the next start, which for a 700 GiB archive is the difference between
116
+ seeding in seconds and seeding in half an hour. That is what "why is everything at 0%" looks like
117
+ from outside, and the silence here is part of why it was hard to see.
118
+
119
+ ### 🐞 Bug fixes
120
+ - **The two pages disagreed about how large an archive was.** The console rounded to whole units
121
+ above ten and the public page always kept a decimal, so the same archive read as `81 GiB` on one
122
+ and `80.6 GiB` on the other. Both now keep a decimal from KiB up — these are mostly archive sizes,
123
+ and half a gigabyte is worth seeing — and a test holds the two helpers character-for-character
124
+ identical, since nothing about a duplicated function announces when it stops being a copy.
125
+
10
126
  ## 0.55.3
11
127
  ### 🐞 Bug fixes
12
128
  - **A `/latest/` document could never be updated once a client had cached it.** The `ETag` was the
@@ -488,9 +488,19 @@ moment a download finishes.
488
488
 
489
489
  The node's setting is the default. A [watched folder](#watched-folders), a
490
490
  [scheduled source](#scheduled-sources), an [RSS feed](#subscriptions) and a
491
- remote node may each carry their own — the **Serve file**, **Web seed** and
492
- **Listed** columns on those tables, where `node` means "no opinion" rather than
493
- "off". And any individual archive can be switched in the console, under **HTTP
491
+ remote node may each carry their own — the **Local file** column on those
492
+ tables, which offers the three as one choice:
493
+
494
+ | option | `serveArchive` | `selfWebSeed` | `publicDownload` |
495
+ | --------------------------- | -------------- | ------------- | ---------------- |
496
+ | `node` | unset | unset | unset |
497
+ | `off` | `false` | — | — |
498
+ | `http` | `true` | — | — |
499
+ | `http + catalog` | `true` | — | `true` |
500
+ | `http + web seed` | `true` | `true` | — |
501
+ | `http + web seed + catalog` | `true` | `true` | `true` |
502
+
503
+ `node` means "no opinion" rather than "off". And any individual archive can be switched in the console, under **HTTP
494
504
  sources** in its details.
495
505
 
496
506
  An archive that says nothing goes on following the node, so changing the node's
@@ -586,6 +596,17 @@ it, and what turns a cold tile read from tens of seconds into well under one.
586
596
  `webSeedBase` on its own assumes the watched folder is already the web root, since
587
597
  nothing is moved.
588
598
 
599
+ **Every table that imports an archive offers this**, not just watched folders:
600
+ a [scheduled source](#scheduled-sources), an [RSS feed](#subscriptions) and a
601
+ remote node each take `latestLink` and `latestLinkType` too. The point is the
602
+ same in all four — a consumer holds one path and never learns the name of any
603
+ particular build.
604
+
605
+ For a subscription the name is pointed at the archive **when the download
606
+ finishes**, not when it is joined: until then there is a marker file or a sparse
607
+ one still filling in, and a name resolving to either is worse than no name at
608
+ all, because whatever opens it reads zeroes rather than failing.
609
+
589
610
  `latestLinkType: 'hard'` is for a name something reads the archive _through_. A
590
611
  hard link still resolves after the build it names is retired, where a symlink is
591
612
  left pointing at nothing. The other kind stays the fallback in both directions,
@@ -625,6 +646,7 @@ entry gives either a `url` template or an `index` directory:
625
646
  | `everyHours` | an interval instead |
626
647
  | `md5` | overrides the node's [`md5`](#md5) for this source alone |
627
648
  | `serveArchive`, `selfWebSeed`, `publicDownload` | override what this node offers of the archives this source fetches |
649
+ | `latestLink`, `latestLinkType` | a stable second name for the newest build, as on a watched folder |
628
650
 
629
651
  Prefer a template where the naming is predictable: it asks a direct question,
630
652
  gets a direct answer, and needs the upstream to publish no listing at all.
package/docs/internals.md CHANGED
@@ -18,6 +18,8 @@ Operator-facing documentation is elsewhere — see [publishing](publishing.md),
18
18
  - [Serving an MBTiles archive](#serving-an-mbtiles-archive)
19
19
  - [Answering for a tile that is not there](#answering-for-a-tile-that-is-not-there)
20
20
  - [Reading an archive that is still arriving](#reading-an-archive-that-is-still-arriving)
21
+ - [Re-reading a summary an older prober wrote](#re-reading-a-summary-an-older-prober-wrote)
22
+ - [A validator for a URL that stays put](#a-validator-for-a-url-that-stays-put)
21
23
  - [The health endpoint](#the-health-endpoint)
22
24
  - [The externally visible base URL](#the-externally-visible-base-url)
23
25
  - [Scheduled sources](#scheduled-sources)
@@ -310,6 +312,86 @@ nothing to read yet and the same archive at 100% will, so a permanent
310
312
  would put a swarm read behind each one. The limiter is in memory on purpose: a
311
313
  restart is a reasonable moment to try again.
312
314
 
315
+ ## Re-reading a summary an older prober wrote
316
+
317
+ An archive's summary — format, zoom range, bounds, vector layers, encoding — is
318
+ read once, written into the catalog, and never questioned again. That is right
319
+ as far as it goes: re-reading a header out of the swarm is not free, and for a
320
+ given infohash the answer cannot change.
321
+
322
+ It goes wrong the moment the prober learns to read something new. Every archive
323
+ probed before that keeps a summary with a hole in it, permanently, and the only
324
+ way out is to remove and re-add the archive by hand.
325
+
326
+ `encoding` was the case that made this plain. The key had been sitting in the
327
+ metadata of archives a node had been serving for months; teaching the prober to
328
+ read it changed nothing at all, because nothing ever asked again.
329
+
330
+ So `summarize()` stamps a `summaryVersion`, and a summary older than the current
331
+ prober is re-read once in the background — on a request for the archive's
332
+ TileJSON, rate-limited to once a minute per archive. Raise `SUMMARY_VERSION`
333
+ whenever a field is added and every archive in the catalog picks it up on its
334
+ own, with nothing to do on upgrade.
335
+
336
+ Two things this needs to get right, both learned the hard way:
337
+
338
+ - **Every route that serves a TileJSON has to trigger it**, not only
339
+ `/archives/<infohash>/tiles.json`. `/latest/<category>/tiles.json` is the URL
340
+ a style points at, so leaving it out missed exactly the archives that were
341
+ being consumed the documented way.
342
+ - **The write-back must not be conditioned on one field.** It began as a
343
+ vector-layer backfill and returned early unless layers turned up, which would
344
+ have discarded the encoding it had just gone to fetch.
345
+
346
+ ## A validator for a URL that stays put
347
+
348
+ Everything under `/archives/` is addressed by infohash: the URL changes when the
349
+ content does, so it can be cached for a year and marked `immutable`. A `/latest/`
350
+ URL is the opposite — stable on purpose, with the content moving underneath it —
351
+ so it needs a validator, and a short TTL alone is a guess.
352
+
353
+ The obvious validator is the infohash of the archive the category resolved to.
354
+ It is wrong, and it fails in a way that has no bottom: these documents carry more
355
+ than which build they name. A TileJSON also carries the archive's summary; a
356
+ magnet also carries its web seeds and trackers; the feed carries both. Enrich a
357
+ summary or add a web seed and the body changes while the infohash does not — so
358
+ every cache in the path revalidates, is told `304`, and goes on serving the old
359
+ document. Not stale for a minute: unable to be updated again for as long as the
360
+ archive exists.
361
+
362
+ So these are tagged over the body they are sending. Two nodes answering
363
+ identically still produce identical tags, which is the property the infohash was
364
+ chosen for; two nodes answering differently no longer claim otherwise.
365
+
366
+ `/latest/<category>/archive.pmtiles` and the `.torrent` redirect keep the
367
+ infohash, because for those the infohash really is the whole content.
368
+
369
+ ### Where the ranges have to be careful
370
+
371
+ A PMTiles reader does not fetch a file. It fetches a header, then a root
372
+ directory, then leaf directories, then tiles, over minutes or hours. If a
373
+ rebuild lands partway through, the offsets it read from the old build address
374
+ bytes in the new one — which does not fail loudly. It decodes as the wrong tile,
375
+ or as nothing, with no error anywhere naming the cause.
376
+
377
+ `If-Range` is therefore honoured on the category range endpoint: a range
378
+ conditioned on a build that is no longer current is refused _as a range_ and
379
+ answered in full, which a reader survives. `Last-Modified` is suppressed there so
380
+ a client cannot condition on a date instead — a build restored from a backup can
381
+ be newer while looking older.
382
+
383
+ The official reader closes the loop from its side, comparing the `ETag` of every
384
+ response against the one it saw first and re-reading the header when they differ.
385
+ That only works if it can see the tag: `ETag` is not exposed to cross-origin
386
+ JavaScript by default, so these routes send `Access-Control-Expose-Headers`.
387
+ Unexposed, the reader compares against `null`, the comparison never fires, and it
388
+ splices two builds in silence.
389
+
390
+ A weak validator is no better. The reader discards any tag beginning with `W/`,
391
+ which is exactly what Express derives from a file's size and mtime — and that
392
+ tag also differs per node, so two nodes behind a load balancer would hand a
393
+ reader two tags for byte-identical archives.
394
+
313
395
  ## The health endpoint
314
396
 
315
397
  For a load balancer, which needs three things: no credential, a cheap answer, and
@@ -256,8 +256,10 @@ directory:
256
256
  -> /etc/pmtiles-swarm/data
257
257
  ```
258
258
 
259
- That is rarely what you want for a service. Use absolute paths for anything
260
- holding data:
259
+ That is rarely what you want for a service, and the node says so on startup —
260
+ `/etc` is for configuration, and a catalog, a resume directory and possibly an
261
+ archive do not belong on that partition. The shipped sample uses absolute paths
262
+ for exactly this reason. Use them for anything holding data:
261
263
 
262
264
  ```json
263
265
  {
@@ -267,6 +269,33 @@ holding data:
267
269
  }
268
270
  ```
269
271
 
272
+ #### Moving state that already landed in the wrong place
273
+
274
+ Repoint **every** path that lived under the directory being moved, not just
275
+ `dataDir`: `libtorrent.resumeDir` is the one that is easy to miss, because it is
276
+ absolute and sits inside it.
277
+
278
+ And move with a destination that cannot nest. `mv /etc/pmtiles-swarm/data
279
+ /var/lib/pmtiles-swarm/data` puts the source _inside_ the destination when the
280
+ destination already exists — which it does, since the setup above creates it —
281
+ leaving `/var/lib/pmtiles-swarm/data/data`. The node then finds no catalog where
282
+ it was told to look, writes a fresh empty one, and every archive appears to have
283
+ been lost.
284
+
285
+ ```bash
286
+ sudo systemctl stop pmtiles-swarm
287
+ sudo python3 -c "import json;print(len(json.load(open('OLD/catalog.json'))['entries']))"
288
+
289
+ sudo mv /etc/pmtiles-swarm/data /var/lib/pmtiles-swarm/ # note: no second 'data'
290
+ # repoint dataDir *and* libtorrent.resumeDir, then
291
+
292
+ sudo systemctl start pmtiles-swarm
293
+ sudo python3 -c "import json;print(len(json.load(open('NEW/catalog.json'))['entries']))"
294
+ ```
295
+
296
+ The entry count before and after is the check that matters: an empty new catalog
297
+ is indistinguishable from a lost library until you count.
298
+
270
299
  ### 2. Every one of them in `ReadWritePaths`
271
300
 
272
301
  `ProtectSystem=strict` presents the whole filesystem as read-only inside the
package/docs/tilejson.md CHANGED
@@ -172,7 +172,22 @@ MapLibre applies every TileJSON member to the source after the source is
172
172
  constructed, so an `encoding` here **overrides** one written in the style. That
173
173
  is the intended direction: the archive is the thing that knows.
174
174
 
175
- Anything other than the three values the style specification defines is dropped
175
+ ### `mlt`
176
+
177
+ The same key carries one more value, for a different kind of source. MapLibre
178
+ spells the tile encoding of a vector source `encoding` too, and `mlt` there means
179
+ the tiles are [MapLibre Tiles](https://maplibre.org/maplibre-tile-spec/) rather
180
+ than MVT — so there was no new key to invent for it, only a value to allow
181
+ through.
182
+
183
+ This one comes from the header rather than the metadata. PMTiles has a tile type
184
+ for MLT (`6`), and an archive whose tile type says MapLibre Tiles _is_
185
+ MLT-encoded; nothing needs to be written in the metadata to make it so.
186
+ Elevation packing is the opposite case — the header knows the tile is WebP and
187
+ nothing about what its three channels mean — which is why the two are read from
188
+ different places despite sharing a key.
189
+
190
+ Anything other than the values the style specification defines is dropped
176
191
  rather than passed on. A client handed an encoding it does not recognise is
177
192
  worse off than one handed nothing, because nothing at least leaves it free to
178
193
  use its own default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.55.3",
3
+ "version": "0.58.1",
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",