@torrent-tv/proxy 2.82.0 → 2.83.1

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CLAUDE.md +11 -0
  3. package/docs/disk-architecture.md +161 -0
  4. package/docs/encode-architecture.md +36 -7
  5. package/package.json +1 -1
  6. package/routes/api/sources/stats/get.js +11 -2
  7. package/routes/stream/get.js +74 -3
  8. package/services/delivery-probe.js +248 -43
  9. package/services/disk/keep.js +48 -0
  10. package/services/disk/returns.js +103 -0
  11. package/services/download/SwarmSelection.js +5 -5
  12. package/services/download/registry.js +20 -0
  13. package/services/encode/EncodeRun.js +1 -0
  14. package/services/encode/Encoder.js +15 -0
  15. package/services/encode/QsvEncoder.js +5 -0
  16. package/services/encode/SegmentStore.js +14 -0
  17. package/services/encode/VaapiEncoder.js +5 -0
  18. package/services/encode/encode-exit.js +17 -0
  19. package/services/encode/start-stop-cost.js +6 -2
  20. package/services/files/CompletedFiles.js +276 -0
  21. package/services/files/piece-from-whole-file.js +118 -0
  22. package/services/hls-session-manager.js +17 -1
  23. package/services/hwaccel.js +4 -0
  24. package/services/output/cut-grid.js +13 -3
  25. package/services/piece-store/piece-disk-store.js +143 -8
  26. package/services/piece-store/piece-lru.js +17 -0
  27. package/services/piece-store/shared-piece-store.js +1803 -1549
  28. package/services/torrent-pool.js +246 -26
  29. package/services/torrent-worker/client.js +21 -0
  30. package/services/torrent-worker/protocol.js +9 -1
  31. package/services/torrent-worker/worker.js +183 -2
  32. package/test/completed-files.test.js +115 -0
  33. package/test/cuts-follow-published-grid.test.js +35 -0
  34. package/test/delivery-probe.test.js +114 -1
  35. package/test/encode-exit.test.js +18 -0
  36. package/test/keeping-period.test.js +83 -0
  37. package/test/piece-disk-store.test.js +114 -0
  38. package/test/piece-from-whole-file.test.js +129 -0
  39. package/test/piece-store-eviction.test.js +28 -15
  40. package/test/piece-store-never-refuses.test.js +153 -0
  41. package/test/piece-store-reservations.test.js +16 -3
  42. package/test/probe-wedge-certainty.test.js +3 -3
  43. package/test/shared-piece-store.test.js +27 -13
  44. package/test/stream-route.test.js +41 -0
  45. package/test/swarm-follows-readers.test.js +126 -0
  46. package/test/swarm-reach.test.js +5 -0
  47. package/test/upload-hurry.test.js +27 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @file A proxy that needs nothing from a swarm is not in that swarm.
