@torrent-tv/proxy 2.80.6 → 2.80.8

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.
@@ -324,6 +324,21 @@ export class EncodeOrchestrator {
324
324
  )
325
325
  });
326
326
 
327
+ // A move is the plan taking a running encoder away from where it already
328
+ // stands, which is exactly the decision that was found wandering back and
329
+ // forth in the field on 2026-09-07 with no way to see why: the "because"
330
+ // line names the comparison in words, never the numbers it was decided
331
+ // from. Said here, once per reconcile, and only when a move actually
332
+ // happens — everything a rerun of the same decision needs: the windows
333
+ // this call saw (priority, the real time, which side of the viewers),
334
+ // the budget, and where every live run stood.
335
+ if (actions.some((action) => action.type === "move")) {
336
+ this.logger.info(
337
+ `encode-plan move on ${address}: windows=${JSON.stringify(windows)} ` +
338
+ `maxRuns=${this.#affordableOn(address, live)} ` +
339
+ `live=${JSON.stringify(live.map((run) => ({ from: run.from, to: run.to, head: run.head, speedX: run.speedX })))}`
340
+ );
341
+ }
327
342
  for (const action of actions) {
328
343
  if (action.type === "stop") {
329
344
  this.#stop(action.run, action.because);
@@ -575,22 +575,26 @@ export class SharedPieceStore {
575
575
  // the machine was full kept a small allowance for its whole life, however
576
576
  // much memory was freed afterwards (roadmap item 2, 2026-09-02).
577
577
  const wanted = Math.floor(Number(allowedBytes) / this.#chunkLength);
578
- // Never below one reader's whole window while a reader exists, even when
579
- // the machine's share says less. A store that cannot hold the window of the
580
- // read it is serving cannot complete that read at all: every resident piece
581
- // ends up pinned, the read returns zero bytes and ffmpeg takes that for the
582
- // end of the file, which killed every encoder on that file in the field on
583
- // 2026-08-15. Exceeding the share is the lesser failure, and the line below
584
- // says when it happens.
578
+ // Never below what the live readers together hold, even when the machine's
579
+ // share says less. A store that cannot hold what its readers are pinning
580
+ // cannot complete any of their reads at all: every resident piece ends up
581
+ // pinned, a read returns zero bytes and ffmpeg takes that for the end of
582
+ // the file, which killed every encoder on that file in the field on
583
+ // 2026-08-15 (one reader) and again on 2026-09-07 (three readers of one
584
+ // file at once — picture, sound and the edge-warming read — where this
585
+ // floor was still computed from only the widest one of them, `wantedBytes`
586
+ // above already asks for the union and got it right; this is the same
587
+ // union, used as the floor instead of only as the ask). Exceeding the share
588
+ // is the lesser failure, and the line below says when it happens.
585
589
  const demand = this.#lru.demand();
586
590
  this.#growthCeiling = Math.max(
587
591
  MIN_RESIDENT_PIECES,
588
- demand.readers > 0 ? demand.widestPieces : MIN_RESIDENT_PIECES,
592
+ demand.readers > 0 ? demand.unionPieces : MIN_RESIDENT_PIECES,
589
593
  Number.isFinite(wanted) ? wanted : MIN_RESIDENT_PIECES
590
594
  );
591
595
  const belowAWindow = demand.readers > 0
592
596
  && Number.isFinite(wanted)
593
- && wanted < demand.widestPieces;
597
+ && wanted < demand.unionPieces;
594
598
  this.#beyondTheMachine = belowAWindow;
595
599
  // The LRU is told too. It was constructed with the store's original
596
600
  // capacity and never revised, so `isFull()` answered against a number that
@@ -278,6 +278,32 @@ test("the store asks for what its readers declared, and for a whole window at le
278
278
  }
279
279
  });
280
280
 
281
+ test("the floor the ceiling will not fall below is the same union, not the widest reader alone", async () => {
282
+ const { store, directory } = await makeStore(64);
283
+ try {
284
+ // The same two overlapping readers as the ask above: picture and sound,
285
+ // 10..44, thirty-five pieces together. The machine now offers far less
286
+ // than that (ten pieces) — before this fix the floor here came from
287
+ // `widestPieces` (twenty, the wider of the two readers alone), fifteen
288
+ // short of what both of them were actually pinning at once. A field
289
+ // session with three such readers on one file (video, audio and the
290
+ // edge-warming read) reached exactly that shortfall on 2026-09-07: every
291
+ // resident piece ended up pinned and WebTorrent destroyed the torrent
292
+ // over it.
293
+ store.protectRange("video", 10, 29);
294
+ store.protectRange("audio", 25, 44);
295
+ const revised = store.reviseGrowthCeiling(10 * PIECE);
296
+ assert.equal(
297
+ revised.ceilingBytes,
298
+ 35 * PIECE,
299
+ "the floor is the union of both readers, not the wider one alone"
300
+ );
301
+ } finally {
302
+ store.destroy(() => undefined);
303
+ await fs.rm(directory, { recursive: true, force: true });
304
+ }
305
+ });
306
+
281
307
  test("a block is re-used instead of a new one being allocated for every piece", async () => {
282
308
  const capacity = 4;
283
309
  const { store, directory } = await makeStore(capacity);