pmtiles-swarm 0.55.2 → 0.55.3

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,24 @@
7
7
  ### šŸž Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.55.3
11
+ ### šŸž Bug fixes
12
+ - **A `/latest/` document could never be updated once a client had cached it.** The `ETag` was the
13
+ infohash, which says which _build_ a category resolved to — and these documents carry more than
14
+ that. A TileJSON also carries the archive's summary; a magnet also carries its web seeds and
15
+ trackers; the feed carries both. So enriching a summary (0.55.1 reading an `encoding` an older
16
+ prober had missed) or adding a web seed changed the body while the infohash stayed put: every cache
17
+ in the path revalidated, was told `304`, and went on serving the old document indefinitely. That is
18
+ not a cache being stale for a minute; it is a document that can never change again.
19
+
20
+ These are now tagged over what is actually sent. `/latest/<category>/archive.pmtiles` and the
21
+ `.torrent` redirect keep the infohash, because for those the infohash really is the whole content.
22
+
23
+ - **A successful metadata re-read was logged as a failure.** The line counting vector layers ran for
24
+ raster archives too, where there are none, and the `TypeError` went to the catch beside it — which
25
+ reported a backfill that had already written its result as "no vector layers yet". A misleading log
26
+ is worse than none when it is what somebody is reading to find out whether the thing works.
27
+
10
28
  ## 0.55.2
11
29
  ### šŸž Bug fixes
12
30
  - **The stale-summary re-read never ran for a category URL.** 0.55.1 added it to
@@ -157,14 +157,23 @@ answers 404 for them exactly as the feeds do.
157
157
 
158
158
  ### How a client knows the build moved
159
159
 
160
- Every one of these carries an `ETag`, and the tag is the infohash of the archive
161
- it resolved to:
160
+ Every one of these carries an `ETag` over the document it is sending:
162
161
 
163
162
  ```
164
- ETag: "913d671f3a28c5b8d605e28cf6bf01e293d36e86"
163
+ ETag: "6b8f1c2d…"
165
164
  Cache-Control: public, max-age=60, must-revalidate
166
165
  ```
167
166
 
167
+ **Over the document, not over the infohash.** The infohash is the obvious
168
+ choice and it is wrong here: it says which _build_ a category resolved to, and
169
+ these documents carry more than that. A TileJSON also carries the archive's
170
+ summary; a magnet also carries its web seeds and trackers. Enrich a summary or
171
+ add a web seed and the body changes while the infohash does not — so every cache
172
+ in the path revalidates, is told `304`, and goes on serving the old document.
173
+ Not stale for a minute: unable to be updated at all. Only
174
+ `/latest/{category}/archive.pmtiles` and the `.torrent` redirect are tagged by
175
+ infohash, because for those the infohash really is the whole content.
176
+
168
177
  A short TTL on its own is a guess. At five minutes, every client and every proxy
169
178
  in front of one serves the previous build for up to five minutes after a rollover
170
179
  and not one of them can tell it is doing so. The infohash is the honest answer:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.55.2",
3
+ "version": "0.55.3",
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
@@ -414,9 +414,16 @@ export function createApp({
414
414
  infoHash: entry.infoHash,
415
415
  pmtiles: { ...entry.pmtiles, ...summary },
416
416
  });
417
+ // Guarded, because this now runs for raster archives too, where there
418
+ // are no layers to count -- and the TypeError went to the catch below,
419
+ // which reported a backfill that had already succeeded as a failure.
420
+ // A misleading log is worse than none when it is the thing somebody is
421
+ // reading to find out whether the backfill works.
417
422
  console.log(
418
- `[tiles] read ${summary.vectorLayers.length} vector layers for ` +
419
- `${entry.name} out of the swarm`,
423
+ summary.vectorLayers
424
+ ? `[tiles] read ${summary.vectorLayers.length} vector layers for ` +
425
+ `${entry.name} out of the swarm`
426
+ : `[tiles] re-read the metadata for ${entry.name}`,
420
427
  );
421
428
  })
