pmtiles-swarm 0.14.1 → 0.14.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,25 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.14.2
11
+ ### 🐞 Bug fixes
12
+ - **Waiting for the DHT's `ready` event was not enough.** It fires when the bootstrap lookup
13
+ *finishes*, whether or not that lookup found anything — so a node with no working UDP path
14
+ reports itself ready and then fails every put with "No nodes to query", and the warning added in
15
+ 0.14.1 never fired because nothing had gone wrong by its measure.
16
+
17
+ It now waits for the routing table to have something in it, and says how empty it is when it
18
+ gives up:
19
+
20
+ ```
21
+ [mutable] DHT ready with 42 nodes
22
+ [mutable] the DHT found no peers in 60s. Publishing will fail until it does — check that
23
+ outbound UDP is not blocked, and that the bootstrap hosts resolve
24
+ ```
25
+
26
+ Per-category failures carry the count too, because "No nodes to query" with an empty table is a
27
+ network problem and the same message with a populated one is not.
28
+
10
29
  ## 0.14.1
11
30
  ### 🐞 Bug fixes
12
31
  - **The publisher waited a fixed fifteen seconds for the DHT and then published into an empty
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.14.1",
3
+ "version": "0.14.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/publisher.js CHANGED
@@ -138,8 +138,14 @@ export class MutablePublisher {
138
138
  );
139
139
  }
140
140
  } catch (error) {
141
- // One category failing must not stop the rest.
142
- this.#log(`[mutable] ${category} failed: ${error.message}`);
141
+ // One category failing must not stop the rest. The node count goes in
142
+ // the line because "No nodes to query" with an empty table is a
143
+ // network problem and the same message with a full one is not.
144
+ const nodes = this.#nodeCount();
145
+ this.#log(
146
+ `[mutable] ${category} failed: ${error.message}` +
147
+ (nodes === null ? '' : ` (${nodes} DHT nodes known)`),
148
+ );
143
149
  }
144
150
  }
145
151
  if (done.length === 0 && this.#current().size > 0) {
@@ -200,37 +206,68 @@ export class MutablePublisher {
200
206
  * @returns {Promise<void>} - Resolves once the first attempt is done.
201
207
  */
202
208
  async #firstPublish(readyMs) {
203
- const bootstrapped = await this.#whenDhtReady(readyMs);
209
+ const nodes = await this.#whenDhtReady(readyMs);
204
210
  if (this.#stopped) return;
205
- if (!bootstrapped) {
206
- // Said rather than swallowed: a DHT that never bootstraps means UDP is
207
- // not getting out, and every failure after this is a consequence of it.
211
+ if (nodes === 0) {
212
+ // Named rather than left to be inferred. A routing table that is still
213
+ // empty after a minute means the bootstrap queries are not being
214
+ // answered, and every "No nodes to query" after this is that same fact
215
+ // reported once per category.
208
216
  this.#log(
209
- `[mutable] the DHT has not found any peers after ${Math.round(readyMs / 1000)}s ` +
210
- '— publishing anyway, but check that outbound UDP is not blocked',
217
+ `[mutable] the DHT found no peers in ${Math.round(readyMs / 1000)}s. Publishing ` +
218
+ 'will fail until it does — check that outbound UDP is not blocked, and that ' +
219
+ 'the bootstrap hosts resolve',
211
220
  );
221
+ } else if (nodes) {
222
+ this.#log(`[mutable] DHT ready with ${nodes} nodes`);
212
223
  }
213
224
  await this.publishAll({ force: true });
214
225
  }
215
226
 
216
227
  /**
217
- * Resolves once the DHT reports itself ready, or the wait runs out.
228
+ * Waits for the DHT to have somewhere to send a query.
229
+ *
230
+ * `ready` alone is not enough: it fires when the bootstrap lookup finishes,
231
+ * whether or not that lookup found anything, so a node with no UDP path
232
+ * reports itself ready and then fails every put with "No nodes to query".
233
+ * What matters is the size of the routing table.
218
234
  * @param {number} timeoutMs - How long to wait.
219
- * @returns {Promise<boolean>} - Whether it became ready.
235
+ * @returns {Promise<number | null>} - Nodes found, or null if unknowable.
220
236
  */
221
- #whenDhtReady(timeoutMs) {
222
- if (this.#dht?.ready) return Promise.resolve(true);
223
- // A stand-in without an event emitter is ready by definition.
224
- if (typeof this.#dht?.once !== 'function') return Promise.resolve(true);
237
+ async #whenDhtReady(timeoutMs) {
238
+ const deadline = Date.now() + timeoutMs;
225
239
 
226
- return new Promise((resolve) => {
227
- const timer = setTimeout(() => resolve(false), timeoutMs);
228
- timer.unref?.();
229
- this.#dht.once('ready', () => {
230
- clearTimeout(timer);
231
- resolve(true);
240
+ if (typeof this.#dht?.once === 'function' && !this.#dht.ready) {
241
+ await new Promise((resolve) => {
242
+ const timer = setTimeout(resolve, timeoutMs);
243
+ timer.unref?.();
244
+ this.#dht.once('ready', () => {
245
+ clearTimeout(timer);
246
+ resolve();
247
+ });
232
248
  });
233
- });
249
+ }
250
+
251
+ let count = this.#nodeCount();
252
+ while (count === 0 && Date.now() < deadline && !this.#stopped) {
253
+ await new Promise((resolve) => {
254
+ const timer = setTimeout(resolve, 1000);
255
+ timer.unref?.();
256
+ });
257
+ count = this.#nodeCount();
258
+ }
259
+ return count;
260
+ }
261
+
262
+ /**
263
+ * How many nodes the DHT currently knows, where it can say.
264
+ * @returns {number | null} - The count, or null for a stand-in that has none.
265
+ */
266
+ #nodeCount() {
267
+ const count = this.#dht?.nodes?.count?.();
268
+ if (typeof count === 'number') return count;
269
+ const listed = this.#dht?.toJSON?.().nodes?.length;
270
+ return typeof listed === 'number' ? listed : null;
234
271
  }
235
272
 
236
273
  /**