pmtiles-swarm 0.28.0 β 0.29.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 +29 -0
- package/package.json +1 -1
- package/src/api.js +11 -0
- package/src/engines/composite.js +23 -0
- package/src/engines/libtorrent.js +19 -0
- package/src/engines/types.js +1 -0
- package/src/engines/webtorrent.js +38 -0
- package/src/web/index.html +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.29.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- **A connection indicator in the header, for whether the swarm can reach this node.** A node
|
|
13
|
+
nothing can connect to still downloads and still uploads β it dials out and its transfers work β
|
|
14
|
+
so none of its own traffic reveals that half the swarm can never start a conversation with it.
|
|
15
|
+
What it loses is invisible and permanent: fewer peers, slower starts, and a seed nobody fetches
|
|
16
|
+
from unless they were introduced to it first.
|
|
17
|
+
|
|
18
|
+
Green when something has connected inward, amber when the node is listening and nothing ever
|
|
19
|
+
has, red when it is not listening at all. libtorrent answers from
|
|
20
|
+
`net.has_incoming_connections`, which latches for the session, so a reachable node that is
|
|
21
|
+
merely quiet stays green rather than flickering when its last peer leaves. WebTorrent keeps no
|
|
22
|
+
such gauge, so it is assembled from the wires β each carries the direction it was made in β and
|
|
23
|
+
latched for the same reason.
|
|
24
|
+
|
|
25
|
+
Reported per engine rather than blended. Two engines means two listening ports, forwarded
|
|
26
|
+
separately, and one can be reachable while the other is not; a single verdict would have to hide
|
|
27
|
+
the one somebody needs to fix. The header shows the primary and names both on hover.
|
|
28
|
+
|
|
29
|
+
The amber state reads "no incoming yet", not "firewalled". On a node no peer has tried those are
|
|
30
|
+
the same observation, and claiming the first would put a warning on a node that is merely new.
|
|
31
|
+
An engine that cannot answer hides the indicator instead of showing red β not being able to ask
|
|
32
|
+
is not the same as being unreachable, and a red light on a healthy node is worse than none.
|
|
33
|
+
|
|
34
|
+
Needs pmtiles-torrent 0.5.0 for the libtorrent engine; against an older sidecar the indicator
|
|
35
|
+
simply stays hidden.
|
|
36
|
+
|
|
37
|
+
### π Bug fixes
|
|
38
|
+
|
|
10
39
|
## 0.28.0
|
|
11
40
|
### β¨ Features and improvements
|
|
12
41
|
- **The archive list can be searched and sorted, and says when each archive was added.** `Added` is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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/api.js
CHANGED
|
@@ -530,6 +530,17 @@ export function createApp({
|
|
|
530
530
|
? config.incompleteSuffix || null
|
|
531
531
|
: null,
|
|
532
532
|
engine: { name: engine.name, ok: engineOk, error: engineError },
|
|
533
|
+
// Whether the swarm can reach us, as opposed to whether we can reach
|
|
534
|
+
// it. A node that cannot be connected to still downloads and still
|
|
535
|
+
// uploads, so nothing about its own traffic reveals that half the
|
|
536
|
+
// swarm can never start a conversation with it.
|
|
537
|
+
reachability:
|
|
538
|
+
typeof engine.reachability === 'function'
|
|
539
|
+
? await engine.reachability().catch((error) => ({
|
|
540
|
+
state: 'unknown',
|
|
541
|
+
error: error.message,
|
|
542
|
+
}))
|
|
543
|
+
: null,
|
|
533
544
|
archives: catalog.list().length,
|
|
534
545
|
categories: catalog.categories(),
|
|
535
546
|
watching: config.watch.map((w) => w.path),
|
package/src/engines/composite.js
CHANGED
|
@@ -303,6 +303,29 @@ export class CompositeEngine {
|
|
|
303
303
|
* Every archive, with the peers and speeds of all engines added together.
|
|
304
304
|
* @returns {Promise<import('./types.js').TorrentStatus[]>} - Merged status.
|
|
305
305
|
*/
|
|
306
|
+
/**
|
|
307
|
+
* Reachability, per engine rather than blended into one verdict.
|
|
308
|
+
*
|
|
309
|
+
* Two engines means two listening ports, and they are forwarded separately.
|
|
310
|
+
* One can be reachable while the other is not, so a single answer would have
|
|
311
|
+
* to either pick a winner or average two facts into something that is not
|
|
312
|
+
* true of either -- and the one it got wrong is the one somebody needs to
|
|
313
|
+
* fix. The primary leads because it is the engine that downloads.
|
|
314
|
+
* @returns {Promise<object>} - `{state, engines}`.
|
|
315
|
+
*/
|
|
316
|
+
async reachability() {
|
|
317
|
+
const engines = [];
|
|
318
|
+
for (const engine of [this.#primary, ...this.#secondaries]) {
|
|
319
|
+
if (typeof engine.reachability !== 'function') continue;
|
|
320
|
+
const report = await engine.reachability().catch((error) => ({
|
|
321
|
+
state: 'unknown',
|
|
322
|
+
error: error.message,
|
|
323
|
+
}));
|
|
324
|
+
if (report) engines.push({ engine: engine.name, ...report });
|
|
325
|
+
}
|
|
326
|
+
return { ...(engines[0] ?? { state: 'unknown' }), engines };
|
|
327
|
+
}
|
|
328
|
+
|
|
306
329
|
async list() {
|
|
307
330
|
if (this.#stopping) return [];
|
|
308
331
|
const primary = await this.#primary.list();
|
|
@@ -284,6 +284,25 @@ export class LibtorrentEngine {
|
|
|
284
284
|
return result?.trackers ?? [];
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Whether peers can open a connection to this node, or only the reverse.
|
|
289
|
+
*
|
|
290
|
+
* See the sidecar's op_reachability for what the three states mean and why
|
|
291
|
+
* the middle one is "unproven" rather than "firewalled": on a node with no
|
|
292
|
+
* peers, blocked and untried are the same observation.
|
|
293
|
+
* @returns {Promise<object|null>} - The report, or null when unavailable.
|
|
294
|
+
*/
|
|
295
|
+
async reachability() {
|
|
296
|
+
if (this.#stopping) return null;
|
|
297
|
+
try {
|
|
298
|
+
return await this.#call('reachability', {});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
// An engine that cannot answer is not an engine that is unreachable, and
|
|
301
|
+
// reporting it as offline would put a red light on a healthy node.
|
|
302
|
+
return { state: 'unknown', error: error.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
287
306
|
async list() {
|
|
288
307
|
// A node that is shutting down still has a console polling it and a sweep
|
|
289
308
|
// or two in flight. Answering "the sidecar exited" to each of them fills
|
package/src/engines/types.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
* @property {(filePath: string, options?: object) => Promise<object>} [createTorrent] - Builds a torrent from a local file, where the engine can do it better than the default β libtorrent produces hybrid v1+v2, which create-torrent cannot.
|
|
61
61
|
* @property {(infoHash: string) => Promise<object[]>} [trackerStatus] - Per-tracker announce results, where the engine keeps them.
|
|
62
62
|
* @property {(infoHash: string) => Promise<Uint8Array | null>} [metadata] - The torrent's metainfo once known, so an archive joined by magnet can be written down rather than re-fetched over BEP 9 on every start.
|
|
63
|
+
* @property {() => Promise<object | null>} [reachability] - Whether peers can open a connection to this node, or only the reverse. Three states: `open` (something has connected inward), `unproven` (listening, but nothing ever has) and `offline` (not listening at all). The middle is deliberately not called firewalled -- on a node with no peers, blocked and untried are the same observation.
|
|
63
64
|
* @property {() => Promise<void>} destroy - Releases resources.
|
|
64
65
|
*/
|
|
65
66
|
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
export class WebTorrentSeedEngine {
|
|
35
35
|
#options;
|
|
36
36
|
#client = null;
|
|
37
|
+
#everIncoming = false;
|
|
37
38
|
/**
|
|
38
39
|
* A client error that means nothing will ever work.
|
|
39
40
|
*
|
|
@@ -319,6 +320,43 @@ export class WebTorrentSeedEngine {
|
|
|
319
320
|
* Lists every torrent the client holds.
|
|
320
321
|
* @returns {Promise<import('./types.js').TorrentStatus[]>} - Normalised statuses.
|
|
321
322
|
*/
|
|
323
|
+
/**
|
|
324
|
+
* Whether peers can open a connection to this node, or only the reverse.
|
|
325
|
+
*
|
|
326
|
+
* WebTorrent keeps no equivalent of libtorrent's has_incoming_connections
|
|
327
|
+
* gauge, so it is assembled from the wires: every one carries the direction
|
|
328
|
+
* it was made in, and a type ending "Incoming" is somebody who reached us.
|
|
329
|
+
*
|
|
330
|
+
* Latched rather than sampled, which is the whole reason for the field. A
|
|
331
|
+
* wire is gone the moment the peer leaves, so asking "is one open now" would
|
|
332
|
+
* report a reachable node as unproven every time it went quiet. What is worth
|
|
333
|
+
* knowing is whether it has ever happened at all.
|
|
334
|
+
* @returns {Promise<object|null>} - The report, or null when not started.
|
|
335
|
+
*/
|
|
336
|
+
async reachability() {
|
|
337
|
+
const client = this.#client;
|
|
338
|
+
if (!client) return null;
|
|
339
|
+
|
|
340
|
+
if (!this.#everIncoming) {
|
|
341
|
+
this.#everIncoming = (client.torrents ?? []).some((torrent) =>
|
|
342
|
+
(torrent.wires ?? []).some((wire) =>
|
|
343
|
+
String(wire.type ?? '').endsWith('Incoming'),
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const listening = Boolean(client.listening);
|
|
349
|
+
return {
|
|
350
|
+
state: !listening ? 'offline' : this.#everIncoming ? 'open' : 'unproven',
|
|
351
|
+
listening,
|
|
352
|
+
port: client.torrentPort ?? null,
|
|
353
|
+
peersConnected: (client.torrents ?? []).reduce(
|
|
354
|
+
(sum, torrent) => sum + (torrent.numPeers ?? 0),
|
|
355
|
+
0,
|
|
356
|
+
),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
322
360
|
async list() {
|
|
323
361
|
if (!this.#client) return [];
|
|
324
362
|
return this.#client.torrents.map((torrent) => this.#normalise(torrent));
|
package/src/web/index.html
CHANGED
|
@@ -49,6 +49,19 @@
|
|
|
49
49
|
h2 { font-size: 0.95rem; margin: 0 0 0.6rem; font-weight: 600; }
|
|
50
50
|
.status { color: var(--muted); font-size: 0.85rem; }
|
|
51
51
|
.status b { color: var(--fg); font-weight: 600; }
|
|
52
|
+
/* A dot rather than an image: it inherits the theme, needs no asset,
|
|
53
|
+
and stays legible at the size a header allows. */
|
|
54
|
+
.reach {
|
|
55
|
+
display: inline-flex; align-items: center; gap: 0.35rem;
|
|
56
|
+
font-size: 0.8rem; color: var(--muted); cursor: default;
|
|
57
|
+
}
|
|
58
|
+
.reach::before {
|
|
59
|
+
content: ""; width: 0.6rem; height: 0.6rem; border-radius: 50%;
|
|
60
|
+
background: var(--muted);
|
|
61
|
+
}
|
|
62
|
+
.reach.open::before { background: var(--ok); }
|
|
63
|
+
.reach.unproven::before { background: var(--warn); }
|
|
64
|
+
.reach.offline::before { background: var(--bad); }
|
|
52
65
|
nav { margin-left: auto; display: flex; gap: 0.35rem; }
|
|
53
66
|
nav button.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
54
67
|
main { padding: 1.25rem; }
|
|
@@ -305,6 +318,7 @@
|
|
|
305
318
|
<header>
|
|
306
319
|
<h1>pmtiles-swarm</h1>
|
|
307
320
|
<div class="status" id="status">connectingβ¦</div>
|
|
321
|
+
<span id="reach" class="reach" hidden></span>
|
|
308
322
|
<button id="speed-toggle" class="speed" hidden title="Switch between the normal and alternative speed limits"></button>
|
|
309
323
|
<nav>
|
|
310
324
|
<button id="tab-archives" class="on">Archives</button>
|
|
@@ -987,6 +1001,50 @@
|
|
|
987
1001
|
}
|
|
988
1002
|
};
|
|
989
1003
|
|
|
1004
|
+
/**
|
|
1005
|
+
* The connection indicator: can the swarm reach us, or only we it?
|
|
1006
|
+
*
|
|
1007
|
+
* A node nothing can connect to still downloads and still uploads, so
|
|
1008
|
+
* none of its own traffic reveals the problem -- it simply gets fewer
|
|
1009
|
+
* peers, slower starts, and nobody fetching from it unless introduced
|
|
1010
|
+
* first. That is the whole reason for showing it at all.
|
|
1011
|
+
*
|
|
1012
|
+
* The middle state says "no incoming yet" rather than "firewalled",
|
|
1013
|
+
* because those are the same observation on a node no peer has tried.
|
|
1014
|
+
* Calling it firewalled would put a warning on a healthy node that is
|
|
1015
|
+
* merely new or quiet, which is worse than saying less.
|
|
1016
|
+
*
|
|
1017
|
+
* @param {object} report - status.reachability.
|
|
1018
|
+
* @returns {void}
|
|
1019
|
+
*/
|
|
1020
|
+
function renderReach(report) {
|
|
1021
|
+
const host = $('reach');
|
|
1022
|
+
if (!report || !report.state || report.state === 'unknown') {
|
|
1023
|
+
host.hidden = true;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
host.hidden = false;
|
|
1027
|
+
host.className = `reach ${report.state}`;
|
|
1028
|
+
|
|
1029
|
+
const label = {
|
|
1030
|
+
open: 'reachable',
|
|
1031
|
+
unproven: 'no incoming yet',
|
|
1032
|
+
offline: 'not listening',
|
|
1033
|
+
}[report.state];
|
|
1034
|
+
host.textContent = label;
|
|
1035
|
+
|
|
1036
|
+
const detail = (report.engines ?? [report]).map((one) => {
|
|
1037
|
+
const which = one.engine ? `${one.engine}: ` : '';
|
|
1038
|
+
const port = one.port ? ` on port ${one.port}` : '';
|
|
1039
|
+
if (one.state === 'offline') return `${which}not listening`;
|
|
1040
|
+
if (one.state === 'open') {
|
|
1041
|
+
return `${which}${one.incomingConnections ?? 0} peer(s) have connected in${port}`;
|
|
1042
|
+
}
|
|
1043
|
+
return `${which}listening${port}, but nothing has connected in yet β it may be firewalled, or simply untried`;
|
|
1044
|
+
});
|
|
1045
|
+
host.title = detail.join('\n');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
990
1048
|
async function refresh() {
|
|
991
1049
|
try {
|
|
992
1050
|
const [status, list, speed, adds] = await Promise.all([
|
|
@@ -1002,6 +1060,7 @@
|
|
|
1002
1060
|
// nothing renames it. The engine decides, not the setting alone.
|
|
1003
1061
|
incompleteMarker = status.incompleteMarker ?? null;
|
|
1004
1062
|
const engine = status.engine;
|
|
1063
|
+
renderReach(status.reachability);
|
|
1005
1064
|
$('status').innerHTML =
|
|
1006
1065
|
`engine <b>${engine.name}</b> ${engine.ok ? 'ready' : 'unavailable'}` +
|
|
1007
1066
|
` Β· <b>${status.archives}</b> archives`;
|