@torrent-tv/proxy 2.77.0 → 2.79.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.
@@ -45,6 +45,12 @@ function orchestrator({ maxRuns = 2 } = {}) {
45
45
  maxRunsFor: () => maxRuns,
46
46
  segmentSeconds: 4,
47
47
  restartCostSec: 0.12,
48
+ // A host that has measured what the swarm charges to fetch a second of film
49
+ // again. Without it the drive-or-move comparison has only one side and the
50
+ // plan keeps the encoder rather than paying an unknown price — which is its
51
+ // own check in `encode-plan.test.js` rather than the shape every check here
52
+ // is written against.
53
+ refetchSecPerFilmSecond: () => 0.25,
48
54
  now: () => 1000,
49
55
  logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) },
50
56
  makeRun: ({ address, from, to }) => {
@@ -195,22 +201,25 @@ test("nothing wanted anywhere is said plainly", () => {
195
201
  assert.match(made.describe(), /nothing wanted/);
196
202
  });
197
203
 
198
- test("a run adopted with no end holds the look-ahead, not the rest of the film", () => {
204
+ test("a run adopted with no end holds what it has made, not the rest of the film", () => {
199
205
  // A session's own encoder is handed to the plan rather than built by it, and
200
- // it carries `to = -1` — no end. Claiming the film from there would leave a
201
- // viewer further in with no encoder at all: they would wait for this run to
202
- // encode its way to them, which on a long film is an hour. What bounds it is
203
- // the look-ahead, because a run is suspended once it is that far in front of
204
- // its viewer and produces nothing until somebody asks.
206
+ // it carries `to = -1` — no end, which means the film's length is not known.
207
+ // Claiming the film from there would leave a viewer further in with no
208
+ // encoder at all: they would wait for this run to encode its way to them,
209
+ // which on a long film is an hour. What it holds is what it has produced,
210
+ // which is a fact rather than a distance nobody measured.
205
211
  const { made } = orchestrator({ maxRuns: 2 });
206
- made.lookaheadSegments = 30;
207
212
  const adopted = {
208
213
  from: 0,
209
214
  to: -1,
210
215
  head: 3,
211
216
  isAlive: true,
212
217
  isStopping: false,
213
- speedX: 8,
218
+ // Slow enough that the swarm is not what limits the count here: at 0.25
219
+ // seconds of swarm time per second of film, one encoder at 1x takes a
220
+ // quarter of what is delivered and four may run. The swarm's own limit has
221
+ // its own check below.
222
+ speedX: 1,
214
223
  stop() {
215
224
  this.isAlive = false;
216
225
  }
@@ -225,3 +234,22 @@ test("a run adopted with no end holds the look-ahead, not the rest of the film",
225
234
  assert.ok(adopted.isAlive, "and the adopted run was not stopped to make room");
226
235
  assert.equal(runs.some((run) => run.from === 200), true, "started where that viewer is waiting");
227
236
  });
237
+
238
+ test("the swarm limits the encoders, whatever the processor allows", () => {
239
+ // Every encoder reads the same torrent, so together they cannot consume
240
+ // faster than it is delivered. At 0.25 seconds of swarm time per second of
241
+ // film, one encoder running at 8x takes twice everything there is — so a
242
+ // machine whose processor would allow two gets one.
243
+ const { made, lines } = orchestrator({ maxRuns: 2 });
244
+ made.want({ claimant: "one", address: PICTURE, from: 100, to: 130 });
245
+ made.reconcile();
246
+ made.runsOn(PICTURE)[0].noteSpeed(8);
247
+ made.want({ claimant: "two", address: PICTURE, from: 500, to: 530 });
248
+ made.reconcile();
249
+
250
+ assert.equal(made.runsOn(PICTURE).length, 1);
251
+ assert.ok(
252
+ lines.some((line) => line.includes("what the swarm delivers")),
253
+ "and the line says which limit decided it"
254
+ );
255
+ });
@@ -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", () => {
@@ -176,7 +238,12 @@ test("every encoder stops when nobody is watching the output", () => {
176
238
  );
177
239
  });
178
240
 
179
- test("a run making material nobody asked for is stopped", () => {
241
+ test("a run standing outside every window keeps working: the file is encoded whole", () => {
242
+ // The rule, stated by the user 2026-09-05: while a file is being encoded it
243
+ // is encoded whole, and a viewer decides the ORDER, not whether a run may
244
+ // live. Stopping a run for standing outside a window is what produced the
245
+ // field oscillation of that day — placed by one rule, killed by another,
246
+ // 350-700ms per cycle, nothing ever produced.
180
247
  const coverage = new CoverageMap({ segmentCount: 1000 });
181
248
  const runA = run({ from: 500, to: 600, head: 520 });
182
249
  coverage.claim(runA, 500, 600);
@@ -186,7 +253,44 @@ test("a run making material nobody asked for is stopped", () => {
186
253
  runs: [runA],
187
254
  ...HOST
188
255
  });
189
- assert.ok(actions.some((action) => action.type === "stop" && action.run === runA));
256
+ assert.ok(
257
+ !actions.some((action) => action.type === "stop" && action.run === runA),
258
+ "it is making film that will be wanted, and nothing else is making it"
259
+ );
260
+ });
261
+
262
+ test("the same plan run twice on an unchanged state gives the same answer", () => {
263
+ // What the oscillation actually was: two passes over one state disagreeing
264
+ // with each other. Nothing about the state changes between them here.
265
+ const coverage = new CoverageMap({ segmentCount: 1000 });
266
+ const runA = run({ from: 500, to: 600, head: 520 });
267
+ coverage.claim(runA, 500, 600);
268
+ const input = { coverage, windows: [{ from: 0, to: 40 }], runs: [runA], ...HOST };
269
+
270
+ const first = planEncoders(input).map((action) => action.type);
271
+ const second = planEncoders(input).map((action) => action.type);
272
+
273
+ assert.deepEqual(first, second);
274
+ assert.ok(!first.includes("stop"), "and neither pass kills what the other would start");
275
+ });
276
+
277
+ test("a viewer's most urgent zone is filled before a less urgent one", () => {
278
+ const coverage = new CoverageMap({ segmentCount: 1000 });
279
+ const actions = planEncoders({
280
+ coverage,
281
+ // The far zone is lower in number and lower in priority: the order must
282
+ // come from the priority, not from the number.
283
+ windows: [
284
+ { from: 0, to: 100, priority: 1 },
285
+ { from: 500, to: 530, priority: 3 }
286
+ ],
287
+ runs: [],
288
+ ...HOST,
289
+ maxRuns: 1
290
+ });
291
+ const started = actions.filter((action) => action.type === "start").map((action) => action.from);
292
+
293
+ assert.deepEqual(started, [500], "the one machine goes where somebody is stopped");
190
294
  });
191
295
 
192
296
  test("two viewers far apart get an encoder each, when the machine can hold two", () => {
@@ -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
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @file A map per viewer, merged into one, in the order the work is taken.
3
+ *
4
+ * In seconds of film throughout: it is the only unit every term of the
5
+ * arithmetic is already stated in, and the one two consumers with different
6
+ * states can both be translated from.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mapForViewer, mergeMaps, inWorkingOrder } from "../services/priority/PriorityMap.js";
12
+
13
+ test("a viewer's own position is the most urgent thing there is", () => {
14
+ const map = mapForViewer({
15
+ atSeconds: 100,
16
+ durationSeconds: 1000,
17
+ allowanceSeconds: 8,
18
+ encodeSpeedX: 2
19
+ });
20
+
21
+ assert.equal(map[0].from, 100);
22
+ assert.equal(map[0].to, 108, "above realtime, only the measured allowance");
23
+ for (const zone of map.slice(1)) {
24
+ assert.ok(zone.priority < map[0].priority);
25
+ }
26
+ });
27
+
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
+ });
44
+
45
+ assert.ok(
46
+ slow[0].to - slow[0].from < quicker[0].to - quicker[0].from,
47
+ "the slower machine holds the viewer for less"
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);
67
+ });
68
+
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
+ });
79
+
80
+ assert.equal(map.length, 1);
81
+ assert.equal(map[0].to, 1000);
82
+ });
83
+
84
+ test("the rest of the track is still wanted, and wanted last", () => {
85
+ const map = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 4, encodeSpeedX: 2 });
86
+
87
+ assert.equal(map[map.length - 1].to, 1000, "the map reaches the end of the film");
88
+ assert.ok(map[map.length - 1].priority > 0, "and the far end is still wanted");
89
+ let previousEnd = 0;
90
+ for (const zone of map) {
91
+ assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
92
+ previousEnd = zone.to;
93
+ }
94
+ });
95
+
96
+ test("nothing is measured yet: the middle zone is left out rather than invented", () => {
97
+ const map = mapForViewer({ atSeconds: 0, durationSeconds: 100, allowanceSeconds: 4, encodeSpeedX: 0 });
98
+
99
+ assert.equal(map.length, 2, "what must be ready, and the rest");
100
+ });
101
+
102
+ test("two viewers merge to the highest priority per second, with no overlaps", () => {
103
+ const first = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
104
+ const second = mapForViewer({ atSeconds: 500, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
105
+
106
+ const merged = mergeMaps([first, second]);
107
+
108
+ let previousEnd = merged[0].from;
109
+ for (const zone of merged) {
110
+ assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
111
+ previousEnd = zone.to;
112
+ }
113
+ const priorityAt = (at) => merged.find((zone) => at >= zone.from && at < zone.to)?.priority;
114
+ assert.equal(priorityAt(0), priorityAt(500), "both viewers' own positions are equally urgent");
115
+ assert.ok(priorityAt(500) > priorityAt(300), "a viewer at 500 outranks the far zone of the one at 0");
116
+ });
117
+
118
+ test("the second viewer's position is not buried under the first viewer's far zone", () => {
119
+ // The case that used to leave a viewer opening the same film further in with
120
+ // no encoder at all, because the first run claimed everything in front of it.
121
+ const first = mapForViewer({ atSeconds: 0, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
122
+ const second = mapForViewer({ atSeconds: 2000, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
123
+
124
+ const order = inWorkingOrder(mergeMaps([first, second]));
125
+ const firstTwo = order.slice(0, 2).map((zone) => zone.from);
126
+
127
+ assert.ok(firstTwo.includes(0), "the first viewer's own position is taken first");
128
+ assert.ok(firstTwo.includes(2000), "so is the second viewer's, before anything less urgent");
129
+ });
130
+
131
+ test("within one priority the earliest film goes first — that is where somebody is stopped", () => {
132
+ const order = inWorkingOrder([
133
+ { from: 900, to: 1000, priority: 2 },
134
+ { from: 100, to: 200, priority: 2 },
135
+ { from: 0, to: 10, priority: 3 }
136
+ ]);
137
+
138
+ assert.deepEqual(order.map((zone) => zone.from), [0, 100, 900]);
139
+ });