@torrent-tv/proxy 2.80.19 → 2.81.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 (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/research/double-spawn-2026-09-10.md +171 -0
  4. package/services/disk/DiskSpace.js +150 -0
  5. package/services/disk/wire.js +60 -0
  6. package/services/encode/EncodeRun.js +25 -3
  7. package/services/encode/SegmentStore.js +137 -9
  8. package/services/hls-session-manager.js +26 -45
  9. package/services/orchestrators/EncodeOrchestrator.js +4 -1
  10. package/services/piece-store/allowance.js +107 -0
  11. package/services/piece-store/piece-disk-store.js +365 -0
  12. package/services/piece-store/shared-piece-store.js +1549 -1535
  13. package/services/torrent-worker/client.js +32 -0
  14. package/services/torrent-worker/pool-adapter.js +15 -0
  15. package/services/torrent-worker/protocol.js +9 -0
  16. package/services/torrent-worker/worker.js +8 -1
  17. package/services/viewer/positions.js +48 -0
  18. package/test/audio-inventory.test.js +176 -176
  19. package/test/auto-quality-step.test.js +514 -514
  20. package/test/concurrent-cost.test.js +138 -138
  21. package/test/coverage-follows-the-disk.test.js +191 -191
  22. package/test/coverage-map.test.js +195 -195
  23. package/test/declared-tracks.test.js +35 -35
  24. package/test/disk-space.test.js +150 -0
  25. package/test/encode-orchestrator.test.js +0 -3
  26. package/test/encode-run.test.js +5 -12
  27. package/test/held-request-width.test.js +155 -155
  28. package/test/helpers/encode-run.js +2 -2
  29. package/test/matroska-blocks.test.js +0 -0
  30. package/test/matroska-cues-track.test.js +192 -192
  31. package/test/mp4-composition-times.test.js +0 -0
  32. package/test/mp4-subtitles.test.js +173 -173
  33. package/test/one-authority.test.js +281 -220
  34. package/test/orchestrator-wired.test.js +199 -199
  35. package/test/packet-witness-ring.test.js +236 -236
  36. package/test/packet-witness.test.js +148 -148
  37. package/test/piece-disk-store.test.js +267 -0
  38. package/test/piece-reader.test.js +4 -4
  39. package/test/piece-store-eviction.test.js +17 -17
  40. package/test/piece-store-reservations.test.js +20 -1
  41. package/test/piece-store-slow-disk.test.js +16 -1
  42. package/test/read-window.test.js +6 -6
  43. package/test/run-intervals.test.js +100 -100
  44. package/test/seek-landing.test.js +109 -109
  45. package/test/segment-store-eviction.test.js +232 -0
  46. package/test/segments-are-shared.test.js +1 -1
  47. package/test/shared-piece-store.test.js +12 -12
  48. package/test/sidecar-naming.test.js +142 -142
  49. package/test/subtitle-cue-framing.test.js +200 -200
  50. package/test/subtitle-cue-walk.test.js +369 -369
  51. package/test/subtitle-defaults.test.js +97 -97
  52. package/test/subtitle-track-numbering.test.js +370 -370
  53. package/test/tail-duplication.test.js +167 -167
  54. package/test/tracks-begin-together.test.js +195 -195
  55. package/test/two-viewers-one-picture.test.js +374 -374
  56. package/test/video-facts.test.js +102 -102
  57. package/test/wedge-certainty.test.js +131 -131
  58. package/services/piece-store/disk-tier.js +0 -151
@@ -1,220 +1,281 @@
1
- /**
2
- * @file One place decides which encoders exist.
3
- *
4
- * Not a check of behaviour but of shape, and it is here because the shape is
5
- * what failed. Three separate places used to decide where an encoder should
6
- * work and whether it should go on living, and they disagreed on every pass:
7
- * measured in the field on 2026-09-05, 684 starts and 660 stops in 482 seconds,
8
- * of which 294 were one place killing what another had just decided to keep,
9
- * while the viewer's own segment went unmade for 32.3 seconds.
10
- *
11
- * Every rule below is one that was broken then. A reader who needs to add a
12
- * fourth place should read this file first and then not.
13
- */
14
-
15
- import test from "node:test";
16
- import assert from "node:assert/strict";
17
- import { readFileSync } from "node:fs";
18
- import path from "node:path";
19
- import { fileURLToPath } from "node:url";
20
-
21
- const HERE = path.dirname(fileURLToPath(import.meta.url));
22
-
23
- /**
24
- * @param {string} relative
25
- * @returns {string}
26
- */
27
- function source(relative) {
28
- return readFileSync(path.join(HERE, "..", relative), "utf8");
29
- }
30
-
31
- /**
32
- * Lines of code, without comments or blanks: a rule about what the code does
33
- * must not be answered by what a comment says about it.
34
- *
35
- * @param {string} text
36
- * @returns {string[]}
37
- */
38
- function statements(text) {
39
- return text
40
- .split("\n")
41
- .map((line) => line.trim())
42
- .filter((line) => line.length > 0 && !line.startsWith("//") && !line.startsWith("*") && !line.startsWith("/*"));
43
- }
44
-
45
- test("an encoder is stopped for scheduling reasons in exactly one place", () => {
46
- // The orchestrator decides; nobody else may. What is left in the session
47
- // manager is teardown the session is going away and its encoders with it —
48
- // which is not a decision about which encoders should exist.
49
- const orchestrator = statements(source("services/orchestrators/EncodeOrchestrator.js"));
50
- const stopsInOrchestrator = orchestrator.filter((line) => line.includes("run.stop("));
51
- assert.equal(stopsInOrchestrator.length, 1, "the orchestrator stops runs in one place");
52
-
53
- const manager = statements(source("services/hls-session-manager.js"));
54
- const stopsInManager = manager.filter((line) => line.includes(".stop("));
55
- assert.equal(
56
- stopsInManager.length,
57
- 2,
58
- "the session manager stops runs only when a session is torn down: " +
59
- stopsInManager.join(" / ")
60
- );
61
- });
62
-
63
- test("nothing outside the encoding layer starts an encoder", () => {
64
- // A run is built in one place. Two places building them is how a start came
65
- // to kill what the plan had decided to keep the killing lived in the
66
- // building.
67
- const manager = statements(source("services/hls-session-manager.js"));
68
- const builds = manager.filter((line) => line.includes("new EncodeRun("));
69
- assert.equal(builds.length, 1, "one place builds an encoder");
70
- });
71
-
72
- test("starting an encoder stops nothing", () => {
73
- // The rule that broke it: the start path looked for a live run whose own
74
- // start was not below the new one's and killed it. It is not enough that the
75
- // line is gone the words it was written with must not come back.
76
- const manager = source("services/hls-session-manager.js");
77
- assert.equal(
78
- manager.includes("previousRun"),
79
- false,
80
- "there is no such thing as the previous run: a session holds several"
81
- );
82
- assert.equal(manager.includes("a new run is taking its place"), false);
83
- });
84
-
85
- test("a seek moves the viewer and nothing else", () => {
86
- // It used to do eleven things and write the position into five places. What
87
- // follows from a viewer moving is the map's business, and the orchestrators
88
- // read the map.
89
- const manager = source("services/hls-session-manager.js");
90
- const seek = manager.slice(
91
- manager.indexOf("requestSeek(sessionId, positionSeconds"),
92
- manager.indexOf("requestSeek(sessionId, positionSeconds") + 2000
93
- );
94
- const body = seek.slice(0, seek.indexOf("\n }\n"));
95
- assert.equal(body.includes("#startEncodeRun"), false, "a seek starts no encoder");
96
- assert.equal(body.includes("setTimeout"), false, "and waits for nothing");
97
- });
98
-
99
- test("how far an encoder may work is answered once", () => {
100
- // The plan computes the stretch and it reaches ffmpeg. A second computation
101
- // somewhere else is what made the first one pointless: it was passed and then
102
- // dropped by a parameter list that did not name it.
103
- const orchestrator = source("services/orchestrators/EncodeOrchestrator.js");
104
- assert.match(
105
- orchestrator,
106
- /makeRun\(\{ address, from, to, because \}\)/,
107
- "the stretch is handed over, and so are the plan's own words for why"
108
- );
109
- });
110
-
111
- test("only the plan places an encoder", () => {
112
- // The count is the whole of this item. There were eight other places: the
113
- // first run of a session, a viewer joining it further in, a rung or a
114
- // soundtrack being warmed, one being switched to, a hardware encoder falling
115
- // back to software, an input coming back, a cut table correcting itself, and
116
- // a settled seek. Each of them chose a position by a rule of its own, and the
117
- // plan which is arithmetic over what is made, what is being made and what is
118
- // wanted was left to compare its answer against theirs.
119
- const manager = source("services/hls-session-manager.js");
120
- const starts = [...manager.matchAll(/this\.#startEncodeRun\(/g)].length;
121
- assert.equal(starts, 1, "one caller, and it is the one the plan asks through");
122
- assert.match(
123
- manager,
124
- /#makeRunAt\(address, from, to, because\)[\s\S]{0,3000}this\.#startEncodeRun\(base, from, because, \{ to \}\)/,
125
- "and that caller is what the plan is given to build runs with"
126
- );
127
- });
128
-
129
- test("nobody stops an encoder for being unwatched", () => {
130
- // Whether an encoder is still wanted is the same question as where one should
131
- // be, and the plan answers it: an output with nobody on it has a priority map
132
- // with nothing in it. Answered here as well, it was answered twice by two
133
- // rules and since a viewer moving between steps announces itself, the plan
134
- // started again what this class had just killed, several times a second.
135
- const manager = source("services/hls-session-manager.js");
136
- assert.equal(
137
- manager.includes("no viewer is watching"),
138
- false,
139
- "a rung nobody is on is a fact, not an act"
140
- );
141
- assert.equal(manager.includes("no viewer is listening to audio track"), false);
142
- assert.equal(
143
- manager.includes("warmed for a switch the viewer did not make"),
144
- false,
145
- "an abandoned warm-up is a viewer leaving an output"
146
- );
147
- assert.equal(manager.includes("prepared for a track change the viewer did not make"), false);
148
- });
149
-
150
- test("the seek settle machinery is gone, whole", () => {
151
- // A viewer's position is applied the moment they state it. The settle was a
152
- // second debounce on a signal the browser had already debounced, and every
153
- // millisecond of it was dead time in front of the viewer; the cooldown behind
154
- // it existed because segment REQUESTS once steered the encoder.
155
- const manager = source("services/hls-session-manager.js");
156
- for (const gone of [
157
- "seekSettleTimer",
158
- "seekTarget",
159
- "seekFirstFarAt",
160
- "SEEK_SETTLE_MS",
161
- "SEEK_SETTLE_MAX_MS",
162
- "RESTART_COOLDOWN_MS",
163
- "SEEK_BACKOFF_SEGMENTS",
164
- "#fireSettledSeek",
165
- "#seekSession"
166
- ]) {
167
- assert.equal(manager.includes(gone), false, `${gone} is gone`);
168
- }
169
- });
170
-
171
- test("where a soundtrack begins is read off the table, not handed in", () => {
172
- // The instant a number really begins is a fact of the FILE's cutting, held in
173
- // the live table every session of the file shares. Passed as an argument by
174
- // the one caller that had measured it, only a run started by that caller ever
175
- // had it, and a run the plan placed at the same number landed apart again.
176
- const manager = source("services/hls-session-manager.js");
177
- assert.match(
178
- manager,
179
- /const positionSecondsOverride = session\.audioOnly === true\s*\n?\s*\? trueStartOf\(session\.timeline, startIndex\)/,
180
- "derived where the run is built"
181
- );
182
- assert.equal(
183
- manager.includes("this.#startEncodeRun(member, index, trueStart)"),
184
- false,
185
- "and not carried in from the correction that measured it"
186
- );
187
- });
188
-
189
- test("a changed bitrate cap stops the encoder carrying the old one and nothing more", () => {
190
- // An argument list is fixed when a process starts, so a run carrying the
191
- // previous cap cannot be told about the new one — that is what is known here.
192
- // Where the replacement stands is a different question, and the old answer to
193
- // it was neither where a viewer is nor a gap in the material: it was the
194
- // segment the process being replaced happened to have reached.
195
- const manager = source("services/hls-session-manager.js");
196
- assert.equal(manager.includes("#restartAtViewer"), false, "the second answer is gone");
197
- assert.match(
198
- manager,
199
- /#reencodeAtNewRate\(session\) \{\s*\n\s*this\.#stopEncodeRun\(session, "its bitrate cap changed"\);\s*\n\s*this\.planEncodersSoon\(\);\s*\n\s*\}/,
200
- "stopped, and then decided again"
201
- );
202
- });
203
-
204
- test("each output is handed its own priority map, and the plan is what reads it", () => {
205
- // The map is one fact asked at two scopes, and both are right for what asks
206
- // them: the swarm is asked for bytes of a FILE, which every output of it
207
- // reads, and encoders are placed per OUTPUT, which a person watching 480p
208
- // wants nothing of at 1080p.
209
- const manager = source("services/hls-session-manager.js");
210
- assert.match(
211
- manager,
212
- /notePriorityMap\([\s\S]{0,200}mapForOutput\(address\)/,
213
- "the encoding reads the output's own map"
214
- );
215
- assert.equal(
216
- manager.includes("this.priority.mapFor("),
217
- false,
218
- "and never the whole film's, which wanted an encoder on every output of it"
219
- );
220
- });
1
+ /**
2
+ * @file One place decides which encoders exist.
3
+ *
4
+ * Not a check of behaviour but of shape, and it is here because the shape is
5
+ * what failed. Three separate places used to decide where an encoder should
6
+ * work and whether it should go on living, and they disagreed on every pass:
7
+ * measured in the field on 2026-09-05, 684 starts and 660 stops in 482 seconds,
8
+ * of which 294 were one place killing what another had just decided to keep,
9
+ * while the viewer's own segment went unmade for 32.3 seconds.
10
+ *
11
+ * Every rule below is one that was broken then. A reader who needs to add a
12
+ * fourth place should read this file first and then not.
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { readFileSync } from "node:fs";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { EventEmitter } from "node:events";
21
+ import { EncodeRun } from "../services/encode/EncodeRun.js";
22
+
23
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
24
+
25
+ /**
26
+ * @param {string} relative
27
+ * @returns {string}
28
+ */
29
+ function source(relative) {
30
+ return readFileSync(path.join(HERE, "..", relative), "utf8");
31
+ }
32
+
33
+ /**
34
+ * Lines of code, without comments or blanks: a rule about what the code does
35
+ * must not be answered by what a comment says about it.
36
+ *
37
+ * @param {string} text
38
+ * @returns {string[]}
39
+ */
40
+ function statements(text) {
41
+ return text
42
+ .split("\n")
43
+ .map((line) => line.trim())
44
+ .filter((line) => line.length > 0 && !line.startsWith("//") && !line.startsWith("*") && !line.startsWith("/*"));
45
+ }
46
+
47
+ test("an encoder is stopped for scheduling reasons in exactly one place", () => {
48
+ // The orchestrator decides; nobody else may. What is left in the session
49
+ // manager is teardown — the session is going away and its encoders with it —
50
+ // which is not a decision about which encoders should exist.
51
+ const orchestrator = statements(source("services/orchestrators/EncodeOrchestrator.js"));
52
+ const stopsInOrchestrator = orchestrator.filter((line) => line.includes("run.stop("));
53
+ assert.equal(stopsInOrchestrator.length, 1, "the orchestrator stops runs in one place");
54
+
55
+ const manager = statements(source("services/hls-session-manager.js"));
56
+ const stopsInManager = manager.filter((line) => line.includes(".stop("));
57
+ assert.equal(
58
+ stopsInManager.length,
59
+ 2,
60
+ "the session manager stops runs only when a session is torn down: " +
61
+ stopsInManager.join(" / ")
62
+ );
63
+ });
64
+
65
+ test("nothing outside the encoding layer starts an encoder", () => {
66
+ // A run is built in one place. Two places building them is how a start came
67
+ // to kill what the plan had decided to keep — the killing lived in the
68
+ // building.
69
+ const manager = statements(source("services/hls-session-manager.js"));
70
+ const builds = manager.filter((line) => line.includes("new EncodeRun("));
71
+ assert.equal(builds.length, 1, "one place builds an encoder");
72
+ });
73
+
74
+ test("a run exists means its process is running, so nothing can start one twice", () => {
75
+ // WHAT THIS IS FOR, and why the test above did not catch it. That one counts
76
+ // where a run is BUILT and asks nothing about where it is STARTED, and until
77
+ // 2026-09-10 those were two acts: the session manager built a run and started
78
+ // it, handed it back, and the orchestrator started it again. Every run of
79
+ // every session therefore had two ffmpeg processes on one output writing one
80
+ // set of names 207 runs against 414 spawns in the field logs of 08-10
81
+ // September, without a single exception — and only the second was reachable,
82
+ // so a stop killed one and the other ran on, measured 105 seconds past its
83
+ // own run's death.
84
+ //
85
+ // Asserted over the source because the guarantee IS a property of the source:
86
+ // there is no second act for a second owner to perform.
87
+ const files = [
88
+ "services/encode/EncodeRun.js",
89
+ "services/orchestrators/EncodeOrchestrator.js",
90
+ "services/hls-session-manager.js"
91
+ ];
92
+ for (const file of files) {
93
+ const starts = statements(source(file)).filter(
94
+ (line) =>
95
+ /(?:^|[^a-zA-Z#])start\(/.test(line) &&
96
+ !/startsWith|startPosition|startIndex|startSeconds|startAt|startNumber/.test(line)
97
+ );
98
+ assert.deepEqual(starts, [], "an encoder is started by hand in " + file + ": " + starts.join(" / "));
99
+ }
100
+ });
101
+
102
+ test("starting an encoder stops nothing", () => {
103
+ // The rule that broke it: the start path looked for a live run whose own
104
+ // start was not below the new one's and killed it. It is not enough that the
105
+ // line is gone — the words it was written with must not come back.
106
+ const manager = source("services/hls-session-manager.js");
107
+ assert.equal(
108
+ manager.includes("previousRun"),
109
+ false,
110
+ "there is no such thing as the previous run: a session holds several"
111
+ );
112
+ assert.equal(manager.includes("a new run is taking its place"), false);
113
+ });
114
+
115
+ test("a seek moves the viewer and nothing else", () => {
116
+ // It used to do eleven things and write the position into five places. What
117
+ // follows from a viewer moving is the map's business, and the orchestrators
118
+ // read the map.
119
+ const manager = source("services/hls-session-manager.js");
120
+ const seek = manager.slice(
121
+ manager.indexOf("requestSeek(sessionId, positionSeconds"),
122
+ manager.indexOf("requestSeek(sessionId, positionSeconds") + 2000
123
+ );
124
+ const body = seek.slice(0, seek.indexOf("\n }\n"));
125
+ assert.equal(body.includes("#startEncodeRun"), false, "a seek starts no encoder");
126
+ assert.equal(body.includes("setTimeout"), false, "and waits for nothing");
127
+ });
128
+
129
+ test("how far an encoder may work is answered once", () => {
130
+ // The plan computes the stretch and it reaches ffmpeg. A second computation
131
+ // somewhere else is what made the first one pointless: it was passed and then
132
+ // dropped by a parameter list that did not name it.
133
+ const orchestrator = source("services/orchestrators/EncodeOrchestrator.js");
134
+ assert.match(
135
+ orchestrator,
136
+ /makeRun\(\{ address, from, to, because \}\)/,
137
+ "the stretch is handed over, and so are the plan's own words for why"
138
+ );
139
+ });
140
+
141
+ test("only the plan places an encoder", () => {
142
+ // The count is the whole of this item. There were eight other places: the
143
+ // first run of a session, a viewer joining it further in, a rung or a
144
+ // soundtrack being warmed, one being switched to, a hardware encoder falling
145
+ // back to software, an input coming back, a cut table correcting itself, and
146
+ // a settled seek. Each of them chose a position by a rule of its own, and the
147
+ // plan which is arithmetic over what is made, what is being made and what is
148
+ // wanted — was left to compare its answer against theirs.
149
+ const manager = source("services/hls-session-manager.js");
150
+ const starts = [...manager.matchAll(/this\.#startEncodeRun\(/g)].length;
151
+ assert.equal(starts, 1, "one caller, and it is the one the plan asks through");
152
+ assert.match(
153
+ manager,
154
+ /#makeRunAt\(address, from, to, because\)[\s\S]{0,3000}this\.#startEncodeRun\(base, from, because, \{ to \}\)/,
155
+ "and that caller is what the plan is given to build runs with"
156
+ );
157
+ });
158
+
159
+ test("nobody stops an encoder for being unwatched", () => {
160
+ // Whether an encoder is still wanted is the same question as where one should
161
+ // be, and the plan answers it: an output with nobody on it has a priority map
162
+ // with nothing in it. Answered here as well, it was answered twice by two
163
+ // rules — and since a viewer moving between steps announces itself, the plan
164
+ // started again what this class had just killed, several times a second.
165
+ const manager = source("services/hls-session-manager.js");
166
+ assert.equal(
167
+ manager.includes("no viewer is watching"),
168
+ false,
169
+ "a rung nobody is on is a fact, not an act"
170
+ );
171
+ assert.equal(manager.includes("no viewer is listening to audio track"), false);
172
+ assert.equal(
173
+ manager.includes("warmed for a switch the viewer did not make"),
174
+ false,
175
+ "an abandoned warm-up is a viewer leaving an output"
176
+ );
177
+ assert.equal(manager.includes("prepared for a track change the viewer did not make"), false);
178
+ });
179
+
180
+ test("the seek settle machinery is gone, whole", () => {
181
+ // A viewer's position is applied the moment they state it. The settle was a
182
+ // second debounce on a signal the browser had already debounced, and every
183
+ // millisecond of it was dead time in front of the viewer; the cooldown behind
184
+ // it existed because segment REQUESTS once steered the encoder.
185
+ const manager = source("services/hls-session-manager.js");
186
+ for (const gone of [
187
+ "seekSettleTimer",
188
+ "seekTarget",
189
+ "seekFirstFarAt",
190
+ "SEEK_SETTLE_MS",
191
+ "SEEK_SETTLE_MAX_MS",
192
+ "RESTART_COOLDOWN_MS",
193
+ "SEEK_BACKOFF_SEGMENTS",
194
+ "#fireSettledSeek",
195
+ "#seekSession"
196
+ ]) {
197
+ assert.equal(manager.includes(gone), false, `${gone} is gone`);
198
+ }
199
+ });
200
+
201
+ test("where a soundtrack begins is read off the table, not handed in", () => {
202
+ // The instant a number really begins is a fact of the FILE's cutting, held in
203
+ // the live table every session of the file shares. Passed as an argument by
204
+ // the one caller that had measured it, only a run started by that caller ever
205
+ // had it, and a run the plan placed at the same number landed apart again.
206
+ const manager = source("services/hls-session-manager.js");
207
+ assert.match(
208
+ manager,
209
+ /const positionSecondsOverride = session\.audioOnly === true\s*\n?\s*\? trueStartOf\(session\.timeline, startIndex\)/,
210
+ "derived where the run is built"
211
+ );
212
+ assert.equal(
213
+ manager.includes("this.#startEncodeRun(member, index, trueStart)"),
214
+ false,
215
+ "and not carried in from the correction that measured it"
216
+ );
217
+ });
218
+
219
+ test("a changed bitrate cap stops the encoder carrying the old one and nothing more", () => {
220
+ // An argument list is fixed when a process starts, so a run carrying the
221
+ // previous cap cannot be told about the new one — that is what is known here.
222
+ // Where the replacement stands is a different question, and the old answer to
223
+ // it was neither where a viewer is nor a gap in the material: it was the
224
+ // segment the process being replaced happened to have reached.
225
+ const manager = source("services/hls-session-manager.js");
226
+ assert.equal(manager.includes("#restartAtViewer"), false, "the second answer is gone");
227
+ assert.match(
228
+ manager,
229
+ /#reencodeAtNewRate\(session\) \{\s*\n\s*this\.#stopEncodeRun\(session, "its bitrate cap changed"\);\s*\n\s*this\.planEncodersSoon\(\);\s*\n\s*\}/,
230
+ "stopped, and then decided again"
231
+ );
232
+ });
233
+
234
+ test("each output is handed its own priority map, and the plan is what reads it", () => {
235
+ // The map is one fact asked at two scopes, and both are right for what asks
236
+ // them: the swarm is asked for bytes of a FILE, which every output of it
237
+ // reads, and encoders are placed per OUTPUT, which a person watching 480p
238
+ // wants nothing of at 1080p.
239
+ const manager = source("services/hls-session-manager.js");
240
+ assert.match(
241
+ manager,
242
+ /notePriorityMap\([\s\S]{0,200}mapForOutput\(address\)/,
243
+ "the encoding reads the output's own map"
244
+ );
245
+ assert.equal(
246
+ manager.includes("this.priority.mapFor("),
247
+ false,
248
+ "and never the whole film's, which wanted an encoder on every output of it"
249
+ );
250
+ });
251
+
252
+ test("one process per run, and no way to ask for a second", () => {
253
+ const spawned = [];
254
+ const run = new EncodeRun({
255
+ address: "torrent:abc:video-only:v=0/copy",
256
+ encoder: { name: "libx264", kind: "software" },
257
+ from: 0,
258
+ to: 4,
259
+ buildArgs: () => [],
260
+ spawn: () => {
261
+ const child = new EventEmitter();
262
+ child.stdout = new EventEmitter();
263
+ child.stderr = new EventEmitter();
264
+ child.stdio = [null, child.stdout, child.stderr, new EventEmitter()];
265
+ child.kill = () => undefined;
266
+ child.pid = 1 + spawned.length;
267
+ spawned.push(child);
268
+ return child;
269
+ },
270
+ logger: { info: () => undefined, warn: () => undefined },
271
+ now: () => 1000,
272
+ because: "a test asked for it"
273
+ });
274
+
275
+ assert.equal(spawned.length, 1, "building a run did not put a process on the machine");
276
+ assert.equal(
277
+ Reflect.get(run, "start"),
278
+ undefined,
279
+ "a run still offers a way to start it a second time"
280
+ );
281
+ });