@torrent-tv/proxy 2.83.0 → 2.83.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 (33) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +1 -1
  3. package/routes/api/sources/stats/get.js +11 -2
  4. package/routes/stream/get.js +74 -3
  5. package/services/delivery-probe.js +248 -43
  6. package/services/download/SwarmSelection.js +5 -5
  7. package/services/download/registry.js +20 -0
  8. package/services/encode/EncodeRun.js +1 -0
  9. package/services/encode/encode-exit.js +17 -0
  10. package/services/files/CompletedFiles.js +276 -0
  11. package/services/files/piece-from-whole-file.js +118 -0
  12. package/services/output/cut-grid.js +13 -3
  13. package/services/piece-store/piece-disk-store.js +72 -2
  14. package/services/piece-store/shared-piece-store.js +274 -27
  15. package/services/torrent-pool.js +238 -19
  16. package/services/torrent-worker/client.js +21 -0
  17. package/services/torrent-worker/protocol.js +9 -1
  18. package/services/torrent-worker/worker.js +183 -2
  19. package/test/completed-files.test.js +115 -0
  20. package/test/cuts-follow-published-grid.test.js +35 -0
  21. package/test/delivery-probe.test.js +114 -1
  22. package/test/encode-exit.test.js +18 -0
  23. package/test/piece-disk-store.test.js +26 -0
  24. package/test/piece-from-whole-file.test.js +129 -0
  25. package/test/piece-store-eviction.test.js +28 -15
  26. package/test/piece-store-never-refuses.test.js +153 -0
  27. package/test/piece-store-reservations.test.js +16 -3
  28. package/test/probe-wedge-certainty.test.js +3 -3
  29. package/test/shared-piece-store.test.js +27 -13
  30. package/test/stream-route.test.js +41 -0
  31. package/test/swarm-follows-readers.test.js +126 -0
  32. package/test/swarm-reach.test.js +5 -0
  33. package/test/upload-hurry.test.js +27 -0
