pmtiles-swarm 0.52.0 → 0.53.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,25 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.53.0
11
+ ### ✨ Features and improvements
12
+ - **The three switches are now settable on every import, from the console.** **Serve file**, **Web
13
+ seed** and **Listed** columns on monitored folders, watched web locations, RSS feeds and remote
14
+ nodes, each offering `node` as well as yes and no — because unset is a real answer here, meaning
15
+ the archive follows the node rather than being switched off.
16
+
17
+ ### 🐞 Bug fixes
18
+ - **An RSS feed could not ask for any of them.** 0.50.0 wired these through watched folders and
19
+ scheduled sources and stopped there, and an archive adopted from a feed takes a different path
20
+ through the library — which is the path a mirror node actually uses. A subscription now carries all
21
+ three, and there was no way to set them from the console on any import at all.
22
+
23
+ - **`selfWebSeed` on an import waits for the download to finish.** Publishing a web seed URL for an
24
+ archive that is still arriving would advertise an address answering `409`, and a peer handed a URL
25
+ that refuses spends its retries on it — worse than no web seed, and unfixable afterwards because by
26
+ then the URL is in every copy of the `.torrent`. The intention is recorded when the archive joins
27
+ and acted on at the first moment this node holds the whole file.
28
+
10
29
  ## 0.52.0
11
30
  ### ✨ Features and improvements
12
31
  - **`publishingUrl`, for the URLs that have to be permanent.** Almost every URL this node emits is
@@ -433,12 +433,23 @@ web seed, because a client spends its retries on it, and a download link that
433
433
 
434
434
  ### Per archive, per folder, per source
435
435
 
