pmtiles-swarm 0.5.0 → 0.5.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 +30 -0
- package/package.json +2 -2
- package/src/engines/libtorrent.js +23 -7
- package/src/prewarm.js +42 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.5.2
|
|
11
|
+
### 🐞 Bug fixes
|
|
12
|
+
- **Asks for the `pmtiles-torrent` that 0.5.1 actually needs.** It shipped declaring `^0.3.2`,
|
|
13
|
+
and a caret on a `0.x` version allows patch updates only — so that range can never install
|
|
14
|
+
0.4.0, which is where the op behind writing a `.torrent` for a magnet-joined archive lives.
|
|
15
|
+
As published, 0.5.1 could not get the sidecar its own new code depends on. It fails safely
|
|
16
|
+
either way, since an older sidecar answers "unknown op" and the call is caught, but it fails
|
|
17
|
+
silently: the feed keeps serving enclosure URLs that 404 and nothing says why.
|
|
18
|
+
|
|
19
|
+
## 0.5.1
|
|
20
|
+
### ✨ Features and improvements
|
|
21
|
+
- **An archive joined by magnet writes its own `.torrent` on a libtorrent node.** The machinery
|
|
22
|
+
was already there — `captureMetadata` writes the metainfo down, and completion sweeps retry it
|
|
23
|
+
for anything still missing one — but it asks the engine for the metainfo, and the libtorrent
|
|
24
|
+
engine had no way to give it. Only WebTorrent did, and WebTorrent only ever receives archives
|
|
25
|
+
that are already complete. So on the engine most people run, a magnet-joined archive could
|
|
26
|
+
never produce a `.torrent`, however long it ran.
|
|
27
|
+
|
|
28
|
+
That is one gap with a long tail: a node with no `.torrent` to publish serves a feed whose
|
|
29
|
+
enclosure URLs 404, so its subscribers join by magnet too, and a magnet carrying no trackers
|
|
30
|
+
has only the DHT to find its first peer with. Needs `pmtiles-torrent` 0.4.0, which added the
|
|
31
|
+
op; against an older sidecar the call is refused and caught, exactly as before.
|
|
32
|
+
|
|
33
|
+
### 🐞 Bug fixes
|
|
34
|
+
- **A head-warm that was simply too early no longer waits out the full backoff.** An archive
|
|
35
|
+
joined by magnet has no metainfo until BEP 9 finishes, so a read asked for in the same second
|
|
36
|
+
the node started is refused before it reaches the swarm at all. That is a wait, not an
|
|
37
|
+
attempt: it is now retried on the next pass rather than in two minutes, and reported once
|
|
38
|
+
rather than every time.
|
|
39
|
+
|
|
10
40
|
## 0.5.0
|
|
11
41
|
### ✨ Features and improvements
|
|
12
42
|
- **The head of a newly joined archive is read without waiting to be asked.** A PMTiles archive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"maplibre-gl": "^6.2.0",
|
|
40
40
|
"parse-torrent": "^11.0.24",
|
|
41
41
|
"pmtiles": "^4.4.1",
|
|
42
|
-
"pmtiles-torrent": "^0.
|
|
42
|
+
"pmtiles-torrent": "^0.4.0",
|
|
43
43
|
"webtorrent": "^3.0.21"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
@@ -324,16 +324,32 @@ export class LibtorrentEngine {
|
|
|
324
324
|
return this.#call('peers', { infoHash });
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The metainfo of a torrent, so a magnet can stop being a magnet.
|
|
329
|
+
* @param {string} infoHash - The archive.
|
|
330
|
+
* @returns {Promise<Uint8Array|null>} - The .torrent bytes, or null.
|
|
331
|
+
*/
|
|
332
|
+
async metadata(infoHash) {
|
|
333
|
+
// The same thing a torrent client's "export .torrent" offers, and for the
|
|
334
|
+
// same reason: once BEP 9 has delivered the info dictionary, this node
|
|
335
|
+
// holds everything a .torrent contains — whether or not a byte of the
|
|
336
|
+
// archive itself has arrived.
|
|
337
|
+
//
|
|
338
|
+
// Without it a node that joined by magnet has no .torrent to publish, so
|
|
339
|
+
// every subscriber following its feed also joins by magnet; and a magnet
|
|
340
|
+
// that carries no trackers has only the DHT to find its first peer with,
|
|
341
|
+
// which is minutes of waiting per archive rather than none. libtorrent
|
|
342
|
+
// could always do this. Nothing had asked it to.
|
|
343
|
+
const result = await this.#call('metadata', { infoHash });
|
|
344
|
+
if (!result?.torrentFile) return null;
|
|
345
|
+
return new Uint8Array(Buffer.from(result.torrentFile, 'base64'));
|
|
346
|
+
}
|
|
347
|
+
|
|
327
348
|
/**
|
|
328
349
|
* Creates a torrent from a local file.
|
|
329
|
-
*
|
|
330
|
-
* Defaults to hybrid v1+v2, which is the capability that justifies this
|
|
331
|
-
* engine: v2 gives per-file merkle trees with 16 KiB leaf blocks, so a peer
|
|
332
|
-
* can verify a small block without holding the whole hash list, while the v1
|
|
333
|
-
* half keeps every existing client working.
|
|
334
|
-
* @param {string} filePath - Path to the archive.
|
|
350
|
+
* @param {string} filePath - The file to hash.
|
|
335
351
|
* @param {object} [options] - Piece length, trackers, web seeds, format.
|
|
336
|
-
* @returns {Promise<object>} - The
|
|
352
|
+
* @returns {Promise<object>} - The torrent file and what it describes.
|
|
337
353
|
*/
|
|
338
354
|
async createTorrent(filePath, options = {}) {
|
|
339
355
|
const result = await this.#call(
|
package/src/prewarm.js
CHANGED
|
@@ -24,6 +24,24 @@
|
|
|
24
24
|
/** How long to leave an archive alone after a failed attempt. */
|
|
25
25
|
const DEFAULT_BACKOFF_MS = 120000;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether a failure means "not yet" rather than "not working".
|
|
29
|
+
*
|
|
30
|
+
* An archive joined from a magnet has no metainfo until BEP 9 has finished, and
|
|
31
|
+
* until then the engine cannot say where a byte range even falls — so a read
|
|
32
|
+
* asked for in the same second the node started is refused before it reaches
|
|
33
|
+
* the swarm at all. That is a wait, not an attempt, and treating it as one cost
|
|
34
|
+
* the full backoff for something that resolves in seconds.
|
|
35
|
+
*
|
|
36
|
+
* Matched on the message because that is what the engine gives us; both the
|
|
37
|
+
* sidecar and the WebTorrent engine word it the same way.
|
|
38
|
+
* @param {Error} error - What the read threw.
|
|
39
|
+
* @returns {boolean} - True when it is worth trying again shortly.
|
|
40
|
+
*/
|
|
41
|
+
function tooEarly(error) {
|
|
42
|
+
return /metadata has not arrived|not held here/i.test(error?.message ?? '');
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
export class HeadWarmer {
|
|
28
46
|
#tiles;
|
|
29
47
|
#catalog;
|
|
@@ -31,6 +49,7 @@ export class HeadWarmer {
|
|
|
31
49
|
#now;
|
|
32
50
|
#timer;
|
|
33
51
|
#tried = new Map();
|
|
52
|
+
#waiting = new Set();
|
|
34
53
|
#running = false;
|
|
35
54
|
|
|
36
55
|
/**
|
|
@@ -95,7 +114,6 @@ export class HeadWarmer {
|
|
|
95
114
|
if (!entry) return null;
|
|
96
115
|
|
|
97
116
|
this.#running = true;
|
|
98
|
-
this.#tried.set(entry.infoHash, this.#now());
|
|
99
117
|
try {
|
|
100
118
|
// The long timeout, not the interactive one: this is a byte range from
|
|
101
119
|
// an archive nobody has asked for a piece of yet.
|
|
@@ -103,6 +121,9 @@ export class HeadWarmer {
|
|
|
103
121
|
timeoutMs: this.#config.tiles?.metadataTimeoutMs ?? 120000,
|
|
104
122
|
});
|
|
105
123
|
|
|
124
|
+
this.#tried.set(entry.infoHash, this.#now());
|
|
125
|
+
this.#waiting.delete(entry.infoHash);
|
|
126
|
+
|
|
106
127
|
const stored = await this.#catalog.put({
|
|
107
128
|
infoHash: entry.infoHash,
|
|
108
129
|
pmtiles: { ...entry.pmtiles, ...summary },
|
|
@@ -115,10 +136,26 @@ export class HeadWarmer {
|
|
|
115
136
|
);
|
|
116
137
|
return stored;
|
|
117
138
|
} catch (error) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
if (tooEarly(error)) {
|
|
140
|
+
// Not stamped, so the next pass tries again in seconds rather than in
|
|
141
|
+
// minutes — and said once, because a node that has just started will
|
|
142
|
+
// answer this way until the metainfo lands.
|
|
143
|
+
if (!this.#waiting.has(entry.infoHash)) {
|
|
144
|
+
this.#waiting.add(entry.infoHash);
|
|
145
|
+
console.log(
|
|
146
|
+
`[warm] ${entry.name}: waiting for the torrent metadata before ` +
|
|
147
|
+
'reading its head',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A real attempt: it reached the swarm and found nothing. Ordinary while
|
|
154
|
+
// an archive is young, since the piece holding the header may not exist
|
|
155
|
+
// anywhere reachable yet. Worth saying, because it is the answer to "why
|
|
156
|
+
// is my preview blank", and the backoff keeps it from becoming noise.
|
|
157
|
+
this.#tried.set(entry.infoHash, this.#now());
|
|
158
|
+
this.#waiting.delete(entry.infoHash);
|
|
122
159
|
console.warn(`[warm] ${entry.name}: ${error.message}`);
|
|
123
160
|
return null;
|
|
124
161
|
} finally {
|