pmtiles-swarm 0.5.4 → 0.5.5

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,19 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.5.5
11
+ ### 🐞 Bug fixes
12
+ - **An archive could retire itself from head-warming and never say so.** The test for "already
13
+ read" was `summary.format !== 'pbf'`, and any stored summary *without* a format satisfies
14
+ that — so it counted as read, not vector, and therefore finished. A read that raced its
15
+ deadline leaves exactly such a summary behind, and from then on the archive was permanently
16
+ ineligible: no attempts, no log lines, and nothing to explain the silence. A summary now only
17
+ counts as an answer if it actually names a format.
18
+ - **A read that never settles no longer disables warming for good.** The flag that stops two
19
+ reads running at once was held for the life of the process by a read that never returned, and
20
+ every later pass then returned at its first line — silently, for every archive. It is
21
+ abandoned after three times the metadata timeout, with a line saying so.
22
+
10
23
  ## 0.5.4
11
24
  ### ✨ Features and improvements
12
25
  - **The head-warm waits sensibly at both ends.** It used to read at the instant the node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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/prewarm.js CHANGED
@@ -58,6 +58,7 @@ export class HeadWarmer {
58
58
  #attempts = new Map();
59
59
  #waiting = new Set();
60
60
  #running = false;
61
+ #runningSince = 0;
61
62
 
62
63
  /**
63
64
  * @param {object} tiles - The tile store, for `summarize`.
@@ -94,8 +95,13 @@ export class HeadWarmer {
94
95
  // archive this can say anything about.
95
96
  if (entry.kind && entry.kind !== 'pmtiles') return false;
96
97
 
98
+ // A summary that names a format is one a header was actually read for.
99
+ // Anything else — an empty object, or one left behind by a read that raced
100
+ // its deadline — is not an answer, and treating it as one retired the
101
+ // archive permanently: no logs, no retries, nothing to explain the silence.
97
102
  const summary = entry.pmtiles;
98
- if (summary && (summary.format !== 'pbf' || summary.vectorLayers)) {
103
+ const read = Boolean(summary?.format);
104
+ if (read && (summary.format !== 'pbf' || summary.vectorLayers)) {
99
105
  return false;
100
106
  }
101
107
 
@@ -137,12 +143,27 @@ export class HeadWarmer {
137
143
  * @returns {Promise<object|null>} - The entry warmed, or null.
138
144
  */
139
145
  async sweep() {
140
- if (!this.enabled || this.#running) return null;
146
+ if (!this.enabled) return null;
147
+
148
+ if (this.#running) {
149
+ // A read that never settles would otherwise hold this flag for the life
150
+ // of the process and stop every archive from ever being warmed again —
151
+ // silently, since nothing logs a pass that returns at the first line.
152
+ const stuck = (this.#config.tiles?.metadataTimeoutMs ?? 120000) * 3;
153
+ if (this.#now() - this.#runningSince < stuck) return null;
154
+ console.warn(
155
+ `[warm] a read has been running for ${Math.round(
156
+ (this.#now() - this.#runningSince) / 1000,
157
+ )}s and is being abandoned`,
158
+ );
159
+ this.#running = false;
160
+ }
141
161
 
142
162
  const entry = this.#catalog.list().find((candidate) => this.due(candidate));
143
163
  if (!entry) return null;
144
164
 
145
165
  this.#running = true;
166
+ this.#runningSince = this.#now();
146
167
  try {
147
168
  // The long timeout, not the interactive one: this is a byte range from
148
169
  // an archive nobody has asked for a piece of yet.