422
429
  .catch((error) => {
@@ -2123,10 +2130,28 @@ export function createApp({
2123
2130
  * @param {import('express').Response} res - The response to tag.
2124
2131
  * @param {object} entry - The archive this URL resolved to.
2125
2132
  */
2126
- const tagAsLatest = (res, entry) => {
2133
+ const tagAsLatest = (res, content) => {
2134
+ // Hashed from what is actually being sent, not from the infohash it
2135
+ // resolved to. The infohash was the obvious choice and it was wrong: it
2136
+ // says which build this is, and these documents carry more than that. A
2137
+ // TileJSON also carries the archive's summary, a magnet also carries the
2138
+ // web seeds and trackers, a feed carries both. Enrich a summary or add a
2139
+ // web seed and the body changes while the infohash does not -- so every
2140
+ // cache in the path revalidates, is told 304, and serves the old body for
2141
+ // ever. That is not a cache being stale for a minute; it is a document
2142
+ // that can never be updated again.
2143
+ //
2144
+ // A hash of the body has the property the infohash was picked for anyway:
2145
+ // two nodes answering identically produce identical tags, and two nodes
2146
+ // answering differently should not claim otherwise.
2147
+ const text =
2148
+ typeof content === 'string' ? content : JSON.stringify(content);
2127
2149
  // Quoted, because that is what an entity-tag is. Unquoted, a validator
2128
2150
  // never matches an If-None-Match and every revalidation misses silently.
2129
- res.setHeader('etag', `"${entry.infoHash}"`);
2151
+ res.setHeader(
2152
+ 'etag',
2153
+ `"${crypto.createHash('sha1').update(text).digest('hex')}"`,
2154
+ );
2130
2155
  // must-revalidate because serving this one stale is not merely "slightly
2131
2156
  // old": a reader holding part of one build and asking for the rest after
2132
2157
  // a rollover assembles a file that never existed. A minute of cache with
@@ -2287,8 +2312,7 @@ export function createApp({
2287
2312
  // makes that re-read cheap — a client already holding the current build
2288
2313
  // gets a 304 and no body, which Express does on its own once an ETag is
2289
2314
  // set before the response goes out.
2290
- tagAsLatest(res, entry);
2291
- res.json({
2315
+ const doc = {
2292
2316
  ...buildTileJson(entry, baseUrl(req)),
2293
2317
  // Names what it resolved to, so a consumer can tell one build from the
2294
2318
  // next without diffing the tile URLs.
@@ -2298,7 +2322,9 @@ export function createApp({
2298
2322
  name: entry.name,
2299
2323
  createdAt: entry.createdAt,
2300
2324
  },
2301
- });
2325
+ };
2326
+ tagAsLatest(res, doc);
2327
+ res.json(doc);
2302
2328
  }),
2303
2329
  );
2304
2330
 
@@ -2319,18 +2345,17 @@ export function createApp({
2319
2345
  // this one says so, and names what it resolved to while it is at it. The
2320
2346
  // tag is the infohash being redirected to, which is precisely the thing
2321
2347
  // that changes when this redirect starts pointing somewhere else.
2322
- tagAsLatest(res, entry);
2323
- res.redirect(
2324
- 302,
2325
- `${baseUrl(req)}/archives/${entry.infoHash}/archive.torrent`,
2326
- );
2348
+ const target = `${baseUrl(req)}/archives/${entry.infoHash}/archive.torrent`;
2349
+ tagAsLatest(res, target);
2350
+ res.redirect(302, target);
2327
2351
  });
2328
2352
 
2329
2353
  app.get('/latest/:category/magnet', (req, res) => {
2330
2354
  const entry = newestIn(req.params.category, req);
2331
2355
  if (!entry) return res.status(404).json({ error: 'no such category' });
2332
- tagAsLatest(res, entry);
2333
- res.type('text/plain').send(entry.magnet ?? '');
2356
+ const magnet = entry.magnet ?? '';
2357
+ tagAsLatest(res, magnet);
2358
+ res.type('text/plain').send(magnet);
2334
2359
  });
2335
2360
 
2336
2361
  /**
@@ -2566,16 +2591,15 @@ export function createApp({
2566
2591
  // validator: a tag meaning "still empty" is indistinguishable from one
2567
2592
  // meaning "still this build", and a reader would cache the emptiness past
2568
2593
  // the arrival of the thing it was waiting for.
2569
- if (entry) tagAsLatest(res, entry);
2570
- res.type('application/rss+xml').send(
2571
- renderFeed(entry ? [entry] : [], {
2572
- title: `${config.feedTitle ?? 'PMTiles archives'} — ${category}, latest`,
2573
- baseUrl: baseUrl(req),
2574
- copyright: config.feedCopyright,
2575
- category,
2576
- maxItems: 1,
2577
- }),
2578
- );
2594
+ const body = renderFeed(entry ? [entry] : [], {
2595
+ title: `${config.feedTitle ?? 'PMTiles archives'} — ${category}, latest`,
2596
+ baseUrl: baseUrl(req),
2597
+ copyright: config.feedCopyright,
2598
+ category,
2599
+ maxItems: 1,
2600
+ });
2601
+ if (entry) tagAsLatest(res, body);
2602
+ res.type('application/rss+xml').send(body);
2579
2603
  });
2580
2604
 
2581
2605
  app.get('/feed.xml', (req, res) => {