pmtiles-swarm 0.14.0 → 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 +36 -0
- package/package.json +1 -1
- package/src/publisher.js +119 -15
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@
|
|
|
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
|
+
|
|
29
|
+
## 0.14.1
|
|
30
|
+
### 🐞 Bug fixes
|
|
31
|
+
- **The publisher waited a fixed fifteen seconds for the DHT and then published into an empty
|
|
32
|
+
routing table**, so on a real node every category failed at once:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
[mutable] openmaptiles failed: failed to publish mutable record: No nodes to query
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A fixed delay was a bet on how long bootstrapping takes. It now waits for the DHT's own `ready`
|
|
39
|
+
event instead, which fires in a couple of seconds on a healthy network, and only gives up after
|
|
40
|
+
a minute — saying so when it does, because a DHT that never bootstraps means outbound UDP is not
|
|
41
|
+
getting out and every later failure is a consequence of that.
|
|
42
|
+
- **A failed attempt no longer waits out the whole republish interval.** Nothing published meant
|
|
43
|
+
half an hour of advertising a public key that resolves to nothing before trying again. It now
|
|
44
|
+
retries after 30 seconds, backing off towards the interval.
|
|
45
|
+
|
|
10
46
|
## 0.14.0
|
|
11
47
|
### ✨ Features and improvements
|
|
12
48
|
- **The Categories page hands you the URL a style should actually use.** A new **For a style** row
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.14.
|
|
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
|
@@ -50,8 +50,11 @@ import { mutableMagnet, publishInfoHash } from './mutable.js';
|
|
|
50
50
|
/** Republish well inside the ~2h a DHT keeps an item. */
|
|
51
51
|
const DEFAULT_INTERVAL_MS = 30 * 60 * 1000;
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
const DEFAULT_READY_MS =
|
|
53
|
+
/** How long to wait for the DHT to bootstrap before publishing anyway. */
|
|
54
|
+
const DEFAULT_READY_MS = 60_000;
|
|
55
|
+
|
|
56
|
+
/** First retry delay after an attempt where nothing was published. */
|
|
57
|
+
const RETRY_MS = 30_000;
|
|
55
58
|
|
|
56
59
|
export class MutablePublisher {
|
|
57
60
|
#catalog;
|
|
@@ -59,6 +62,7 @@ export class MutablePublisher {
|
|
|
59
62
|
#key;
|
|
60
63
|
#intervalMs;
|
|
61
64
|
#timer = null;
|
|
65
|
+
#retryTimer = null;
|
|
62
66
|
#stopped = false;
|
|
63
67
|
#log;
|
|
64
68
|
/** Last published infohash per category, so an unchanged build stays quiet. */
|
|
@@ -134,10 +138,19 @@ export class MutablePublisher {
|
|
|
134
138
|
);
|
|
135
139
|
}
|
|
136
140
|
} catch (error) {
|
|
137
|
-
// One category failing must not stop the rest.
|
|
138
|
-
|
|
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
|
+
);
|
|
139
149
|
}
|
|
140
150
|
}
|
|
151
|
+
if (done.length === 0 && this.#current().size > 0) {
|
|
152
|
+
this.#scheduleRetry(options.retryDelayMs ?? RETRY_MS);
|
|
153
|
+
}
|
|
141
154
|
return done;
|
|
142
155
|
}
|
|
143
156
|
|
|
@@ -165,17 +178,14 @@ export class MutablePublisher {
|
|
|
165
178
|
* @returns {void}
|
|
166
179
|
*/
|
|
167
180
|
start(options = {}) {
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
}, options.readyMs ?? DEFAULT_READY_MS);
|
|
178
|
-
first.unref?.();
|
|
181
|
+
// Waited for rather than guessed at. A put into a DHT whose routing table
|
|
182
|
+
// is still empty fails with "No nodes to query", and a fixed delay is a
|
|
183
|
+
// bet on how long bootstrapping takes -- one this lost in the field, where
|
|
184
|
+
// fifteen seconds was not enough and every category failed on the first
|
|
185
|
+
// attempt.
|
|
186
|
+
this.#firstPublish(options.readyMs ?? DEFAULT_READY_MS).catch((error) =>
|
|
187
|
+
this.#log(`[mutable] first publish failed: ${error.message}`),
|
|
188
|
+
);
|
|
179
189
|
|
|
180
190
|
this.#timer = setInterval(() => {
|
|
181
191
|
this.publishAll().catch((error) =>
|
|
@@ -190,6 +200,98 @@ export class MutablePublisher {
|
|
|
190
200
|
);
|
|
191
201
|
}
|
|
192
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Waits for the DHT to bootstrap, then publishes.
|
|
205
|
+
* @param {number} readyMs - How long to wait before going ahead regardless.
|
|
206
|
+
* @returns {Promise<void>} - Resolves once the first attempt is done.
|
|
207
|
+
*/
|
|
208
|
+
async #firstPublish(readyMs) {
|
|
209
|
+
const nodes = await this.#whenDhtReady(readyMs);
|
|
210
|
+
if (this.#stopped) return;
|
|
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.
|
|
216
|
+
this.#log(
|
|
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',
|
|
220
|
+
);
|
|
221
|
+
} else if (nodes) {
|
|
222
|
+
this.#log(`[mutable] DHT ready with ${nodes} nodes`);
|
|
223
|
+
}
|
|
224
|
+
await this.publishAll({ force: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
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.
|
|
234
|
+
* @param {number} timeoutMs - How long to wait.
|
|
235
|
+
* @returns {Promise<number | null>} - Nodes found, or null if unknowable.
|
|
236
|
+
*/
|
|
237
|
+
async #whenDhtReady(timeoutMs) {
|
|
238
|
+
const deadline = Date.now() + timeoutMs;
|
|
239
|
+
|
|
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
|
+
});
|
|
248
|
+
});
|
|
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;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Tries again sooner than the republish interval.
|
|
275
|
+
*
|
|
276
|
+
* An attempt where nothing published usually means the DHT is not ready yet,
|
|
277
|
+
* and waiting out a thirty-minute interval to discover that again is half an
|
|
278
|
+
* hour of a node advertising a key that resolves to nothing.
|
|
279
|
+
* @param {number} delayMs - How long to wait first.
|
|
280
|
+
* @returns {void}
|
|
281
|
+
*/
|
|
282
|
+
#scheduleRetry(delayMs) {
|
|
283
|
+
if (this.#stopped || this.#retryTimer) return;
|
|
284
|
+
const timer = setTimeout(() => {
|
|
285
|
+
this.#retryTimer = null;
|
|
286
|
+
if (this.#stopped) return;
|
|
287
|
+
this.publishAll({ retryDelayMs: Math.min(delayMs * 2, this.#intervalMs) }).catch(
|
|
288
|
+
(error) => this.#log(`[mutable] retry failed: ${error.message}`),
|
|
289
|
+
);
|
|
290
|
+
}, delayMs);
|
|
291
|
+
timer.unref?.();
|
|
292
|
+
this.#retryTimer = timer;
|
|
293
|
+
}
|
|
294
|
+
|
|
193
295
|
/**
|
|
194
296
|
* Stops republishing.
|
|
195
297
|
* @returns {void}
|
|
@@ -197,6 +299,8 @@ export class MutablePublisher {
|
|
|
197
299
|
stop() {
|
|
198
300
|
this.#stopped = true;
|
|
199
301
|
if (this.#timer) clearInterval(this.#timer);
|
|
302
|
+
if (this.#retryTimer) clearTimeout(this.#retryTimer);
|
|
200
303
|
this.#timer = null;
|
|
304
|
+
this.#retryTimer = null;
|
|
201
305
|
}
|
|
202
306
|
}
|