pmtiles-swarm 0.15.0 → 0.15.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 CHANGED
@@ -7,6 +7,31 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.15.2
11
+ ### 🐞 Bug fixes
12
+ - **A mutable magnet is named after its category rather than a build.** `dn=` was taken from
13
+ whichever archive was newest when the string was generated, so a magnet whose whole purpose is to
14
+ resolve to *the current* build carried the name of one particular build — dated the moment the
15
+ next one landed. It now reads `dn=openmaptiles`.
16
+
17
+ Nothing depended on it: `dn` is a display hint, replaced by the real name as soon as metadata
18
+ arrives. It was simply describing the wrong thing.
19
+
20
+ ## 0.15.1
21
+ ### 🐞 Bug fixes
22
+ - **The downloaded bar showed nothing on an archive that was plainly downloading.** A column on
23
+ that bar covers many pieces, and the sidecar reduced "held" by `all` — so a column lit only when
24
+ every piece beneath it had arrived, and an archive 18% complete showed an empty bar. Fixed in
25
+ pmtiles-torrent 0.4.4, which reports a proportion; the console now shades those columns by it,
26
+ with a floor so the first few percent of a download are visible rather than indistinguishable
27
+ from none.
28
+
29
+ Peer bars had the opposite fault and are fixed the same way, so a peer holding a little no longer
30
+ reads as a seed.
31
+
32
+ Renders correctly against either sidecar: the newer one never rounds a non-empty column below 2,
33
+ so values above 1 identify the new encoding.
34
+
10
35
  ## 0.15.0
11
36
  ### ✨ Features and improvements
12
37
  - **The DHT routing table is remembered between runs**, which is the difference between publishing
package/README.md CHANGED
@@ -115,8 +115,31 @@ subscribers forward, and they fail differently, so publishing both is cheap insu
115
115
 
116
116
  - **RSS** — easy to consume, understood by existing clients, needs a server that stays up.
117
117
  - **BEP 46** — an ed25519-signed DHT record naming the current infohash, addressed by public key
118
- rather than infohash (`magnet:?xs=urn:btpk:…`). No server needed, but the record expires and must
119
- be republished. See [src/mutable.js](src/mutable.js).
118
+ rather than infohash (`magnet:?xs=urn:btpk:…`). No server needed at all.
119
+
120
+ Publishing those records is built in. The node that builds gets a key — `pmtiles-swarm
121
+ publisher-key` — and announces the newest archive in each category, salted by category name so one
122
+ keypair addresses all of them. Records expire from the DHT after about two hours, so it republishes
123
+ on a timer; that timer is the feature rather than an optimisation. Serving nodes need nothing: they
124
+ receive the public half on the catalog entry and hand it out in the TileJSON, which is why a
125
+ serving tier can be compromised without anyone being able to publish.
126
+
127
+ ```json
128
+ { "mutable": { "publish": true, "keyPath": "/etc/pmtiles-swarm/publisher.pem" } }
129
+ ```
130
+
131
+ The routing table is remembered between runs (`dht-nodes.json` in the data directory), which
132
+ matters more than it sounds: bootstrapping from hostnames alone was measured working about one
133
+ start in seven on a domestic connection, and a saved table turns that into every start. It is the
134
+ same thing libtorrent does, and why its DHT works on hosts where a fresh socket does not.
135
+
136
+ **What a style should point at** is then the category's TileJSON URL with that magnet in its
137
+ fragment — the console's Categories page has a **For a style** row that gives you the whole
138
+ string. A fragment is never sent in an HTTP request, so ordinary clients fetch the TileJSON and
139
+ ignore it, while a swarm-aware one reads the magnet before making any call and can start when the
140
+ server cannot answer. With `xs=urn:btpk:` rather than an infohash it does not go stale on the next
141
+ build either. See [docs/serving-tiles.md](docs/serving-tiles.md) and
142
+ [docs/security.md](docs/security.md), because that key behaves unlike every other secret here.
120
143
 