436
- The same three-level rule as [`md5`](#md5). The node's setting is the default; a
437
- [watched folder](#watched-folders) or a [scheduled source](#scheduled-sources)
438
- may carry its own; and any individual archive can be switched in the console,
439
- under **HTTP sources** in its details. An archive that says nothing goes on
440
- following the node, so changing the node's answer reaches every archive that
441
- never had one of its own.
436
+ The node's setting is the default. A [watched folder](#watched-folders), a
437
+ [scheduled source](#scheduled-sources), an [RSS feed](#subscriptions) and a
438
+ remote node may each carry their own — the **Serve file**, **Web seed** and
439
+ **Listed** columns on those tables, where `node` means "no opinion" rather than
440
+ "off". And any individual archive can be switched in the console, under **HTTP
441
+ sources** in its details.
442
+
443
+ An archive that says nothing goes on following the node, so changing the node's
444
+ answer reaches every archive that never had one of its own.
445
+
446
+ Unlike `md5`, these do apply to a subscription. `md5` is a hashing pass that only
447
+ happens where a torrent is built, and a subscription adopts one somebody else
448
+ built; this is about what happens to the archive afterwards, which is this node's
449
+ business whoever made it. **`selfWebSeed` waits for the download to finish**: a
450
+ web seed URL for an archive still arriving answers `409`, and a peer handed a URL
451
+ that refuses spends its retries on it — worse than no web seed, and unfixable
452
+ afterwards, because by then the URL is in every copy of the `.torrent`.
442
453
 
443
454
  ## Trackers
444
455
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.52.0",
3
+ "version": "0.53.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/library.js CHANGED
@@ -526,6 +526,39 @@ export class Library {
526
526
  * @returns {Promise<object>} - The updated catalog entry.
527
527
  */
528
528
  async finalize(infoHash) {
529
+ const settled = await this.#finalizeOnce(infoHash);
530
+
531
+ // Only now, and never at import. A web seed URL for an archive that is
532
+ // still arriving answers 409, and a peer handed a URL that refuses spends
533
+ // its retries on it — worse than no web seed at all, and unfixable
534
+ // afterwards, because the URL is in the .torrent every peer holds. So a
535
+ // subscription records the intention when it joins and it is acted on
536
+ // here, at the first moment this node actually holds the whole file.
537
+ //
538
+ // Safe to reach on an archive that was already complete: setPublishing
539
+ // works from what is on record rather than from a transition, so this does
540
+ // nothing the second time — and does the right thing the first time for an
541
+ // archive that finished before the setting existed.
542
+ if (settled && publishingFor(settled, this.#config).selfWebSeed) {
543
+ try {
544
+ await this.setPublishing(infoHash, {});
545
+ return this.#catalog.get(infoHash) ?? settled;
546
+ } catch (error) {
547
+ console.warn(
548
+ `[web seed] ${settled.name} is not published as a web seed by this ` +
549
+ `node: ${error.message}`,
550
+ );
551
+ }
552
+ }
553
+ return settled;
554
+ }
555
+
556
+ /**
557
+ * The rename and the bookkeeping, without the publishing that follows it.
558
+ * @param {string} infoHash - The archive that finished.
559
+ * @returns {Promise<object>} - The updated catalog entry.
560
+ */
561
+ async #finalizeOnce(infoHash) {
529
562
  const entry = this.#catalog.get(infoHash);
530
563
  if (!entry) throw new Error('unknown archive');
531
564
  if (entry.complete) return entry;
@@ -1145,6 +1178,11 @@ export class Library {
1145
1178
  complete,
1146
1179
  // Held until the download finishes, which may be hours away.
1147
1180
  originMtime: options.originMtime,
1181
+ // Recorded now, applied when the download finishes. Unset leaves the
1182
+ // archive following the node, which is the rule everywhere else.
1183
+ serveArchive: options.serveArchive,
1184
+ selfWebSeed: options.selfWebSeed,
1185
+ publicDownload: options.publicDownload,
1148
1186
  // What the peer that offered this says it holds, where it said anything.
1149
1187
  // The head warmer replaces it with what the archive's own header says as
1150
1188
  // soon as it can read one; until then this is what makes the archive
@@ -423,6 +423,14 @@ export class SubscriptionManager {
423
423
  // 698 GiB download before starting one, which means it is populated
424
424
  // before a single byte exists here.
425
425
  summarySource: 'feed',
426
+ // What this node will offer of the archive once it holds it. Unset here
427
+ // means the node's own answer applies, the same rule as everywhere else.
428
+ // Nothing takes effect until the download finishes: a web seed URL for
429
+ // an archive that is still arriving answers 409, which is worse than no
430
+ // web seed because peers spend their retries on it.
431
+ serveArchive: subscription.serveArchive,
432
+ selfWebSeed: subscription.selfWebSeed,
433
+ publicDownload: subscription.publicDownload,
426
434
  };
427
435
 
428
436
  // The .torrent is preferred where there is one: it carries the trackers
@@ -4654,6 +4654,34 @@ Every piece is hashed against the ` +
4654
4654
  await renderTokenEditor(body);
4655
4655
  renderHookEditor(body, config, restartKeys);
4656
4656
 
4657
+ // The three switches the details panel offers, as import defaults.
4658
+ // Each has a blank option as well as yes and no, because unset is a
4659
+ // real answer here: it means the archive follows the node, and a row
4660
+ // that has never said anything must keep not saying it. See
4661
+ // publishingFor() -- the node's setting reaches everything that never
4662
+ // had one of its own, and that only works if "no opinion" is storable.
4663
+ const publishing = ['', 'node'];
4664
+ const publishingColumns = [
4665
+ {
4666
+ field: 'serveArchive',
4667
+ label: 'Serve file',
4668
+ boolean: true,
4669
+ options: [publishing, ['true', 'yes'], ['false', 'no']],
4670
+ },
4671
+ {
4672
+ field: 'selfWebSeed',
4673
+ label: 'Web seed',
4674
+ boolean: true,
4675
+ options: [publishing, ['true', 'yes'], ['false', 'no']],
4676
+ },
4677
+ {
4678
+ field: 'publicDownload',
4679
+ label: 'Listed',
4680
+ boolean: true,
4681
+ options: [publishing, ['true', 'yes'], ['false', 'no']],
4682
+ },
4683
+ ];
4684
+
4657
4685
  renderRowEditor({
4658
4686
  into: body,
4659
4687
  key: 'locations',
@@ -4747,6 +4775,7 @@ Every piece is hashed against the ` +
4747
4775
  placeholder: 'for ever',
4748
4776
  number: true,
4749
4777
  },
4778
+ ...publishingColumns,
4750
4779
  ],
4751
4780
  rows: config.watch ?? [],
4752
4781
  footnote:
@@ -4855,6 +4884,7 @@ Every piece is hashed against the ` +
4855
4884
  placeholder: 'for ever',
4856
4885
  number: true,
4857
4886
  },
4887
+ ...publishingColumns,
4858
4888
  ],
4859
4889
  rows: config.sources ?? [],
4860
4890
  checkNow: '/api/sources/check',
@@ -4979,6 +5009,7 @@ Every piece is hashed against the ` +
4979
5009
  ['false', 'no'],
4980
5010
  ],
4981
5011
  },
5012
+ ...publishingColumns,
4982
5013
  ],
4983
5014
  rows: following.filter((row) => !isPeer(row)),
4984
5015
  checkNow: '/api/subscriptions/refresh',
@@ -5053,6 +5084,7 @@ Every piece is hashed against the ` +
5053
5084
  ['false', 'no'],
5054
5085
  ],
5055
5086
  },
5087
+ ...publishingColumns,
5056
5088
  ],
5057
5089
  rows: following.filter(isPeer),
5058
5090
  checkNow: '/api/subscriptions/refresh',