@torrent-tv/proxy 2.83.2 → 2.83.4

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.
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @file WHEN NOBODY WANTS A FILE ANY MORE, THE MAP SAYS SO.
3
+ *
4
+ * The map is the only statement of what is wanted, and until 2.83.4 it could
5
+ * make only one of the two statements it exists for. A file whose viewers had
6
+ * gone was deleted from the orchestrator's own memory and published nowhere, so
7
+ * the bands it had stated on the swarm's behalf stood until the torrent itself
8
+ * was removed — read from the demand register, a film nobody had watched for an
9
+ * hour was indistinguishable from one being watched now.
10
+ *
11
+ * Two halves, and both are here: the side that builds the map has to say it,
12
+ * and the side that receives it has to act on it without needing the file's
13
+ * length, the torrent's list, or the torrent to exist at all.
14
+ */
15
+
16
+ import test from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import { PriorityOrchestrator } from "../services/priority/PriorityOrchestrator.js";
19
+ import { Viewers } from "../services/viewer/Viewers.js";
20
+ import { viewersOf } from "../services/viewer/Viewer.js";
21
+ import { TorrentPool } from "../services/torrent-pool.js";
22
+ import { demandFor, forgetTorrent } from "../services/download/registry.js";
23
+ import { Urgency } from "../services/demand/index.js";
24
+
25
+ const STALE_AFTER_MS = 60_000;
26
+
27
+ /**
28
+ * A session, as much of one as the orchestrator reads.
29
+ *
30
+ * @param {{ id: string, fileIndex?: number }} params
31
+ * @returns {object}
32
+ */
33
+ function outputOf({ id, fileIndex = 0 }) {
34
+ return {
35
+ id,
36
+ outputKey: `out:${id}`,
37
+ sourceKey: "source-1",
38
+ fileIndex,
39
+ file: { key: `film-${fileIndex}`, durationSeconds: 600 }
40
+ };
41
+ }
42
+
43
+ /**
44
+ * The real orchestrator, with everything it publishes kept.
45
+ *
46
+ * @param {object[]} sessions
47
+ * @returns {{ priority: PriorityOrchestrator, viewers: Viewers, published: object[], publish: (sessions?: object[]) => void }}
48
+ */
49
+ function over(sessions) {
50
+ const published = [];
51
+ const viewers = new Viewers();
52
+ const priority = new PriorityOrchestrator({
53
+ publish: (one) => published.push(one),
54
+ viewersOf: (session) => viewersOf(session),
55
+ allowanceFor: () => 10
56
+ });
57
+ return {
58
+ priority,
59
+ viewers,
60
+ published,
61
+ publish: (live = sessions) =>
62
+ priority.publishFor({ sessionGroups: [live], staleAfterMs: STALE_AFTER_MS })
63
+ };
64
+ }
65
+
66
+ test("a file whose viewers have all gone is published as wanting nothing", () => {
67
+ const picture = outputOf({ id: "pic" });
68
+ const { viewers, published, publish } = over([picture]);
69
+ const person = viewers.of(picture, "p");
70
+ person.moveTo(300);
71
+ publish();
72
+ assert.ok(published.at(-1).zones.length > 0, "somebody is watching, so something is wanted");
73
+
74
+ // They stop answering. The session is still there — it outlives them by half
75
+ // an hour — so this is the case that used to say nothing at all.
76
+ person.seen(Date.now() - STALE_AFTER_MS * 2);
77
+ publish();
78
+
79
+ const last = published.at(-1);
80
+ assert.deepEqual(last.zones, []);
81
+ assert.equal(last.sourceKey, "source-1");
82
+ assert.equal(last.fileIndex, 0);
83
+ });
84
+
85
+ test("a file whose session has gone is published as wanting nothing, once", () => {
86
+ const picture = outputOf({ id: "pic" });
87
+ const { viewers, published, publish } = over([picture]);
88
+ viewers.of(picture, "p").moveTo(300);
89
+ publish();
90
+ const said = published.length;
91
+
92
+ // The session is disposed: it is not among the live ones any more.
93
+ publish([]);
94
+ assert.deepEqual(published.at(-1).zones, []);
95
+ assert.equal(published.length, said + 1);
96
+
97
+ // And it is not repeated on every pass afterwards — the file is gone from
98
+ // this class's memory, and a departure is said once.
99
+ publish([]);
100
+ publish([]);
101
+ assert.equal(published.length, said + 1);
102
+ });
103
+
104
+ test("an unchanged map is still not republished", () => {
105
+ const picture = outputOf({ id: "pic" });
106
+ const { viewers, published, publish } = over([picture]);
107
+ viewers.of(picture, "p").moveTo(300);
108
+ publish();
109
+ const said = published.length;
110
+ publish();
111
+ publish();
112
+ assert.equal(published.length, said, "the downloading rebuilds its requests on every one");
113
+ });
114
+
115
+ /** A torrent that is nothing but one file of a known length. */
116
+ function torrentOf({ length = 1_000_000 } = {}) {
117
+ return {
118
+ infoHash: `hash-${Math.random().toString(36).slice(2)}`,
119
+ pieceLength: 1024,
120
+ files: [{ offset: 0, length, name: "film.mkv" }],
121
+ _selections: { _items: [] },
122
+ _select() {},
123
+ _deselect() {},
124
+ critical() {}
125
+ };
126
+ }
127
+
128
+ const applyPriorityMap = TorrentPool.prototype.applyPriorityMap;
129
+
130
+ /**
131
+ * @param {object} torrent
132
+ * @returns {object[]}
133
+ */
134
+ function stated(torrent) {
135
+ return demandFor(torrent)
136
+ .register.windows()
137
+ .filter((one) => String(one.claimant).startsWith("priority-map:"));
138
+ }
139
+
140
+ test("a map with nothing in it withdraws everything that file had stated", () => {
141
+ const torrent = torrentOf();
142
+ try {
143
+ applyPriorityMap.call(null, torrent, 0, [
144
+ { from: 0, to: 100, priority: 100 },
145
+ { from: 100, to: 600, priority: 50 }
146
+ ], 600);
147
+ assert.equal(stated(torrent).length, 2);
148
+
149
+ applyPriorityMap.call(null, torrent, 0, [], 600);
150
+ assert.equal(stated(torrent).length, 0);
151
+ } finally {
152
+ forgetTorrent(torrent);
153
+ }
154
+ });
155
+
156
+ test("it withdraws that file's bands and nobody else's", () => {
157
+ const torrent = torrentOf();
158
+ try {
159
+ const { register } = demandFor(torrent);
160
+ applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 600, priority: 100 }], 600);
161
+ // A read stopped on a piece right now, which only a read can say, and the
162
+ // background fill, which the pool states for itself. Neither is the map's
163
+ // to withdraw.
164
+ register.state({
165
+ claimant: "read-7:blocked",
166
+ fileIndex: 0,
167
+ byteStart: 0,
168
+ byteEnd: 999,
169
+ urgency: Urgency.BLOCKED
170
+ });
171
+ register.state({
172
+ claimant: "background-fill:0",
173
+ fileIndex: 0,
174
+ byteStart: 1000,
175
+ byteEnd: 9999,
176
+ urgency: Urgency.TAIL
177
+ });
178
+
179
+ applyPriorityMap.call(null, torrent, 0, [], 600);
180
+
181
+ assert.equal(stated(torrent).length, 0);
182
+ assert.deepEqual(
183
+ register.windows().map((one) => one.claimant).sort(),
184
+ ["background-fill:0", "read-7:blocked"]
185
+ );
186
+ } finally {
187
+ forgetTorrent(torrent);
188
+ }
189
+ });
190
+
191
+ test("a departure is answered even when nothing else about the file is known", () => {
192
+ // The length, the duration and the torrent's own list are what the ordinary
193
+ // path needs to turn seconds into bytes. A departure needs none of them, and
194
+ // it arrives exactly when they are going away: the map that says it is
195
+ // published as the last viewer leaves.
196
+ const torrent = torrentOf();
197
+ try {
198
+ applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 600, priority: 100 }], 600);
199
+ assert.equal(stated(torrent).length, 1);
200
+
201
+ torrent.files = [];
202
+ applyPriorityMap.call(null, torrent, 0, [], 0);
203
+ assert.equal(stated(torrent).length, 0);
204
+ } finally {
205
+ forgetTorrent(torrent);
206
+ }
207
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @file WHAT THE BROWSER IS TOLD IS IN A TORRENT.
3
+ *
4
+ * This is the one place the product answers it. The browser used to answer it
5
+ * as well — a list of video extensions in its parser and a second, shorter pair
6
+ * inside its picker — and the three had already diverged: measured 2026-09-12,
7
+ * `.dat` was offered there as video and not counted here, which also decides
8
+ * whether a sidecar whose name matches nothing can belong to the only video
9
+ * present.
10
+ *
11
+ * So the shape of this answer is a contract with the page, and the page shows
12
+ * what it is given without looking at a name.
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { handleApiSourceFilesGet } from "../routes/api/sources/files/get.js";
18
+
19
+ /**
20
+ * A request, a reply and the torrent behind them.
21
+ *
22
+ * @param {Array<{ path: string, length: number }>} files
23
+ * @returns {{ req: object, reply: object, sent: { body: object | null, code: number }, deps: object }}
24
+ */
25
+ function harness(files) {
26
+ const sent = { body: null, code: 200 };
27
+ const reply = {
28
+ code(value) {
29
+ sent.code = value;
30
+ return reply;
31
+ },
32
+ send(body) {
33
+ sent.body = body;
34
+ return reply;
35
+ }
36
+ };
37
+ const deps = {
38
+ sourceRegistry: { get: () => ({ sourceType: "magnet", source: "magnet:?xt=urn:btih:abc" }) },
39
+ torrentPool: {
40
+ async getTorrent() {
41
+ return { name: "Drifters", infoHash: "abc", files };
42
+ }
43
+ }
44
+ };
45
+ return { req: { params: { sourceKey: "key" }, query: {} }, reply, sent, deps };
46
+ }
47
+
48
+ test("the files come back in reading order, each saying what it is", () => {
49
+ const { req, reply, sent, deps } = harness([
50
+ { path: "Drifters/ep 10.mkv", length: 9 },
51
+ { path: "Drifters/ep 2.mkv", length: 9 },
52
+ { path: "Drifters/Rus Sound/ep 2.mka", length: 2 },
53
+ { path: "Drifters/notes.nfo", length: 1 }
54
+ ]);
55
+
56
+ return handleApiSourceFilesGet(req, reply, deps).then(() => {
57
+ assert.equal(sent.code, 200);
58
+ assert.deepEqual(
59
+ sent.body.files.map((file) => [file.relativePath, file.kind]),
60
+ [
61
+ // Runs of digits compare as numbers, so 2 comes before 10; the torrent's
62
+ // own order is whatever the tool that made it chose.
63
+ ["ep 2.mkv", "video"],
64
+ ["ep 10.mkv", "video"],
65
+ ["notes.nfo", "other"],
66
+ ["Rus Sound/ep 2.mka", "audio"]
67
+ ]
68
+ );
69
+ // Relative to the torrent root: WebTorrent prefixes its own name to every
70
+ // path, and stripping it here means one rule in the product rather than two
71
+ // that can disagree.
72
+ assert.ok(sent.body.files.every((file) => !file.relativePath.startsWith("Drifters/")));
73
+ });
74
+ });
75
+
76
+ test("each picture is named with what belongs to it, by index", () => {
77
+ const { req, reply, sent, deps } = harness([
78
+ { path: "Drifters/ep 1.mkv", length: 9 },
79
+ { path: "Drifters/Rus Sound/ep 1.mka", length: 2 },
80
+ { path: "Drifters/Sub/ep 1.ass", length: 1 },
81
+ { path: "Drifters/ep 2.mkv", length: 9 }
82
+ ]);
83
+
84
+ return handleApiSourceFilesGet(req, reply, deps).then(() => {
85
+ assert.deepEqual(sent.body.items, [
86
+ { fileIndex: 0, audio: [1], subtitles: [2], images: [] },
87
+ { fileIndex: 3, audio: [], subtitles: [], images: [] }
88
+ ]);
89
+ // The files themselves are in the list above; saying them twice is how two
90
+ // copies of one fact start.
91
+ assert.ok(sent.body.items.every((item) => item.audio.every((one) => Number.isInteger(one))));
92
+ });
93
+ });
94
+
95
+ test("a torrent whose metadata has not arrived says so and lists nothing", () => {
96
+ const { req, reply, sent, deps } = harness([]);
97
+
98
+ return handleApiSourceFilesGet(req, reply, deps).then(() => {
99
+ assert.deepEqual(sent.body.files, []);
100
+ assert.deepEqual(sent.body.items, []);
101
+ assert.equal(sent.body.name, "Drifters");
102
+ });
103
+ });
@@ -19,11 +19,11 @@ import { handleStreamGet } from "../routes/stream/get.js";
19
19
  * Minimal stand-ins for the parts of Fastify and the pool this route touches.
