pmtiles-swarm 0.55.0 → 0.55.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.55.2
11
+ ### 🐞 Bug fixes
12
+ - **The stale-summary re-read never ran for a category URL.** 0.55.1 added it to
13
+ `/archives/<infohash>/tiles.json` and nowhere else, which missed exactly the archives that matter:
14
+ `/latest/<category>/tiles.json` is the URL a style points at, so a category consumed through the
15
+ documented path was the one place a summary stayed stale for ever. An archive only healed if
16
+ somebody happened to ask for it by infohash.
17
+
18
+ ## 0.55.1
19
+ ### 🐞 Bug fixes
20
+ - **0.55.0 reached new archives only.** `encoding` sits in the metadata of archives some nodes have
21
+ been serving for months, the prober learned to read it, and nothing changed — because a summary is
22
+ written into the catalog once and never questioned. Every path that re-read one was gated on the
23
+ summary being *absent*, and these summaries were present, merely old.
24
+
25
+ The summary now carries a `summaryVersion`, and one written by an older prober is re-read once, in
26
+ the background, on the next TileJSON request for that archive. Rate-limited to once a minute per
27
+ archive, as the vector-layer backfill beside it already was, and written back on any read that
28
+ produced something newer — the old early return threw away everything except vector layers, which
29
+ would have discarded the encoding it went to fetch.
30
+
31
+ This is general, not a fix for one field: raise `SUMMARY_VERSION` whenever the prober learns to
32
+ read something new, and every archive already in the catalog picks it up on its own. Nothing is
33
+ needed on an upgrade beyond asking for the archive's TileJSON, which anything using it does anyway.
34
+
10
35
  ## 0.55.0
11
36
  ### ✨ Features and improvements
12
37
  - **The TileJSON now carries a `raster-dem` archive's `encoding`**, read out of the archive's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.55.0",
3
+ "version": "0.55.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
@@ -33,6 +33,7 @@ import {
33
33
  } from './sources.js';
34
34
  import { limitFor, remaining } from './seeding.js';
35
35
  import { buildTileJson, extensionMatches } from './tilejson.js';
36
+ import { SUMMARY_VERSION } from './pmtiles-probe.js';
36
37
  import { TileReadError } from './tiles.js';
37
38
 
38
39
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -363,9 +364,18 @@ export function createApp({
363
364
  * @returns {boolean} - True to attempt a read now.
364
365
  */
365
366
  const metadataRetries = new Map();
366
- const needsVectorLayers = (summary, infoHash) => {
367
- if (!summary || summary.format !== 'pbf' || summary.vectorLayers)
368
- return false;
367
+ const needsReread = (summary, infoHash) => {
368
+ if (!summary) return false;
369
+ // Two reasons to go back to the archive. The first is the one this began
370
+ // as: a vector archive whose layers have not been read yet. The second is
371
+ // the general case it turned out to be — a summary written by an older
372
+ // prober, missing whatever that prober did not know to look for. Without
373
+ // it, adding a field to the summary reaches new archives only, and every
374
+ // archive already in the catalog keeps its hole until somebody removes and
375
+ // re-adds it by hand.
376
+ const stale = summary.summaryVersion !== SUMMARY_VERSION;
377
+ const missingLayers = summary.format === 'pbf' && !summary.vectorLayers;
378
+ if (!stale && !missingLayers) return false;
369
379
  const last = metadataRetries.get(infoHash) ?? 0;
370
380
  if (Date.now() - last < 60000) return false;
371
381
  metadataRetries.set(infoHash, Date.now());
@@ -388,13 +398,18 @@ export function createApp({
388
398
  // .torrent endpoints work without one. Enriching a summary is the least
389
399
  // important thing here and must never be what takes a reply down.
390
400
  if (typeof tiles?.summarize !== 'function') return;
391
- if (!needsVectorLayers(entry.pmtiles, entry.infoHash)) return;
401
+ if (!needsReread(entry.pmtiles, entry.infoHash)) return;
392
402
 
393
403
  const timeoutMs = config.tiles?.metadataTimeoutMs ?? 120000;
394
404
  tiles
395
405
  .summarize(entry.infoHash, { timeoutMs })
396
406
  .then(async (summary) => {
397
- if (!summary.vectorLayers) return;
407
+ // Written back whenever the read produced something newer than what
408
+ // was stored. Returning early unless vector layers turned up was right
409
+ // when layers were the only thing this looked for; it would now throw
410
+ // away the encoding it went to fetch.
411
+ const fresh = summary.summaryVersion !== entry.pmtiles?.summaryVersion;
412
+ if (!summary.vectorLayers && !fresh) return;
398
413
  await catalog.put({
399
414
  infoHash: entry.infoHash,
400
415
  pmtiles: { ...entry.pmtiles, ...summary },
@@ -2259,6 +2274,12 @@ export function createApp({
2259
2274
  });
2260
2275
  }
2261
2276
 
2277
+ // The same background re-read the per-archive route does. Left out
2278
+ // here, it missed exactly the archives that matter most: /latest/ is the
2279
+ // URL a style points at, so a category anyone actually consumes through
2280
+ // the documented path was the one place a stale summary never healed.
2281
+ startMetadataBackfill(entry);
2282
+
2262
2283
  res.setHeader('access-control-allow-origin', '*');
2263
2284
  // Short-lived, and the only mutable document in the system: everything
2264
2285
  // it points at is content-addressed and cached for a year, and this is
@@ -110,6 +110,24 @@ export function customEncodingFactors(metadata) {
110
110
  * @param {string} location - Local path or http(s) URL.
111
111
  * @returns {Promise<PMTilesSummary>} - The summary.
112
112
  */
113
+ /**
114
+ * What this prober reads, as a number that goes up when that changes.
115
+ *
116
+ * A summary is stored in the catalog and never read again, which is right --
117
+ * re-reading a header out of the swarm is not free, and the answer does not
118
+ * change for a given infohash. It goes wrong the moment the prober learns to
119
+ * read something new: every archive probed before that keeps a summary with a
120
+ * hole in it, for ever, and the only way out was to remove and re-add it.
121
+ *
122
+ * `encoding` was the case that made this obvious. It sat in the metadata of
123
+ * archives this node had been serving for months, and adding the code to read
124
+ * it changed nothing at all, because nothing ever asked again.
125
+ *
126
+ * So: stamp what the prober knew at the time, and re-read once when that is
127
+ * behind. Raise this whenever a field is added to the summary below.
128
+ */
129
+ export const SUMMARY_VERSION = 2;
130
+
113
131
  export function summarize(header, metadata = {}) {
114
132
  const type = TILE_TYPES[header.tileType] ?? TILE_TYPES[0];
115
133
 
@@ -123,6 +141,7 @@ export function summarize(header, metadata = {}) {
123
141
  );
124
142
 
125
143
  return {
144
+ summaryVersion: SUMMARY_VERSION,
126
145
  specVersion: header.specVersion,
127
146
  format: type.format,
128
147
  contentType: type.contentType,