3
+ *
4
+ * WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
5
+ * `_addIncomingPeer` checks that the torrent is neither destroyed nor paused
6
+ * and registers the peer. A proxy with its port mapped is reachable, so
7
+ * connections arrive and are never turned away — field 2026-09-11: 249
8
+ * connected at the start of one viewing, 596 at the end, 15 862 more queued, on
9
+ * a file that had been complete for three quarters of an hour, each connection
10
+ * served by reading a 4 MB piece off the disk for every 16 KB sent.
11
+ *
12
+ * The rule is not a limit on connections. While anybody is reading, every
13
+ * connection is worth keeping: the one that has delivered nothing yet may
14
+ * deliver next. It is about a torrent nobody is reading at all.
15
+ */
16
+
17
+ import test from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { TorrentPool } from "../services/torrent-pool.js";
20
+ import { demandFor, forgetTorrent } from "../services/download/registry.js";
21
+
22
+ /**
23
+ * A torrent that records what was done to it. Only the surface the rule
24
+ * touches: `paused` is the library's own flag, and the peers are what the pause
25
+ * does not close by itself.
26
+ *
27
+ * @param {number} peerCount
28
+ * @returns {object}
29
+ */
30
+ function torrentWith(peerCount) {
31
+ const peers = new Map();
32
+ for (let index = 0; index < peerCount; index += 1) {
33
+ peers.set(String(index), {
34
+ wire: { downloaded: 0 },
35
+ destroy() {
36
+ peers.delete(String(index));
37
+ }
38
+ });
39
+ }
40
+ return {
41
+ infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
42
+ files: [{ length: 1024 }],
43
+ paused: false,
44
+ wires: [...peers.values()].map((peer) => peer.wire),
45
+ _peers: peers,
46
+ pause() {
47
+ this.paused = true;
48
+ },
49
+ resume() {
50
+ this.paused = false;
51
+ }
52
+ };
53
+ }
54
+
55
+ /**
56
+ * @param {object} torrent
57
+ * @returns {TorrentPool}
58
+ */
59
+ function poolWith(torrent) {
60
+ const pool = Object.create(TorrentPool.prototype);
61
+ pool.torrents = new Map([["source", torrent]]);
62
+ pool.fileUsageByTorrent = new WeakMap();
63
+ return pool;
64
+ }
65
+
66
+ test("a torrent nobody is reading is let go of, and its data is not", () => {
67
+ const torrent = torrentWith(6);
68
+ const pool = poolWith(torrent);
69
+ try {
70
+ pool.followTheReaders(torrent);
71
+
72
+ assert.equal(torrent.paused, true, "the swarm was not left");
73
+ assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
74
+ } finally {
75
+ forgetTorrent(torrent);
76
+ }
77
+ });
78
+
79
+ test("a torrent somebody is reading keeps every connection it has", () => {
80
+ const torrent = torrentWith(6);
81
+ const pool = poolWith(torrent);
82
+ pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
83
+ try {
84
+ pool.followTheReaders(torrent);
85
+
86
+ assert.equal(torrent.paused, false, "a torrent being read was taken out of its swarm");
87
+ assert.equal(torrent._peers.size, 6, "connections were closed while somebody was reading");
88
+ } finally {
89
+ forgetTorrent(torrent);
90
+ }
91
+ });
92
+
93
+ test("a stated window keeps the swarm even with no file held", () => {
94
+ // The register is what the download layer states; a reader that has declared
95
+ // a window but not yet taken a file hold is still a reader.
96
+ const torrent = torrentWith(2);
97
+ const pool = poolWith(torrent);
98
+ demandFor(torrent).register.state({
99
+ claimant: "read-1",
100
+ fileIndex: 0,
101
+ byteStart: 0,
102
+ byteEnd: 1023,
103
+ urgency: 0
104
+ });
105
+ try {
106
+ pool.followTheReaders(torrent);
107
+ assert.equal(torrent.paused, false, "a declared window did not count as somebody reading");
108
+ } finally {
109
+ forgetTorrent(torrent);
110
+ }
111
+ });
112
+
113
+ test("the swarm is rejoined when a reader comes back", () => {
114
+ const torrent = torrentWith(3);
115
+ const pool = poolWith(torrent);
116
+ try {
117
+ pool.followTheReaders(torrent);
118
+ assert.equal(torrent.paused, true);
119
+
120
+ pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
121
+ pool.followTheReaders(torrent);
122
+ assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
123
+ } finally {
124
+ forgetTorrent(torrent);
125
+ }
126
+ });
@@ -25,6 +25,7 @@ test("connected and known are separate numbers", () => {
25
25
  const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
26
26
  assert.deepEqual(describeSwarmReach(torrent), {
27
27
  connectedPeers: 2,
28
+ deliveringPeers: 0,
28
29
  knownPeers: 7,
29
30
  queuedPeers: 4
30
31
  });
@@ -46,16 +47,19 @@ test("internals that are gone say nothing rather than breaking the poll", () =>
46
47
  // cost the field and not the answer.
47
48
  assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
48
49
  connectedPeers: 1,
50
+ deliveringPeers: 0,
49
51
  knownPeers: null,
50
52
  queuedPeers: null
51
53
  });
52
54
  assert.deepEqual(describeSwarmReach({}), {
53
55
  connectedPeers: 0,
56
+ deliveringPeers: 0,
54
57
  knownPeers: null,
55
58
  queuedPeers: null
56
59
  });
57
60
  assert.deepEqual(describeSwarmReach(null), {
58
61
  connectedPeers: 0,
62
+ deliveringPeers: 0,
59
63
  knownPeers: null,
60
64
  queuedPeers: null
61
65
  });
@@ -67,6 +71,7 @@ test("internals that are gone say nothing rather than breaking the poll", () =>
67
71
  };
68
72
  assert.deepEqual(describeSwarmReach(throwing), {
69
73
  connectedPeers: 0,
74
+ deliveringPeers: 0,
70
75
  knownPeers: null,
71
76
  queuedPeers: null
72
77
  });
@@ -119,3 +119,30 @@ test("a torrent nobody is reading is not treated as starving", () => {
119
119
  "a reader that IS waiting must still earn unchoke slots"
120
120
  );
121
121
  });
122
+
123
+ test("a torrent short of nothing is not worth uploading for", () => {
124
+ // Field 2026-09-11: the file being watched was complete, its windows all
125
+ // present, and the proxy went on offering 512 KB/s to 596 peers for
126
+ // forty-eight minutes — reading a 4 MB piece off the disk for every 16 KB it
127
+ // sent. Upload is bought with reciprocity, and reciprocity is only worth
128
+ // buying while somebody is still short of bytes.
129
+ const complete = {
130
+ name: "watched to the end",
131
+ hasActiveReader: true,
132
+ hasUnmetDemand: false,
133
+ done: false,
134
+ downloadSpeed: 0,
135
+ wires: [
136
+ { amInterested: true, peerChoking: true },
137
+ { amInterested: true, peerChoking: true },
138
+ { amInterested: true, peerChoking: true }
139
+ ]
140
+ };
141
+ const decided = decideUploadLimit([complete]);
142
+ assert.equal(decided.bytesPerSec, 8 * 1024, "a complete torrent is given the idle floor");
143
+ assert.match(decided.reason, /buys nothing/);
144
+
145
+ // And a torrent that IS short of something still earns its unchoke.
146
+ const short = { ...complete, hasUnmetDemand: true };
147
+ assert.ok(decideUploadLimit([short]).bytesPerSec > 8 * 1024, "a starving torrent still buys reciprocity");
148
+ });