20
20
  *
21
21
  * @param {{ method: string, range?: string }} request
22
- * @returns {{ req: object, reply: object, sent: object, opened: string[], claims: number }}
22
+ * @returns {{ req: object, reply: object, sent: object, opened: string[], state: object }}
23
23
  */
24
24
  function harness({ method, range }) {
25
25
  const opened = [];
26
- const state = { claims: 0, prioritized: [] };
26
+ const state = { prioritized: [] };
27
27
 
28
28
  const sent = { code: 200, headers: {}, body: undefined, called: false };
29
29
  const reply = {
@@ -71,10 +71,6 @@ function harness({ method, range }) {
71
71
  async getTorrent() {
72
72
  return { files: [file], sourceKey: "key" };
73
73
  },
74
- acquireFile() {
75
- state.claims += 1;
76
- return () => undefined;
77
- },
78
74
  prioritizeByteRange(_torrent, fileIndex, byteStart, _windowBytes, options) {
79
75
  state.prioritized.push({ byteStart, wholeFileRead: options?.wholeFileRead === true });
80
76
  }
@@ -96,7 +92,7 @@ test("HEAD reports the size without opening a read", async () => {
96
92
  await handleStreamGet(req, reply, deps);
97
93
 
98
94
  assert.deepEqual(opened, [], "HEAD started a read of the file");
99
- assert.equal(state.claims, 0, "HEAD claimed the file it never read");
95
+ assert.deepEqual(state.prioritized, [], "HEAD asked the swarm for a read it never made");
100
96
  // The real size, not the zero Fastify substitutes for an empty payload — the
101
97
  // keyframe index reads this header and treats 0 as "no index".
102
98
  assert.equal(sent.headers["content-length"], "5869669065");
@@ -182,5 +178,5 @@ test("a file this proxy does not have whole still goes to the torrent", async ()
182
178
  const { req, reply, state, deps } = harness({ method: "GET", range: "bytes=0-99" });
183
179
  deps.torrentPool.wholeFiles = new Map([["other/7", { path: "/nowhere", length: 1, name: "x" }]]);
184
180
  await handleStreamGet(req, reply, deps);
185
- assert.equal(state.claims, 1, "the ordinary path was not taken");
181
+ assert.equal(state.prioritized.length, 1, "the ordinary path was not taken");
186
182
  });
@@ -3,21 +3,38 @@
3
3
  *
4
4
  * WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
5
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.
6
+ * and registers the peer. A proxy with its port mapped is reachable, so on a
7
+ * popular torrent connections arrive and are never turned away — field
8
+ * 2026-09-11: 249 connected at the start of one viewing, 596 at the end, 15 862
9
+ * more queued, on a file complete for three quarters of an hour.
11
10
  *
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.
11
+ * The rule is not a limit on connections. While anybody is reading, every one is
12
+ * worth keeping: the one that has delivered nothing yet may deliver next.
13
+ *
14
+ * AND IT IS ASKED OF WHAT HAS BEEN STATED, never of a count of readers. The
15
+ * first version asked every five seconds whether anybody was READING, and a
16
+ * torrent added three seconds earlier answered no — its edges still being read,
17
+ * its plan still being built. It left the swarm with 741 connections let go,
18
+ * and nothing rejoined it: rejoining waited for a reader, and the reader was
19
+ * waiting for the header the swarm had been fetching. Playback did not start at
20
+ * all.
21
+ *
22
+ * The question now is whether anything is WANTED of it, which the priority map,
23
+ * the ends of an open file and a stopped read all answer — and which is true
24
+ * from the moment a torrent is opened.
15
25
  */
16
26
 
17
27
  import test from "node:test";
18
28
  import assert from "node:assert/strict";
19
- import { TorrentPool } from "../services/torrent-pool.js";
29
+ import {
30
+ isWanted,
31
+ leaveSwarm,
32
+ rejoinSwarm,
33
+ stateFileEdges,
34
+ swarmDecisionFor
35
+ } from "../services/torrent-pool.js";
20
36
  import { demandFor, forgetTorrent } from "../services/download/registry.js";
37
+ import { Urgency } from "../services/demand/index.js";
21
38
 
22
39
  /**
23
40
  * A torrent that records what was done to it. Only the surface the rule
@@ -39,7 +56,7 @@ function torrentWith(peerCount) {
39
56
  }
40
57
  return {
41
58
  infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
42
- files: [{ length: 1024 }],
59
+ files: [{ length: 1024 }, { length: 2048 }],
43
60
  paused: false,
44
61
  wires: [...peers.values()].map((peer) => peer.wire),
45
62
  _peers: peers,
@@ -52,117 +69,106 @@ function torrentWith(peerCount) {
52
69
  };
53
70
  }
54
71
 
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", () => {
72
+ test("letting a swarm go pauses the torrent and closes what the pause does not", () => {
67
73
  const torrent = torrentWith(6);
68
- const pool = poolWith(torrent);
69
- try {
70
- // Read once and left, which is the state this is about: a film nobody has
71
- // opened yet is being set up, and the test below covers that.
72
- pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
73
- pool.followTheReaders(torrent);
74
- pool.fileUsageByTorrent.set(torrent, new Map());
75
- pool.followTheReaders(torrent);
76
-
77
- assert.equal(torrent.paused, true, "the swarm was not left");
78
- assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
79
- } finally {
80
- forgetTorrent(torrent);
81
- }
82
- });
83
74
 
84
- test("a torrent somebody is reading keeps every connection it has", () => {
85
- const torrent = torrentWith(6);
86
- const pool = poolWith(torrent);
87
- pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
88
- try {
89
- pool.followTheReaders(torrent);
75
+ const closed = leaveSwarm(torrent);
90
76
 
91
- assert.equal(torrent.paused, false, "a torrent being read was taken out of its swarm");
92
- assert.equal(torrent._peers.size, 6, "connections were closed while somebody was reading");
93
- } finally {
94
- forgetTorrent(torrent);
95
- }
77
+ assert.equal(torrent.paused, true, "the library's own word for this was not used");
78
+ assert.equal(closed, 6);
79
+ assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
96
80
  });
97
81
 
98
- test("a stated window keeps the swarm even with no file held", () => {
99
- // The register is what the download layer states; a reader that has declared
100
- // a window but not yet taken a file hold is still a reader.
101
- const torrent = torrentWith(2);
102
- const pool = poolWith(torrent);
103
- demandFor(torrent).register.state({
104
- claimant: "read-1",
105
- fileIndex: 0,
106
- byteStart: 0,
107
- byteEnd: 1023,
108
- urgency: 0
109
- });
110
- try {
111
- pool.followTheReaders(torrent);
112
- assert.equal(torrent.paused, false, "a declared window did not count as somebody reading");
113
- } finally {
114
- forgetTorrent(torrent);
115
- }
82
+ test("a swarm already let go is not let go twice", () => {
83
+ const torrent = torrentWith(4);
84
+ leaveSwarm(torrent);
85
+ assert.equal(leaveSwarm(torrent), 0);
116
86
  });
117
87
 
118
- test("the swarm is rejoined when a reader comes back", () => {
88
+ test("the reader that comes back takes the swarm with it", () => {
119
89
  const torrent = torrentWith(3);
120
- const pool = poolWith(torrent);
121
- try {
122
- pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
123
- pool.followTheReaders(torrent);
124
- pool.fileUsageByTorrent.set(torrent, new Map());
125
- pool.followTheReaders(torrent);
126
- assert.equal(torrent.paused, true);
127
-
128
- pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
129
- pool.followTheReaders(torrent);
130
- assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
131
- } finally {
132
- forgetTorrent(torrent);
133
- }
90
+ leaveSwarm(torrent);
91
+
92
+ assert.equal(rejoinSwarm(torrent), true);
93
+ assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
94
+ assert.equal(rejoinSwarm(torrent), false, "a torrent already in its swarm was resumed again");
134
95
  });
135
96
 
136
- test("a torrent nobody has read yet is being set up, and is left alone", () => {
137
- // Field 2026-09-11, and it broke playback outright: a torrent added at
138
- // 20:57:17 had its first peer at 20:57:18 and was taken out of the swarm at
139
- // 20:57:21while the read of the file's edges was still in flight. Nothing
140
- // rejoined it, because rejoining waits for a reader and the reader was
141
- // waiting for the header the swarm was fetching.
97
+ test("a torrent whose metadata has not arrived is wanted, whatever is stated", () => {
98
+ // THE FIELD FAILURE OF 2026-09-11, as a question rather than as a mechanism.
99
+ // A torrent being added has no list of files, so nothing can name a byte of
100
+ // itand it is being fetched precisely because somebody asked for it.
142
101
  const torrent = torrentWith(103);
143
- const pool = poolWith(torrent);
102
+ torrent.files = [];
103
+ assert.equal(isWanted(torrent), true);
104
+ });
105
+
106
+ test("a torrent with files and nothing stated for them is wanted by nobody", () => {
107
+ const torrent = torrentWith(4);
144
108
  try {
145
- pool.followTheReaders(torrent);
146
- assert.equal(torrent.paused, false, "a torrent being opened was taken out of its swarm");
147
- assert.equal(torrent._peers.size, 103, "and its connections were let go");
109
+ assert.equal(isWanted(torrent), false);
148
110
  } finally {
149
111
  forgetTorrent(torrent);
150
112
  }
151
113
  });
152
114
 
153
- test("once it has been read, leaving is allowed again", () => {
115
+ test("anything stated makes it wanted, and the last withdrawal ends that", () => {
154
116
  const torrent = torrentWith(4);
155
- const pool = poolWith(torrent);
156
117
  try {
157
- // A reader arrives and goes.
158
- pool.fileUsageByTorrent.set(torrent, new Map([[0, 1]]));
159
- pool.followTheReaders(torrent);
160
- assert.equal(torrent.paused, false);
161
-
162
- pool.fileUsageByTorrent.set(torrent, new Map());
163
- pool.followTheReaders(torrent);
164
- assert.equal(torrent.paused, true, "a film somebody left keeps its swarm for nobody");
118
+ // The ends of a file being opened, which is the first thing said about a
119
+ // torrent anybody has picked and the reason it stays in its swarm through
120
+ // the seconds when nothing else can say anything about it.
121
+ stateFileEdges(torrent, 0, Urgency.TAIL);
122
+ assert.equal(isWanted(torrent), true);
123
+
124
+ const { register } = demandFor(torrent);
125
+ register.withdraw("file-edges:0:head");
126
+ assert.equal(isWanted(torrent), true, "one end of it is still wanted");
127
+ register.withdraw("file-edges:0:tail");
128
+ assert.equal(isWanted(torrent), false);
165
129
  } finally {
166
130
  forgetTorrent(torrent);
167
131
  }
168
132
  });
133
+
134
+ test("a torrent that has been destroyed is wanted by nobody", () => {
135
+ const torrent = torrentWith(1);
136
+ torrent.destroyed = true;
137
+ assert.equal(isWanted(torrent), false);
138
+ });
139
+
140
+ test("a torrent something is wanted of keeps its swarm and is off the clock", () => {
141
+ assert.deepEqual(
142
+ swarmDecisionFor({ wanted: true, everWanted: true }),
143
+ { swarm: "take", onTheClock: false }
144
+ );
145
+ assert.deepEqual(
146
+ swarmDecisionFor({ wanted: true, everWanted: false }),
147
+ { swarm: "take", onTheClock: false }
148
+ );
149
+ });
150
+
151
+ test("a swarm is let go on a DEPARTURE, never on a beginning", () => {
152
+ // The whole of the 2.83.1 failure in one line: a torrent that has never been
153
+ // wanted is one being opened — its metadata has just landed, the file list is
154
+ // on its way to the person choosing, and nothing has had a chance to state
155
+ // anything about it yet. Taking its swarm away there destroyed 741
156
+ // connections it needed a minute later.
157
+ assert.deepEqual(
158
+ swarmDecisionFor({ wanted: false, everWanted: false }),
159
+ { swarm: "leave alone", onTheClock: true }
160
+ );
161
+ assert.deepEqual(
162
+ swarmDecisionFor({ wanted: false, everWanted: true }),
163
+ { swarm: "let go", onTheClock: true }
164
+ );
165
+ });
166
+
167
+ test("whatever happens to the swarm, an unwanted torrent is on the idle clock", () => {
168
+ // Including the one nobody has ever wanted: a file list fetched and never
169
+ // played used to be held for the life of the process, because the only thing
170
+ // that started the clock was a reader letting go.
171
+ for (const everWanted of [true, false]) {
172
+ assert.equal(swarmDecisionFor({ wanted: false, everWanted }).onTheClock, true);
173
+ }
174
+ });