pmtiles-swarm 0.13.1 → 0.14.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 CHANGED
@@ -7,6 +7,35 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.14.0
11
+ ### ✨ Features and improvements
12
+ - **The Categories page hands you the URL a style should actually use.** A new **For a style** row
13
+ gives the category's TileJSON URL with a magnet in its fragment, which is the string worth
14
+ copying — the plain TileJSON row is still there for anything that only speaks HTTP.
15
+
16
+ Where a publisher is announcing the category over the DHT, the fragment carries the **mutable**
17
+ magnet (`xs=urn:btpk:…&s=<category>`), which is the form a category needs: it names the category
18
+ rather than a build, so it does not go stale on the next one while the URL keeps following it.
19
+ Without a publisher it falls back to the newest build's own magnet, which pins that build but
20
+ still beats a blank map on the only occasion it is read at all.
21
+
22
+ Also on `GET /api/categories` as `endpoints.styleUrl`, and `null` for a category whose newest
23
+ archive is not PMTiles — the same rule the tile endpoints already follow.
24
+
25
+ The magnet was already available on an individual archive, but a category URL is what a style
26
+ points at, and that is the page where it was missing.
27
+
28
+ ### 🐞 Bug fixes
29
+ - **CI and the release workflow install with `--ignore-scripts`.** The 0.14.0 release failed in
30
+ `npm ci` because `node-datachannel` — WebTorrent's WebRTC binary, four levels down the tree —
31
+ found no prebuilt binary for the runner and crashed trying to build from source. The same
32
+ lockfile had published minutes earlier, so it was the download rather than an incompatibility.
33
+
34
+ Nothing in CI needs that binary: the suite passes without it, and `npm publish` ships source
35
+ rather than `node_modules`, so install scripts have no bearing on what is published. A
36
+ third-party binary download should not be able to block a release. Whether WebRTC works on a
37
+ given machine is still checked where it matters, after installing on the host.
38
+
10
39
  ## 0.13.1
11
40
  ### 🐞 Bug fixes
12
41
  - **A node with a secondary engine no longer takes a quarter of an hour to start listening.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.13.1",
3
+ "version": "0.14.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",
package/src/api.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  isPublicSurface,
15
15
  } from './auth.js';
16
16
  import { normalizeCategories } from './catalog.js';
17
+ import { mutableMagnet } from './mutable.js';
17
18
  import { guessKind } from './library.js';
18
19
  import { QBittorrentEngine } from './engines/qbittorrent.js';
19
20
  import { RESTART_REQUIRED, redactConfig, saveConfig } from './config.js';
@@ -60,6 +61,25 @@ function route(handler) {
60
61
  * @param {object} deps.config - Resolved configuration.
61
62
  * @returns {import('express').Express} - The configured app.
62
63
  */
64
+ /**
65
+ * A TileJSON URL carrying a magnet in its fragment.
66
+ * @param {string} category - Which category.
67
+ * @param {object} newest - Its newest entry.
68
+ * @param {string} base - Public base URL.
69
+ * @returns {string} - The URL a style should use.
70
+ */
71
+ function styleUrlFor(category, newest, base) {
72
+ const url = `${base}/latest/${category}/tiles.json`;
73
+ const magnet = newest?.mutable?.publicKey
74
+ ? mutableMagnet(newest.mutable.publicKey, {
75
+ salt: newest.mutable.salt ?? category,
76
+ name: newest.name,
77
+ webSeeds: newest.webSeeds,
78
+ })
79
+ : newest?.magnet;
80
+ return magnet ? `${url}#${magnet}` : url;
81
+ }
82
+
63
83
  export function createApp({
64
84
  library,
65
85
  catalog,
@@ -1703,6 +1723,18 @@ export function createApp({
1703
1723
  servable,
1704
1724
  endpoints: {
1705
1725
  tileJson: servable ? `${base}/latest/${category}/tiles.json` : null,
1726
+ // The same URL with a magnet in the fragment, which is what a
1727
+ // style should carry. A fragment is never sent in a request, so
1728
+ // an ordinary client fetches the TileJSON and ignores it, while
1729
+ // a swarm-aware one has the magnet before it makes any call --
1730
+ // and can therefore still start when this server cannot answer.
1731
+ //
1732
+ // The mutable magnet where there is one, because a category is
1733
+ // precisely where an infohash goes stale: it names the category
1734
+ // and resolves the current build over the DHT. Otherwise the
1735
+ // newest build's own magnet, which pins that build but still
1736
+ // beats a blank map when the fallback is needed at all.
1737
+ styleUrl: servable ? styleUrlFor(category, newest, base) : null,
1706
1738
  torrent: `${base}/latest/${category}/archive.torrent`,
1707
1739
  magnet: `${base}/latest/${category}/magnet`,
1708
1740
  feed: `${base}/feed/${category}.xml`,
@@ -2356,6 +2356,7 @@
2356
2356
  const newest = entry.newest;
2357
2357
  const rows = [
2358
2358
  ['TileJSON', entry.endpoints.tileJson],
2359
+ ['For a style', entry.endpoints.styleUrl],
2359
2360
  ['Torrent', entry.endpoints.torrent],
2360
2361
  ['Magnet', entry.endpoints.magnet],
2361
2362
  ['Feed', entry.endpoints.feed],
@@ -2384,7 +2385,20 @@
2384
2385
  <td style="width:8rem">${label}</td>
2385
2386
  <td>${
2386
2387
  url
2387
- ? `<code>${escapeHtml(url)}</code>`
2388
+ ? `<code>${escapeHtml(
2389
+ url.length > 96 ? `${url.slice(0, 96)}…` : url,
2390
+ )}</code>${
2391
+ label === 'For a style'
2392
+ ? `<div class="sub">the magnet rides in the fragment, which is
2393
+ never sent to the server — an ordinary client fetches the
2394
+ TileJSON and ignores it, a swarm-aware one reads it before
2395
+ making any call${
2396
+ url.includes('xs=urn:btpk')
2397
+ ? ' and follows the category rather than this build'
2398
+ : ''
2399
+ }</div>`
2400
+ : ''
2401
+ }`
2388
2402
  : '<span class="sub">not a PMTiles archive, so there is no tile endpoint</span>'
2389
2403
  }</td>
2390
2404
  <td style="width:11rem">${
@@ -2406,6 +2420,7 @@
2406
2420
  const [index, row] = button.dataset.copy.split('-').map(Number);
2407
2421
  const url = [
2408
2422
  categories[index].endpoints.tileJson,
2423
+ categories[index].endpoints.styleUrl,
2409
2424
  categories[index].endpoints.torrent,
2410
2425
  categories[index].endpoints.magnet,
2411
2426
  categories[index].endpoints.feed,