@torrent-tv/proxy 2.72.1 → 2.73.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.
@@ -156,7 +156,12 @@ test("a spill that fails gives back the slot the eviction claimed", async () =>
156
156
  }
157
157
  });
158
158
 
159
- test("a claim that cannot be met ends in an error, not in waiting for ever", async () => {
159
+ // A bound, because the failure this catches is a claim that never ends: without
160
+ // it the check does not fail, it HANGS, and `node --test` then cannot finish at
161
+ // all — which is what happened between 2026-08-31 and 2026-09-03 (roadmap item
162
+ // 54). The store gives up after PINNED_WAIT_MS, so 30 s is ample for the answer
163
+ // and short enough to be a failure rather than a stoppage.
164
+ test("a claim that cannot be met ends in an error, not in waiting for ever", { timeout: 30_000 }, async () => {
160
165
  const disk = makeDisk({ holdWrites: true });
161
166
  const { store } = makeStore({ pieces: 2, disk });
162
167
  try {
@@ -183,7 +188,7 @@ test("a claim that cannot be met ends in an error, not in waiting for ever", asy
183
188
  }
184
189
  });
185
190
 
186
- test("closing the store fails whoever is waiting for room", async () => {
191
+ test("closing the store fails whoever is waiting for room", { timeout: 30_000 }, async () => {
187
192
  const disk = makeDisk({ holdWrites: true });
188
193
  const { store } = makeStore({ pieces: 2, disk });
189
194
  try {
@@ -194,7 +199,15 @@ test("closing the store fails whoever is waiting for room", async () => {
194
199
  store.pin(1);
195
200
 
196
201
  const waiting = put(store, 3);
197
- await until(() => store.stats().waitedForPins > 0, "the claim is waiting");
202
+ // Either counter: the claim waits for the disk first — a block is in flight
203
+ // and the store is full, so evicting another piece would only raise the
204
+ // memory in use — and reaches the pinned wait five seconds later. Asking for
205
+ // `waitedForPins` alone named one of the two ways of waiting and timed out
206
+ // while the store was demonstrably doing the other.
207
+ await until(
208
+ () => store.stats().waitedForPins + store.stats().waitedForDisk > 0,
209
+ "the claim is waiting"
210
+ );
198
211
 
199
212
  await new Promise((resolve) => store.close(resolve));
200
213
  await assert.rejects(() => waiting, /closed/);
@@ -207,20 +220,30 @@ test("closing the store fails whoever is waiting for room", async () => {
207
220
  }
208
221
  });
209
222
 
210
- test("a piece written back to memory is not resurrected on disk by its own spill", async () => {
223
+ test("a piece written back to memory is not resurrected on disk by its own spill", { timeout: 30_000 }, async () => {
211
224
  const disk = makeDisk({ holdWrites: true });
212
- const { store } = makeStore({ pieces: 3, disk });
225
+ const { store } = makeStore({ pieces: 4, disk });
213
226
  try {
214
227
  await put(store, 0);
215
228
  await put(store, 1);
216
229
  await put(store, 2);
217
230
 
218
231
  // Piece 0 leaves memory because the machine's allowance fell; its write is
219
- // still in flight. The allowance then recovers, so there is room again
220
- // without waiting for that write.
232
+ // still in flight. The allowance then recovers to MORE than it was, so the
233
+ // piece coming back needs no eviction and no wait for that write.
234
+ //
235
+ // Four blocks, not three, and the reason is what this check is about. A
236
+ // block being written out is still memory in use, so with a ceiling of three
237
+ // and one block in flight the store is full, and the claim correctly waits
238
+ // for the disk — which the earlier setup did not allow for, so the check
239
+ // timed out on an admission policy it never meant to measure. Whether a
240
+ // piece arriving while its OWN spill is in flight should be admitted without
241
+ // taking a second block is a real question about the accounting and is
242
+ // roadmap item 9, not this check's subject: the subject is that the spill,
243
+ // when it completes, must not put the stale copy back on disk.
221
244
  store.reviseGrowthCeiling(CHUNK * 2);
222
245
  await until(() => disk.heldCount > 0, "the spill of piece 0 is in flight");
223
- store.reviseGrowthCeiling(CHUNK * 3);
246
+ store.reviseGrowthCeiling(CHUNK * 4);
224
247
 
225
248
  // The swarm hands piece 0 back while that write is still going. The store
226
249
  // must drop the disk copy AFTER the write has recorded it, not before —
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file Where a viewer's resume position falls in the file, in bytes.
3
+ *
4
+ * The warm-up fetches a file's two edges, because that is what the codec probe
5
+ * reads. The region the VIEWER will resume at was asked for by nobody until the
6
+ * encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
7
+ * cold torrent, and the piece it then needed took another 46.3 s to arrive.
8
+ * Turning the position into an offset is the only arithmetic in that path, so it
9
+ * is the only part with anything to get wrong.
10
+ */
11
+
12
+ import assert from "node:assert/strict";
13
+ import test from "node:test";
14
+
15
+ import { resumeByteOffset } from "../services/torrent-worker/container-tracks.js";
16
+
17
+ test("a position halfway through a film is halfway through its file", () => {
18
+ assert.equal(resumeByteOffset(1000, 100, 50), 500);
19
+ });
20
+
21
+ test("the field case lands where the encoder went looking", () => {
22
+ // The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
23
+ // read that blocked was at 63 MB.
24
+ const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
25
+ const megabytes = at / (1024 * 1024);
26
+ assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
27
+ });
28
+
29
+ test("a position past the end reads the end, rather than past it", () => {
30
+ const length = 1000;
31
+ const at = resumeByteOffset(length, 100, 10_000);
32
+ assert.equal(at, length - 1);
33
+ });
34
+
35
+ test("nothing is known, nothing is guessed", () => {
36
+ assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
37
+ assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
38
+ assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
39
+ });
@@ -60,7 +60,7 @@ test("the missing blocks are freed and asked of a second wire", () => {
60
60
  });
61
61
 
62
62
  test("at most one duplicate per wire, however long the tail", () => {
63
- const target = piece(512, 40);
63
+ const target = piece(512, 16);
64
64
  const torrent = {
65
65
  pieces: [target],
66
66
  wires: [wire(), wire()],
@@ -73,7 +73,29 @@ test("at most one duplicate per wire, however long the tail", () => {
73
73
  assert.equal(target.cancelled.length, 2, "and no reservation is freed that nobody was asked for");
74
74
  });
75
75
 
76
- test("a wire whose pipeline is full ends the attempt", () => {
76
+ test("a piece still arriving normally is not duplicated at all", () => {
77
+ // Forty blocks outstanding is not a tail, it is a piece in transit. Asking
78
+ // for a second copy of it would spend the shared link on bytes that are
79
+ // already on their way — which is the whole difference between this and the
80
+ // 2-14 blocks a blocked reader was measured waiting on.
81
+ const target = piece(512, 40);
82
+ const torrent = {
83
+ pieces: [target],
84
+ wires: [wire(), wire()],
85
+ _request: () => true
86
+ };
87
+
88
+ const result = duplicateTailFor(torrent, 0);
89
+
90
+ assert.equal(result.duplicated, 0);
91
+ assert.equal(target.cancelled.length, 0, "and nothing is freed either");
92
+ });
93
+
94
+ test("a wire whose pipeline is full does not end the attempt", () => {
95
+ // Pipelines are per wire, so one wire being full says nothing about the next.
96
+ // This used to stop the whole pass on the first refusal, on the stated
97
+ // reasoning that the remaining wires were "no emptier" — an assumption about
98
+ // other peers' queues that nothing measures.
77
99
  const target = piece(512, 5);
78
100
  const torrent = {
79
101
  pieces: [target],
@@ -88,11 +110,33 @@ test("a wire whose pipeline is full ends the attempt", () => {
88
110
  assert.equal(result.duplicated, 0);
89
111
  assert.equal(
90
112
  target.cancelled.length,
91
- 1,
92
- "only the block that was actually offered is freed; the rest keep their reservations"
113
+ 3,
114
+ "every wire was offered a block; a freed reservation nobody took is handed to whoever asks next"
93
115
  );
94
116
  });
95
117
 
118
+ test("a refusal by one wire does not cost the block a faster wire would have taken", () => {
119
+ const target = piece(512, 3);
120
+ const placed = [];
121
+ const torrent = {
122
+ pieces: [target],
123
+ wires: [wire({ speed: 900_000 }), wire({ speed: 800_000 })],
124
+ _request: (wireAsked, index) => {
125
+ // The first wire is full; the second is not.
126
+ if (wireAsked === torrent.wires[0]) {
127
+ return false;
128
+ }
129
+ placed.push(index);
130
+ return true;
131
+ }
132
+ };
133
+
134
+ const result = duplicateTailFor(torrent, 0);
135
+
136
+ assert.equal(result.duplicated, 1);
137
+ assert.deepEqual(placed, [0], "the second wire was still asked");
138
+ });
139
+
96
140
  test("a piece with nothing missing is left alone", () => {
97
141
  const target = piece(4, 0);
98
142
  const torrent = { pieces: [target], wires: [wire()], _request: () => true };