@@ -99,16 +99,46 @@ export function allowedGap({
99
99
  if (!(bytesPerSecond > 0) || !(intervalMs > 0)) {
100
100
  return null;
101
101
  }
102
- const drainMs = (Math.max(queuedBytes, 0) / bytesPerSecond) * 1000;
103
- const waitMs =
104
- drainMs +
105
- Math.max(rttMs, 0) +
106
- Math.max(echoIntervalMs, 0) +
107
- Math.max(peerLoopLagMs, 0);
108
102
  // At least one: a probe sent and not yet echoed is the ordinary state.
109
- return Math.max(1, Math.ceil(waitMs / intervalMs));
103
+ return Math.max(1, Math.ceil(allowedWaitMs({ queuedBytes, bytesPerSecond, rttMs, echoIntervalMs, peerLoopLagMs }) / intervalMs));
104
+ }
105
+
106
+ /**
107
+ * How long a probe may legitimately take to be reported back, in milliseconds.
108
+ *
109
+ * THE QUANTITY {@link allowedGap} COMPUTES AND THEN THROWS AWAY by dividing it
110
+ * into probes. Probes are the wrong unit and always were: the same probe goes
111
+ * down every channel INCLUDING the one carrying the film, and SCTP schedules
112
+ * per association, so a probe waits behind queued video exactly as a segment
113
+ * does. What is behind is then not the peer's answer but the probe itself —
114
+ * and the honest measure of that is the age of the newest probe the peer has
115
+ * seen, which this is compared against.
116
+ *
117
+ * Four terms, every one measured: the queue's own drain time at the rate this
118
+ * connection is achieving, the crossing, the peer's reporting cadence, and how
119
+ * late the peer's event loop is running.
120
+ *
121
+ * @param {{ queuedBytes: number, bytesPerSecond: number, rttMs: number,
122
+ * echoIntervalMs?: number, peerLoopLagMs?: number }} state
123
+ * @returns {number}
124
+ */
125
+ export function allowedWaitMs({ queuedBytes, bytesPerSecond, rttMs, echoIntervalMs = 0, peerLoopLagMs = 0 }) {
126
+ if (!(bytesPerSecond > 0)) {
127
+ return 0;
128
+ }
129
+ const drainMs = (Math.max(queuedBytes, 0) / bytesPerSecond) * 1000;
130
+ return drainMs + Math.max(rttMs, 0) + Math.max(echoIntervalMs, 0) + Math.max(peerLoopLagMs, 0);
110
131
  }
111
132
 
133
+ /**
134
+ * How far back the send times of probes are kept.
135
+ *
136
+ * A probe older than the point at which the reverse direction is called gone
137
+ * can say nothing further, and that bound is itself derived per connection —
138
+ * this is the outer edge of it, kept so the map cannot grow with the session.
139
+ */
140
+ const PROBE_HISTORY_MS = 10 * 60 * 1000;
141
+
112
142
  /** The label the browser gives the unordered, non-retransmitting channel. */
113
143
  export const UNRELIABLE_LABEL = "proxy-fast";
114
144
 
@@ -144,12 +174,33 @@ const REPORT_INTERVAL_MS = 5_000;
144
174
  * as {@link wedgeIsCertain} in `data-channel-handler.js`, applied to the
145
175
  * probe's own counter instead of the transport's byte counter.
146
176
  *
147
- * @param {{ stuckForMs: number, longestHealthySeenGapMs: number, intervalMs?: number }} state
148
- * @returns {{ certain: boolean, needMs: number }}
177
+ * THE FLOOR IS NOT ONE PROBE INTERVAL. "The longest gap this connection has
178
+ * ever shown" is nothing at all in its first minutes, and the floor under it
179
+ * was a single interval — 500 ms — so any two-second quiet stretch while the
180
+ * browser filled its cushion read as longer than anything healthy ever seen.
181
+ * Field 2026-09-11: two captures of 180 s each, triggered at `wedged 1s` and
182
+ * `wedged 2s`, on a connection with `rtt=5ms`, every queue at 0 B and the
183
+ * viewer watching; 65 MB of them were left on the addon's disk.
184
+ *
185
+ * What replaces it is the connection's own answer to how long a legitimate
186
+ * report may take: the peer reports twice a second, the report has to cross,
187
+ * and the peer's own event loop may be late — all three measured, all three
188
+ * already summed for `echoStaleMs`. A stretch shorter than that proves nothing
189
+ * whatever about the association.
190
+ *
191
+ * @param {{ stuckForMs: number, longestHealthySeenGapMs: number, intervalMs?: number,
192
+ * legitimateReportMs?: number }} state
193
+ * @returns {{ isCertain: boolean, needMs: number }}
149
194
  */
150
- export function probeWedgeIsCertain({ stuckForMs, longestHealthySeenGapMs, intervalMs = PROBE_INTERVAL_MS }) {
151
- const needMs = Math.max(longestHealthySeenGapMs, intervalMs);
152
- return { certain: stuckForMs >= needMs, needMs };
195
+ export function probeWedgeIsCertain({
196
+ stuckForMs,
197
+ longestHealthySeenGapMs,
198
+ intervalMs = PROBE_INTERVAL_MS,
199
+ legitimateReportMs = 0
200
+ }) {
201
+ const floorMs = Math.max(intervalMs, Number.isFinite(legitimateReportMs) ? legitimateReportMs : 0);
202
+ const needMs = Math.max(longestHealthySeenGapMs, floorMs);
203
+ return { isCertain: stuckForMs >= needMs, needMs };
153
204
  }
154
205
 
155
206
  /**
@@ -218,47 +269,76 @@ export function probeWedgeIsCertain({ stuckForMs, longestHealthySeenGapMs, inter
218
269
  export function readProbeState(state) {
219
270
  const seenOf = (label) =>
220
271
  state.seen instanceof Map ? state.seen.get(label) : state.seen?.[label];
272
+ /**
273
+ * @param {unknown} source
274
+ * @param {string} label
275
+ * @returns {number | null}
276
+ */
277
+ const timeOf = (source, label) => {
278
+ const value = source instanceof Map ? source.get(label) : /** @type {any} */ (source)?.[label];
279
+ return Number.isFinite(value) ? Number(value) : null;
280
+ };
221
281
  const allowedOf = (label) => {
222
282
  const source = state.allowed;
223
283
  const value = source instanceof Map ? source.get(label) : source?.[label];
224
284
  return Number.isInteger(value) ? Number(value) : null;
225
285
  };
226
286
  const parts = [];
227
- let orderedBehind = false;
228
- let unreliableBehind = false;
229
- let unreliableKnown = false;
230
- let judgeable = false;
287
+ let isOrderedBehind = false;
288
+ let isUnreliableBehind = false;
289
+ let isUnreliableKnown = false;
290
+ let isJudgeable = false;
231
291
  for (const label of state.labels) {
232
292
  const seen = seenOf(label);
233
293
  const gap = Number.isInteger(seen) ? state.seq - Number(seen) : null;
234
294
  const allowance = allowedOf(label);
235
- parts.push(`${label}=${seen ?? "?"}(gap ${gap ?? "?"} of ${allowance ?? "?"})`);
295
+ // BY TIME WHERE IT IS KNOWN. The count is what the line prints, because it
296
+ // is what a reader recognises; what decides is a time.
297
+ //
298
+ // The measured one-way delay first: it is the forward direction itself,
299
+ // with the two clocks reconciled from the exchange. The age of the newest
300
+ // reported probe is the fallback, and it is larger than the thing itself by
301
+ // the peer's reporting cadence and the way back — which is why its own
302
+ // allowance carries both of those and the one-way allowance carries
303
+ // neither.
304
+ const oneWay = timeOf(state.oneWayMs, label);
305
+ const oneWayAllowed = timeOf(state.allowedOneWayMs, label);
306
+ const lagMs = oneWay !== null && oneWayAllowed !== null ? oneWay : timeOf(state.behindMs, label);
307
+ const mayWaitMs = oneWay !== null && oneWayAllowed !== null
308
+ ? oneWayAllowed
309
+ : timeOf(state.allowedWaitMs, label);
310
+ const lagText = lagMs === null || mayWaitMs === null
311
+ ? ""
312
+ : ` ${Math.round(lagMs)}ms of ${Math.round(mayWaitMs)}ms${oneWay !== null ? " one way" : ""}`;
313
+ parts.push(`${label}=${seen ?? "?"}(gap ${gap ?? "?"} of ${allowance ?? "?"}${lagText})`);
236
314
  if (allowance === null) {
237
315
  continue;
238
316
  }
239
- judgeable = true;
240
- const behind = gap === null || gap > allowance;
317
+ isJudgeable = true;
318
+ const isBehind = lagMs !== null && mayWaitMs !== null
319
+ ? lagMs > mayWaitMs
320
+ : gap === null || gap > allowance;
241
321
  if (label === UNRELIABLE_LABEL) {
242
- unreliableKnown = true;
243
- unreliableBehind = behind;
244
- } else if (behind) {
245
- orderedBehind = true;
322
+ isUnreliableKnown = true;
323
+ isUnreliableBehind = isBehind;
324
+ } else if (isBehind) {
325
+ isOrderedBehind = true;
246
326
  }
247
327
  }
248
- const advancing = state.peerBytesAdvancing === true;
328
+ const isPeerAdvancing = state.peerBytesAdvancing === true;
249
329
  const detail =
250
330
  `sent=${state.seq} ${parts.join(" ")} ` +
251
331
  `echoAge=${state.echoAgeMs === null ? "never" : `${state.echoAgeMs}ms`}` +
252
332
  (state.peerBytesAdvancing === null || state.peerBytesAdvancing === undefined
253
333
  ? ""
254
- : ` peerBytes=${advancing ? "advancing" : "still"}`) +
334
+ : ` peerBytes=${isPeerAdvancing ? "advancing" : "still"}`) +
255
335
  (Number.isFinite(state.peerLoopLagMs) ? ` peerLoopLag=${Math.round(Number(state.peerLoopLagMs))}ms` : "") +
256
336
  (state.peerVisibility ? ` peerTab=${state.peerVisibility}` : "");
257
337
 
258
338
  if (state.echoes === 0) {
259
339
  return { verdict: "no-echo-yet", detail };
260
340
  }
261
- if (!judgeable) {
341
+ if (!isJudgeable) {
262
342
  return { verdict: "no-rate-yet", detail };
263
343
  }
264
344
  const staleAfterMs = Number.isFinite(state.echoStaleMs) && state.echoStaleMs > 0
@@ -267,21 +347,21 @@ export function readProbeState(state) {
267
347
  if (state.echoAgeMs !== null && state.echoAgeMs > staleAfterMs) {
268
348
  return { verdict: "reverse-direction-gone", detail };
269
349
  }
270
- if (!orderedBehind && !(unreliableKnown && unreliableBehind)) {
350
+ if (!isOrderedBehind && !(isUnreliableKnown && isUnreliableBehind)) {
271
351
  return { verdict: "flowing", detail };
272
352
  }
273
353
  // Bytes are still arriving at the far end. Whatever the probe gaps say, this
274
354
  // association has not stopped — the probes are behind a backlog, which is
275
355
  // what filling a cushion looks like from here.
276
- if (advancing) {
356
+ if (isPeerAdvancing) {
277
357
  return { verdict: "flowing", detail };
278
358
  }
279
- if (orderedBehind && unreliableKnown && !unreliableBehind) {
359
+ if (isOrderedBehind && isUnreliableKnown && !isUnreliableBehind) {
280
360
  return { verdict: "stream-stuck", detail };
281
361
  }
282
- if (orderedBehind) {
362
+ if (isOrderedBehind) {
283
363
  return {
284
- verdict: unreliableKnown ? "association-stopped" : "ordered-behind-no-comparison",
364
+ verdict: isUnreliableKnown ? "association-stopped" : "ordered-behind-no-comparison",
285
365
  detail
286
366
  };
287
367
  }
@@ -327,6 +407,18 @@ export function createDeliveryProbe({
327
407
  const now = Date.now();
328
408
  connection.seq += 1;
329
409
  connection.sentAt = now;
410
+ // WHEN each probe went out, so that what the peer reports can be read as a
411
+ // time rather than as a count of probes. Pruned to the oldest probe any
412
+ // judgement could still be about: once a probe is older than the point at
413
+ // which the reverse direction is called gone, its age says nothing further.
414
+ connection.sentAtBySeq.set(connection.seq, now);
415
+ for (const [probeNumber, sentAt] of connection.sentAtBySeq) {
416
+ if (now - sentAt > PROBE_HISTORY_MS) {
417
+ connection.sentAtBySeq.delete(probeNumber);
418
+ } else {
419
+ break;
420
+ }
421
+ }
330
422
  const message = JSON.stringify({ type: "probe", seq: connection.seq, sentAt: now });
331
423
  for (const channel of connection.channels.keys()) {
332
424
  try {
@@ -343,6 +435,10 @@ export function createDeliveryProbe({
343
435
  const rttMs = Number(delivery?.rttMs) || 0;
344
436
  /** @type {Map<string, number | null>} */
345
437
  const allowed = new Map();
438
+ /** How long each channel's newest unreported probe may legitimately be. */
439
+ const allowedWait = new Map();
440
+ /** The same, for the measured one-way time where the clocks are reconciled. */
441
+ const allowedOneWay = new Map();
346
442
  for (const [channel, label] of connection.channels) {
347
443
  let queuedBytes = 0;
348
444
  try {
@@ -358,6 +454,26 @@ export function createDeliveryProbe({
358
454
  peerLoopLagMs: connection.peerLoopLagMs ?? 0,
359
455
  intervalMs
360
456
  });
457
+ const waitMs = allowedWaitMs({
458
+ queuedBytes,
459
+ bytesPerSecond,
460
+ rttMs,
461
+ echoIntervalMs: connection.echoIntervalMs,
462
+ peerLoopLagMs: connection.peerLoopLagMs ?? 0
463
+ });
464
+ const heldWait = allowedWait.get(label);
465
+ if (!Number.isFinite(heldWait) || waitMs > heldWait) {
466
+ allowedWait.set(label, waitMs);
467
+ }
468
+ // What a probe may take ONE WAY: the queue's own drain time and half the
469
+ // crossing. Neither the peer's reporting cadence nor its loop delay
470
+ // belongs here — with the clocks reconciled they are no longer in the
471
+ // measurement they would have to be allowed for.
472
+ const oneWayAllowed = allowedWaitMs({ queuedBytes, bytesPerSecond, rttMs: rttMs / 2 });
473
+ const heldOneWay = allowedOneWay.get(label);
474
+ if (!Number.isFinite(heldOneWay) || oneWayAllowed > heldOneWay) {
475
+ allowedOneWay.set(label, oneWayAllowed);
476
+ }
361
477
  // Several channels can carry one label only in malformed cases; the
362
478
  // larger allowance is the safer of the two.
363
479
  const held = allowed.get(label);
@@ -398,6 +514,25 @@ export function createDeliveryProbe({
398
514
  peerLoopLagMs: connection.peerLoopLagMs,
399
515
  peerVisibility: connection.peerVisibility,
400
516
  allowed,
517
+ // WHAT IS BEHIND, IN TIME. For each channel: how old the newest probe the
518
+ // peer has reported seeing is. The same probe goes down every channel
519
+ // including the one carrying the film, and SCTP schedules per
520
+ // association, so a probe waits behind queued video exactly as a segment
521
+ // does — which means a count of outstanding probes measures the queue,
522
+ // not the association. This is the queue's own time, against the time the
523
+ // queue is allowed to take.
524
+ behindMs: Object.fromEntries(
525
+ [...connection.seen.entries()].map(([label, newestSeenNumber]) => {
526
+ const sentAt = connection.sentAtBySeq.get(newestSeenNumber);
527
+ return [label, Number.isFinite(sentAt) ? now - sentAt : null];
528
+ })
529
+ ),
530
+ allowedWaitMs: Object.fromEntries(allowedWait.entries()),
531
+ // The measured forward delay, where the two clocks have been reconciled.
532
+ // Preferred over the age above because it is the thing itself: the age
533
+ // also carries the peer's reporting cadence and the way back.
534
+ oneWayMs: Object.fromEntries(connection.oneWayMs.entries()),
535
+ allowedOneWayMs: Object.fromEntries(allowedOneWay.entries()),
401
536
  // Same arithmetic for the echo's own age: the peer cannot answer sooner
402
537
  // than its own cadence allows, nor sooner than its own event loop runs.
403
538
  echoStaleMs:
@@ -422,11 +557,24 @@ export function createDeliveryProbe({
422
557
  // as a legitimate gap, is the wedge this exists to catch.
423
558
  const stuckForMs = connection.lastSeenAdvanceAt === 0 ? 0 : now - connection.lastSeenAdvanceAt;
424
559
  if (verdict === "association-stopped") {
425
- const { certain, needMs } = probeWedgeIsCertain({
560
+ const { isCertain, needMs } = probeWedgeIsCertain({
426
561
  stuckForMs,
427
- longestHealthySeenGapMs: connection.longestHealthySeenGapMs
562
+ longestHealthySeenGapMs: connection.longestHealthySeenGapMs,
563
+ // The same sum the echo's own staleness is judged by: the peer reports
564
+ // twice a second, the report has to cross, and the peer's loop may be
565
+ // late. Nothing shorter than that says anything.
566
+ // BOTH cadences, because they beat against each other: a probe sent
567
+ // just after the peer composed a report shows up only in the NEXT one.
568
+ // So the longest legitimate gap between two advances is one probe
569
+ // interval plus one report interval plus the crossing plus whatever
570
+ // the peer's own loop is behind by.
571
+ legitimateReportMs:
572
+ intervalMs +
573
+ connection.echoIntervalMs +
574
+ rttMs +
575
+ Math.max(connection.peerLoopLagMs ?? 0, 0)
428
576
  });
429
- if (certain && !connection.probeCaptureStarted) {
577
+ if (isCertain && !connection.probeCaptureStarted) {
430
578
  connection.probeCaptureStarted = true;
431
579
  const reasonText =
432
580
  `probe seen-counter unmoved ${Math.round(stuckForMs / 1000)}s against the ` +
@@ -497,6 +645,24 @@ export function createDeliveryProbe({
497
645
  reportedAt: 0,
498
646
  lastSeenAdvanceAt: 0,
499
647
  longestHealthySeenGapMs: 0,
648
+ sentAtBySeq: new Map(),
649
+ // THE TWO CLOCKS, separated by the exchange rather than assumed to
650
+ // agree. Every report carries when each channel's newest probe
651
+ // arrived at the peer and when the report itself left; with the time
652
+ // this proxy stamped into that probe, and the time the report
653
+ // arrives, that is the four timestamps an offset is computed from.
654
+ //
655
+ // The estimate is kept from the report with the SMALLEST round trip
656
+ // seen, because that is the one that queued the least — and the two
657
+ // directions here are as unequal as they get, film one way and almost
658
+ // nothing the other, which is exactly where an offset taken from a
659
+ // busy moment is wrong by half the difference.
660
+ /** @type {number | null} */
661
+ clockOffsetMs: null,
662
+ /** @type {number} */
663
+ offsetFromRoundTripMs: Number.POSITIVE_INFINITY,
664
+ /** How long the newest probe took to reach the peer, by channel. @type {Map<string, number>} */
665
+ oneWayMs: new Map(),
500
666
  probeCaptureStarted: false,
501
667
  timer: null
502
668
  };
@@ -565,6 +731,43 @@ export function createDeliveryProbe({
565
731
  connection.lastSeenAdvanceAt = now;
566
732
  }
567
733
  }
734
+ // THE FOUR TIMESTAMPS. `t1` is when this proxy sent the probe the peer
735
+ // is reporting, `t2` when the peer received it, `t3` when the peer sent
736
+ // this report, `t4` is now. From them the round trip is
737
+ // `(t4 - t1) - (t3 - t2)` — the peer's own thinking time removed — and
738
+ // the difference between the two clocks is `((t2 - t1) + (t3 - t4)) / 2`.
739
+ //
740
+ // The offset is kept from the report whose round trip was SMALLEST: that
741
+ // arithmetic assumes the two directions are equally quick, which here
742
+ // they are not — film one way, almost nothing the other — and the least
743
+ // queued sample is the one where the assumption costs least. A fresh
744
+ // minimum replaces the estimate, so a clock that jumps is followed within
745
+ // a few reports rather than believed for ever.
746
+ const peerSentAt = Number(echo?.sentAt);
747
+ const seenAt = echo?.seenAt;
748
+ if (Number.isFinite(peerSentAt) && seenAt && typeof seenAt === "object") {
749
+ for (const [label, peerSawAtRaw] of Object.entries(seenAt)) {
750
+ const peerSawAt = Number(peerSawAtRaw);
751
+ const newestSeenNumber = connection.seen.get(label);
752
+ const weSentAt = Number.isInteger(newestSeenNumber)
753
+ ? connection.sentAtBySeq.get(newestSeenNumber)
754
+ : undefined;
755
+ if (!Number.isFinite(peerSawAt) || !Number.isFinite(weSentAt)) {
756
+ continue;
757
+ }
758
+ const roundTrip = (now - weSentAt) - (peerSentAt - peerSawAt);
759
+ if (roundTrip >= 0 && roundTrip < connection.offsetFromRoundTripMs) {
760
+ connection.offsetFromRoundTripMs = roundTrip;
761
+ connection.clockOffsetMs = ((peerSawAt - weSentAt) + (peerSentAt - now)) / 2;
762
+ }
763
+ if (connection.clockOffsetMs !== null) {
764
+ // What the probe itself took, one way, with the clocks reconciled.
765
+ // Negative only if the offset is stale, and then it says so rather
766
+ // than being hidden.
767
+ connection.oneWayMs.set(label, peerSawAt - weSentAt - connection.clockOffsetMs);
768
+ }
769
+ }
770
+ }
568
771
  // What the far end says it has received at the transport level. It is the
569
772
  // one figure that separates a backlog from a stopped association, and it
570
773
  // arrives on the direction that goes on working through a freeze.
@@ -575,17 +778,19 @@ export function createDeliveryProbe({
575
778
  // Sum of per-channel bytes. Transport's bytesReceived advances on SACKs
576
779
  // (140 B/s on a wedge) and gives a false "advancing" every other tick —
577
780
  // channel bytes are flat on a wedge, so they are the correct signal.
578
- const ch = echo?.report?.channels;
579
- if (ch && typeof ch === "object") {
580
- let total = 0;
581
- let has = false;
582
- for (const v of Object.values(ch)) {
583
- if (v && typeof v.bytes === "number" && Number.isFinite(v.bytes)) {
584
- total += v.bytes;
585
- has = true;
781
+ const peerChannels = echo?.report?.channels;
782
+ if (peerChannels && typeof peerChannels === "object") {
783
+ let bytesAcrossChannels = 0;
784
+ let hasChannelReport = false;
785
+ for (const channelReport of Object.values(peerChannels)) {
786
+ if (channelReport && Number.isFinite(channelReport.bytes)) {
787
+ bytesAcrossChannels += channelReport.bytes;
788
+ hasChannelReport = true;
586
789
  }
587
790
  }
588
- if (has) connection.peerChannelBytes = total;
791
+ if (hasChannelReport) {
792
+ connection.peerChannelBytes = bytesAcrossChannels;
793
+ }
589
794
  }
590
795
  // How far behind the far end's own event loop is running. A browser that
591
796
  // cannot run its timers cannot answer a probe, and every allowance here
@@ -180,15 +180,15 @@ export class SwarmSelection {
180
180
  this.#clearDisplacement();
181
181
  return;
182
182
  }
183
- const from = Math.min(...blocked.map((range) => range.from));
184
- const to = Math.max(...blocked.map((range) => range.to));
185
- if (this.#displacing && this.#displacing.from === from && this.#displacing.to === to) {
183
+ const nearest = Math.min(...blocked.map((range) => range.from));
184
+ const furthest = Math.max(...blocked.map((range) => range.to));
185
+ if (this.#displacing && this.#displacing.from === nearest && this.#displacing.to === furthest) {
186
186
  return;
187
187
  }
188
188
  this.#clearDisplacement();
189
189
  try {
190
- this.#torrent.critical?.(from, to);
191
- this.#displacing = { from, to };
190
+ this.#torrent.critical?.(nearest, furthest);
191
+ this.#displacing = { from: nearest, to: furthest };
192
192
  } catch {
193
193
  // silent-ok: displacement is an optimisation, and a torrent being torn
194
194
  // down is not worth failing a read over.
@@ -41,6 +41,26 @@ export function demandFor(torrent) {
41
41
  return entry;
42
42
  }
43
43
 
44
+ /**
45
+ * Whether this torrent is still short of anything anybody asked for.
46
+ *
47
+ * Read by the upload policy: while a reader is missing bytes, a little upload
48
+ * buys the reciprocity that gets them; once nothing declared is missing it buys
49
+ * nothing at all, and on 2026-09-11 the proxy went on offering 512 KB/s of a
50
+ * fully downloaded file to 596 peers for forty-eight minutes, reading a 4 MB
51
+ * piece off the disk for every 16 KB it sent.
52
+ *
53
+ * False for a torrent nothing has been stated about, which is the same answer
54
+ * and the right one: nobody is waiting for it.
55
+ *
56
+ * @param {object} torrent
57
+ * @returns {boolean}
58
+ */
59
+ export function hasUnmetDemand(torrent) {
60
+ const held = byTorrent.get(torrent);
61
+ return held ? held.selection.hasUrgentMissing() : false;
62
+ }
63
+
44
64
  /**
45
65
  * Give up everything stated for a torrent that is going.
46
66
  *
@@ -642,6 +642,7 @@ export class EncodeRun {
642
642
  const outcome = classifyEncodeExit({
643
643
  code,
644
644
  producedThrough: this.#produced.size > 0 ? this.reached : null,
645
+ producedCount: this.#produced.size,
645
646
  lastSegmentIndex: endOfWork,
646
647
  inputUnavailable: this.inputUnavailable(this.lastError)
647
648
  });
@@ -73,6 +73,11 @@ export const ENCODE_EXIT = Object.freeze({
73
73
  * session has on disk, or null when that could not be read.
74
74
  * @param {number | null} [facts.lastSegmentIndex] - Index of the file's last
75
75
  * segment according to the published playlist, or null when unknown.
76
+ * @param {number | null} [facts.producedCount] - How many segments THIS run
77
+ * made. Zero and "could not be read" are different facts and were both `null`
78
+ * until 2026-09-11: a run that made nothing exited zero and was recorded as
79
+ * having finished, which is how a segment that does not exist came to be
80
+ * believed present for forty-six minutes.
76
81
  * @param {boolean} [facts.inputUnavailable] - The error names a missing input.
77
82
  * @returns {string} One of {@link ENCODE_EXIT}.
78
83
  */
@@ -81,12 +86,24 @@ export function classifyEncodeExit({
81
86
  code = null,
82
87
  producedThrough = null,
83
88
  lastSegmentIndex = null,
89
+ producedCount = null,
84
90
  inputUnavailable = false
85
91
  } = {}) {
86
92
  if (superseded) {
87
93
  return ENCODE_EXIT.IGNORED;
88
94
  }
89
95
  if (code === 0) {
96
+ // PRODUCING NOTHING IS THE CLEAREST CASE OF NOT FINISHING, and it used to be
97
+ // the one case that read as success: `producedThrough` is null when the run
98
+ // made no segment at all, null is "unknown", and unknown fell through to
99
+ // complete. Field 2026-09-11: a run given #541..#541 was handed a start
100
+ // later than its own end, wrote 190 bytes that are not a fragment, exited
101
+ // zero — and was recorded as having reached the end of what it was given.
102
+ // Nothing asked for that segment again for forty-six minutes, until the
103
+ // viewer arrived at it and waited 23 s for a 404.
104
+ if (producedCount === 0 && lastSegmentIndex !== null) {
105
+ return ENCODE_EXIT.SHORT;
106
+ }
90
107
  const stoppedShort =
91
108
  lastSegmentIndex !== null &&
92
109
  producedThrough !== null &&