121
144
  ## Configuration
122
145
 
@@ -742,9 +765,13 @@ another node, watched web locations including directory listings and date templa
742
765
  limits and what is left of them, in-place settings reload and process restart, and two-node
743
766
  subscription sync in both directions over both RSS and the catalog API.
744
767
 
745
- Not yet exercised: the qBittorrent engine against a real instance, watch-folder imports, and
746
- BEP 46 publish/resolve against a live DHT (the crypto and magnet handling are tested; interop
747
- with libtorrent's encoding is not).
768
+ Running in production: watch-folder imports feeding a nightly planet build, an 18-archive library
769
+ of roughly 2.5 TB behind HAProxy, and BEP 46 records published to a live DHT — 54 nodes stored the
770
+ last one.
771
+
772
+ Not yet exercised: the qBittorrent engine against a real instance, and **resolving** a BEP 46
773
+ record from outside the publishing network. Nodes accept the records and report storing them, which
774
+ is not the same claim as a stranger reading one back, and only the second matters to a subscriber.
748
775
 
749
776
  ## License and attribution
750
777
 
@@ -779,4 +779,12 @@ rebuild, which fail differently, so publishing both is cheap insurance:
779
779
  public key rather than infohash (`magnet:?xs=urn:btpk:…`). No server required, but the
780
780
  record expires and must be republished.
781
781
 
782
- See [subscribing.md](subscribing.md).
782
+ Publishing those records is built in: give the building node a key with `pmtiles-swarm
783
+ publisher-key`, set `mutable.publish`, and it announces the newest archive in each category
784
+ and republishes on a timer. Serving nodes need nothing — they receive the public half on
785
+ the catalog entry and hand it out in the TileJSON.
786
+
787
+ See [subscribing.md](subscribing.md) for the shape of it,
788
+ [serving-tiles.md](serving-tiles.md#a-fragment-that-survives-a-rebuild) for what a style
789
+ should then point at, and [security.md](security.md#the-publisher-key-is-not-a-credential)
790
+ for why that key wants treating like a signing key rather than an API token.
@@ -419,9 +419,29 @@ RSS and BEP 46 fail in opposite ways, which is the argument for publishing both:
419
419
  | RSS | a server that stays up | the server goes away |
420
420
  | BEP 46 | periodic republishing | the record expires (hours, not days) |
421
421
 
422
- **Status:** the crypto and magnet handling are tested — 32-byte keys, 64-byte signatures
423
- verifying, roundtrip stable. Publishing and resolving against a live DHT, and interop
424
- with libtorrent's exact value encoding, are **not yet verified**.
422
+ **This is built in.** The code above is what a host application would call; a node only
423
+ needs a key and a setting:
424
+
425
+ ```sh
426
+ pmtiles-swarm publisher-key > /etc/pmtiles-swarm/publisher.pem
427
+ chmod 400 /etc/pmtiles-swarm/publisher.pem
428
+ ```
429
+
430
+ ```json
431
+ { "mutable": { "publish": true, "keyPath": "/etc/pmtiles-swarm/publisher.pem" } }
432
+ ```
433
+
434
+ It then announces the newest archive in each category, salted by category name so one key
435
+ addresses all of them, and republishes before the records expire. Only the node that builds
436
+ needs the key — see
437
+ [running-as-a-service.md](running-as-a-service.md#the-publisher-key) for setting it up and
438
+ [security.md](security.md#the-publisher-key-is-not-a-credential) for why it is not a
439
+ credential.
440
+
441
+ **Status:** publishing is verified against a live DHT — records stored by 54 nodes at the
442
+ last check. **Resolving** one from outside the publishing network is not yet verified:
443
+ nodes accepting a record is not the same claim as a stranger reading it back, and only the
444
+ second matters to a subscriber.
425
445
 
426
446
  ## Making the archives serveable
427
447
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.15.0",
3
+ "version": "0.15.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",
package/src/api.js CHANGED
@@ -73,7 +73,8 @@ function styleUrlFor(category, newest, base) {
73
73
  const magnet = newest?.mutable?.publicKey
74
74
  ? mutableMagnet(newest.mutable.publicKey, {
75
75
  salt: newest.mutable.salt ?? category,
76
- name: newest.name,
76
+ // The category, since that is what this magnet resolves to.
77
+ name: newest.mutable.salt ?? category,
77
78
  webSeeds: newest.webSeeds,
78
79
  })
79
80
  : newest?.magnet;
package/src/publisher.js CHANGED
@@ -214,7 +214,12 @@ export class MutablePublisher {
214
214
  magnetFor(category, entry) {
215
215
  return mutableMagnet(this.#key.publicKey, {
216
216
  salt: category,
217
- name: entry?.name,
217
+ // The category rather than the build. This magnet resolves to whichever
218
+ // archive is current, so naming one of them dates the string the moment
219
+ // the next build lands. `dn` is only a label -- the real name arrives
220
+ // with the metadata and overrides it -- so nothing depends on this
221
+ // beyond being honest about what the magnet identifies.
222
+ name: category,
218
223
  webSeeds: entry?.webSeeds,
219
224
  });
220
225
  }
package/src/tilejson.js CHANGED
@@ -100,7 +100,9 @@ function buildTorrentBlock(entry, root) {
100
100
  // and buildable by any node, because it contains only the public half.
101
101
  magnet: mutableMagnet(entry.mutable.publicKey, {
102
102
  salt: entry.mutable.salt,
103
- name: entry.name,
103
+ // The category, not this build: the record resolves to whichever
104
+ // archive is current, and `dn` is a label the metadata replaces.
105
+ name: entry.mutable.salt,
104
106
  webSeeds: entry.webSeeds,
105
107
  }),
106
108
  };
@@ -2894,9 +2894,17 @@
2894
2894
  };
2895
2895
  const [r, g, b] = rgb(accent);
2896
2896
 
2897
- // Availability is a count, so it shades: the rarer the piece, the
2898
- // fainter the column. The other two are yes or no.
2899
- const ceiling = kind === 'availability' ? Math.max(1, ...bytesIn) : 1;
2897
+ // Availability is a count, so it shades by rarity. The other two are
2898
+ // proportions now: a column covers many pieces — 178,000 of them
2899
+ // across a bar a thousand wide on a 698 GiB archive — so "held" is a
2900
+ // fraction of its slice rather than a yes or no. Reduced any other way
2901
+ // the bar only tells the truth at 0% and 100%.
2902
+ //
2903
+ // A sidecar older than pmtiles-torrent 0.4.4 still sends 0 or 1, and
2904
+ // is recognised by nothing exceeding 1: the newer one never rounds a
2905
+ // non-empty bucket below 2, precisely so this test works.
2906
+ const ceiling = kind === 'availability' ? Math.max(1, ...bytesIn) : 255;
2907
+ const proportional = kind !== 'availability' && Math.max(0, ...bytesIn) > 1;
2900
2908
 
2901
2909
  for (let index = 0; index < bytesIn.length; index += 1) {
2902
2910
  const value = bytesIn[index];
@@ -2905,7 +2913,15 @@
2905
2913
  image.data[at + 3] = 0;
2906
2914
  continue;
2907
2915
  }
2908
- const strength = kind === 'availability' ? 0.35 + 0.65 * (value / ceiling) : 1;
2916
+ // A floor, because a column holding a little is worth seeing: without
2917
+ // one, the first few percent of a download are indistinguishable
2918
+ // from none at all.
2919
+ const strength =
2920
+ kind === 'availability'
2921
+ ? 0.35 + 0.65 * (value / ceiling)
2922
+ : proportional
2923
+ ? Math.max(0.18, value / 255)
2924
+ : 1;
2909
2925
  image.data[at] = r;
2910
2926
  image.data[at + 1] = g;
2911
2927
  image.data[at + 2] = b;