@torrent-tv/proxy 2.80.6 → 2.80.7

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.80.7
2
+
3
+ - **Fix**: The piece store's memory floor, re-derived every time the machine's free memory changes, took the widest single reader's declared window (`demand.widestPieces`) as its lower bound instead of what every live reader together needs (`demand.unionPieces`) — right for one reader, wrong for several. Field 2026-09-07: three concurrent readers of one file — the picture, the sound, and the edge-warming read — each declared their own window; the floor guaranteed room for only the widest of the three, so once residents filled that smaller number, every resident piece belonged to some reader's still-live window and none could be evicted. `Every resident piece is pinned and nothing moved for 5000ms; no slot can be freed`, which is not our own failure — it reaches WebTorrent's own `store.put()` callback, and WebTorrent treats any error there as fatal and destroys the torrent outright: `torrent.files` went to `[]`, every later read answered `File N not found in torrent:<hash>`, and the session never recovered. `wantedBytes` in the same file already asked the machine for the union, correctly; the floor below which the store refuses to shrink now asks for the same thing.
4
+
1
5
  ## 2.80.6
2
6
 
3
7
  - **Fix**: `waitForBufferDrain` resolved after 5 s while `bufferedAmount` was still above `LOW_WATER` and the send loop kept queueing into a wedged association — `399 MB` in `libdatachannel` `rss 455→982 MB` `drainMs=5000` on every line for 6 h 50 min. Now waits until `LOW_WATER` with `50 ms` polling and never resolves while bytes are still held.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.80.6",
3
+ "version": "2.80.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -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);