@torrent-tv/proxy 2.78.0 → 2.80.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.
@@ -14,7 +14,19 @@ import { endOfRun } from "../services/encode/EncodeRun.js";
14
14
  import { firstUnmetWant, planEncoders } from "../services/encode/EncodePlan.js";
15
15
 
16
16
  /** A host that can afford two encoders, four-second segments, a cheap restart. */
17
- const HOST = { maxRuns: 2, segmentSeconds: 4, restartCostSec: 0.12 };
17
+ // A host that has measured itself: the start and the death from its own runs,
18
+ // and what the swarm charges to fetch a second of film again. All four terms
19
+ // have to be present for the drive-or-move comparison to mean anything, and a
20
+ // host missing any of them keeps its encoders instead — which is its own check
21
+ // below rather than the shape every other check is written against.
22
+ const HOST = {
23
+ maxRuns: 2,
24
+ segmentSeconds: 4,
25
+ restartCostSec: 0.12,
26
+ killCostSec: 0.5,
27
+ firstByteWaitSec: 1,
28
+ refetchSecPerFilmSecond: 0.25
29
+ };
18
30
 
19
31
  /**
20
32
  * @param {Partial<import("../services/encode/EncodePlan.js").LiveRun>} run
@@ -121,9 +133,13 @@ test("a covered stretch shorter than a restart is driven through instead", () =>
121
133
  assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
122
134
  });
123
135
 
124
- test("a run whose speed nothing has measured is moved rather than left driving through", () => {
125
- // Not a default: driving through is work that is certainly wasted, and the
126
- // restart is a known and small cost. What cannot be done is the comparison.
136
+ test("a run whose speed nothing has measured is kept, not taken away", () => {
137
+ // Moving costs a known amount for an unknown gain, and a run nothing has
138
+ // measured has produced nothing yet so taking its work away is certainly a
139
+ // loss and the comparison cannot be made. It used to answer the other way,
140
+ // and then every just-started run was moved the moment anything ahead of it
141
+ // was covered, which, once the film ahead had been made, was always: 684
142
+ // starts in 482 seconds in the field on 2026-09-05.
127
143
  const coverage = new CoverageMap({ segmentCount: 100 });
128
144
  const runA = run({ head: 10, speedX: 0 });
129
145
  coverage.claim(runA, 0, 100);
@@ -134,9 +150,55 @@ test("a run whose speed nothing has measured is moved rather than left driving t
134
150
  runs: [runA],
135
151
  ...HOST
136
152
  });
153
+ assert.equal(actions.some((action) => action.type === "move"), false);
154
+ assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
155
+ });
156
+
157
+ test("both sides of the move are counted, not just the encoder's own time", () => {
158
+ // Driving through costs this run's encode time AND the swarm the same bytes a
159
+ // second time; moving costs the death, the start and the wait for the first
160
+ // bytes. Here driving is dear enough to lose: 20 covered segments of 4 s at
161
+ // 1x is 80 s of encoding, against a move priced at 0.12 + 0.5 + 3 seconds.
162
+ const coverage = new CoverageMap({ segmentCount: 200 });
163
+ const runA = run({ head: 10, speedX: 1 });
164
+ coverage.claim(runA, 0, 200);
165
+ for (let at = 10; at < 30; at += 1) {
166
+ coverage.markReady(at);
167
+ }
168
+ const actions = planEncoders({
169
+ coverage,
170
+ windows: [{ from: 0, to: 190 }],
171
+ runs: [runA],
172
+ ...HOST,
173
+ killCostSec: 0.5,
174
+ firstByteWaitSec: 3,
175
+ refetchSecPerFilmSecond: 0.25
176
+ });
137
177
  const move = actions.find((action) => action.type === "move");
138
178
  assert.ok(move);
139
- assert.match(move.because, /speed is not measured/);
179
+ assert.match(move.because, /refetch 20\.00s/);
180
+ assert.match(move.because, /against 3\.62s to move/);
181
+ });
182
+
183
+ test("a short covered stretch is driven through rather than paid a restart for", () => {
184
+ // One covered segment at 1x is 4 s of encoding plus 1 s of refetch, against a
185
+ // move priced at 0.12 + 0.5 + 30 seconds on a host where the first bytes are
186
+ // slow to come. The comparison, not a rule, decides it.
187
+ const coverage = new CoverageMap({ segmentCount: 200 });
188
+ const runA = run({ head: 10, speedX: 1 });
189
+ coverage.claim(runA, 0, 200);
190
+ coverage.markReady(10);
191
+ const actions = planEncoders({
192
+ coverage,
193
+ windows: [{ from: 0, to: 190 }],
194
+ runs: [runA],
195
+ ...HOST,
196
+ killCostSec: 0.5,
197
+ firstByteWaitSec: 30,
198
+ refetchSecPerFilmSecond: 0.25
199
+ });
200
+ assert.equal(actions.some((action) => action.type === "move"), false);
201
+ assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
140
202
  });
141
203
 
142
204
  test("a run with nothing left to make ahead of it is stopped", () => {
@@ -0,0 +1,105 @@
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.ok(orchestrator.includes("makeRun({ address, from, to })"), "the stretch is handed over");
105
+ });
@@ -8,7 +8,7 @@
8
8
 
9
9
  import test from "node:test";
10
10
  import assert from "node:assert/strict";
11
- import { mapForViewer, mergeMaps, inWorkingOrder } from "../services/encode/DemandMap.js";
11
+ import { mapForViewer, mergeMaps, inWorkingOrder } from "../services/priority/PriorityMap.js";
12
12
 
13
13
  test("a viewer's own position is the most urgent thing there is", () => {
14
14
  const map = mapForViewer({
@@ -25,23 +25,60 @@ test("a viewer's own position is the most urgent thing there is", () => {
25
25
  }
26
26
  });
27
27
 
28
- test("a machine that cannot keep up must have more ready before the viewer sets off", () => {
29
- // 900s of film in front. At 0.25x the encoder loses three seconds of film per
30
- // second played, so 675s must exist first, or the viewer stalls partway.
31
- const slow = mapForViewer({ atSeconds: 100, durationSeconds: 1000, allowanceSeconds: 0, encodeSpeedX: 0.25 });
32
- const fast = mapForViewer({ atSeconds: 100, durationSeconds: 1000, allowanceSeconds: 0, encodeSpeedX: 2 });
28
+ test("a machine that cannot keep up needs more encoders, not a longer first zone", () => {
29
+ // An encoder starting where the viewer stands stays ahead of them for
30
+ // `held x s / (1 - s)`, so the SLOWER the machine the shorter that is — and
31
+ // the film is held by more encoders rather than by one working further.
32
+ const slow = mapForViewer({
33
+ atSeconds: 0,
34
+ durationSeconds: 1000,
35
+ allowanceSeconds: 10,
36
+ encodeSpeedX: 0.25
37
+ });
38
+ const quicker = mapForViewer({
39
+ atSeconds: 0,
40
+ durationSeconds: 1000,
41
+ allowanceSeconds: 10,
42
+ encodeSpeedX: 0.5
43
+ });
33
44
 
34
- assert.equal(slow[0].to, 775, "100 + 900 x 0.75");
35
45
  assert.ok(
36
- slow[0].to - slow[0].from > fast[0].to - fast[0].from,
37
- "the slower the machine, the more of the film must be made in advance"
46
+ slow[0].to - slow[0].from < quicker[0].to - quicker[0].from,
47
+ "the slower machine holds the viewer for less"
38
48
  );
49
+ assert.ok(slow.length > quicker.length, "so more encoders are needed to hold the film");
50
+ });
51
+
52
+ test("the first zone is exactly what one encoder can hold", () => {
53
+ // `held x s / (1 - s)`: ten seconds held at half speed is ten seconds held,
54
+ // and the encoder is caught exactly there. A longer first zone would stall
55
+ // the viewer inside it; a shorter one would start the next encoder nearer,
56
+ // where its own bound is tighter.
57
+ const map = mapForViewer({
58
+ atSeconds: 0,
59
+ durationSeconds: 100,
60
+ allowanceSeconds: 10,
61
+ encodeSpeedX: 0.5
62
+ });
63
+
64
+ assert.equal(map[0].to, 10);
65
+ assert.equal(map[1].to, 30, "the next holds three times as long, because they arrive later");
66
+ assert.equal(map[2].to, 70);
39
67
  });
40
68
 
41
- test("the allowance is added to the shortfall, not chosen instead of it", () => {
42
- const map = mapForViewer({ atSeconds: 0, durationSeconds: 100, allowanceSeconds: 10, encodeSpeedX: 0.5 });
69
+ test("a viewer holding nothing is told the whole of it is urgent", () => {
70
+ // With nothing held, no partition holds them: the first encoder stays ahead
71
+ // for zero seconds. Slicing the film into equal slivers would pretend
72
+ // otherwise.
73
+ const map = mapForViewer({
74
+ atSeconds: 0,
75
+ durationSeconds: 1000,
76
+ allowanceSeconds: 0,
77
+ encodeSpeedX: 0.25
78
+ });
43
79
 
44
- assert.equal(map[0].to, 60, "50s of shortfall plus 10s of measured allowance");
80
+ assert.equal(map.length, 1);
81
+ assert.equal(map[0].to, 1000);
45
82
  });
46
83
 
47
84
  test("the rest of the track is still wanted, and wanted last", () => {
@@ -62,7 +62,7 @@ async function waitFor(check) {
62
62
  }
63
63
  }
64
64
 
65
- test("gdb is invoked attached to this process with the bundled script", async () => {
65
+ test("the deadline is counted outside the process gdb stops", async () => {
66
66
  const { spawnProcess, calls } = makeSpawn("state=8 rwnd=95890\n");
67
67
  const lines = [];
68
68
  const reader = createUsrsctpStateReader({
@@ -74,7 +74,23 @@ test("gdb is invoked attached to this process with the bundled script", async ()
74
74
  assert.equal(started, true);
75
75
  await waitFor(() => lines.length > 0);
76
76
  assert.equal(calls.length, 1);
77
- assert.deepEqual(calls[0].args, ["-q", "-batch", "-p", "4242", "-x", SCTPSTATE_SCRIPT_PATH]);
77
+ // `timeout` is a separate process, so it keeps counting while gdb holds every
78
+ // thread of this one. A `setTimeout` here cannot fire — measured in the field
79
+ // on 2026-09-05, where gdb held the proxy for four minutes and the guard set
80
+ // for fifteen seconds never ran.
81
+ assert.equal(calls[0].command, "timeout");
82
+ assert.deepEqual(calls[0].args, [
83
+ "-s",
84
+ "KILL",
85
+ "15",
86
+ "gdb",
87
+ "-q",
88
+ "-batch",
89
+ "-p",
90
+ "4242",
91
+ "-x",
92
+ SCTPSTATE_SCRIPT_PATH
93
+ ]);
78
94
  assert.match(lines[0], /state=8 rwnd=95890/);
79
95
  assert.match(lines[0], /test wedge/);
80
96
  });