@torrent-tv/proxy 2.95.6 → 2.95.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.95.6",
3
+ "version": "2.95.8",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -665,14 +665,17 @@ export class SharedPieceStore {
665
665
  * range to declare.
666
666
  */
667
667
  get wantedBytes() {
668
- // Download preferences may use spare RAM, but cannot become a required
669
- // floor or a retained reading window after the map has been withdrawn.
670
- const demand = this.#lru.demand();
668
+ // MEMORY HOLDS WHAT IS READ; WHAT IS DOWNLOADED AHEAD HAS THE DISK. Download
669
+ // preferences order eviction and nothing else. Counted here, the map of a
670
+ // whole film asked for the whole film, the adaptive budget handed over all
671
+ // the memory the machine had free, and once the swarm was told the whole
672
+ // band the store filled 4.4 GB in five minutes and the kernel killed the
673
+ // process (Home Assistant, 2026-10-07, torrent-tv/meta#95).
671
674
  const required = this.#lru.demand({ memoryRequiredOnly: true });
672
675
  const pinned = this.#lru.pinnedCount;
673
- if (demand.readers > 0) {
676
+ if (required.readers > 0) {
674
677
  this.#widestSeenPieces = Math.max(this.#widestSeenPieces, required.widestPieces);
675
- const pieces = Math.max(MIN_RESIDENT_PIECES, demand.unionPieces, demand.widestPieces, pinned);
678
+ const pieces = Math.max(MIN_RESIDENT_PIECES, required.unionPieces, required.widestPieces, pinned);
676
679
  // Plus room to absorb what arrives while a write is finishing. Asking for
677
680
  // exactly what the readers want leaves no free place ever, so every
678
681
  // arrival evicts one of them — measured 2026-09-02: `6 reader(s) want 23
@@ -1826,17 +1826,19 @@ export class TorrentPool {
1826
1826
 
1827
1827
  /**
1828
1828
  * Schedule removal of a torrent (with its on-disk store) after
1829
- * {@link TORRENT_IDLE_TTL_MS} of nothing being wanted of it. Idempotent —
1830
- * replaces any existing timer for the torrent.
1829
+ * {@link TORRENT_IDLE_TTL_MS} of nothing being wanted of it. The clock runs
1830
+ * from when it stopped being wanted, so a timer already running is kept:
1831
+ * replacing it on every report of the same state extended the time without
1832
+ * end, and the withdrawal of one map's 4900 zones replaced it 3524 times in
1833
+ * four seconds (Home Assistant 2026-10-07, torrent-tv/meta#95).
1831
1834
  *
1832
1835
  * @param {import("webtorrent").Torrent} torrent
1833
1836
  * @returns {void}
1834
1837
  */
1835
1838
  #scheduleIdleRemoval(torrent) {
1836
- if (!torrent) {
1839
+ if (!torrent || this.#idleTimers.has(torrent)) {
1837
1840
  return;
1838
1841
  }
1839
- this.#cancelIdleRemoval(torrent);
1840
1842
  const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
1841
1843
  const infoHashShort = String(torrent.infoHash ?? "?").slice(0, 8);
1842
1844
  logger.info(`torrent-pool: scheduling idle removal for "${name}" [${infoHashShort}] in ${TORRENT_IDLE_TTL_MS / 1000}s`);
@@ -19,13 +19,25 @@ export class DownloadMaps {
19
19
  file.sourceZones = zones.map(zone => Number.isFinite(zone.deadlineAt) || !Number.isFinite(zone.withinSeconds)
20
20
  ? zone : { ...zone, deadlineAt: now + Math.max(0, zone.withinSeconds) * 1000 });
21
21
  let changed = false;
22
+ // Until the new zones are resolved to bytes, a resolved zone stays while
23
+ // the film it covers is still wanted. A viewer moving on re-cuts every zone
24
+ // without making its film unwanted; dropping the zones that lost their
25
+ // exact interval published an empty map every few seconds of playback,
26
+ // which let the swarm go and emptied the encoder's resolved input (Home
27
+ // Assistant 2026-10-07, torrent-tv/meta#95). A zone no new zone overlaps —
28
+ // a seek away — is withdrawn at once.
22
29
  const retained = file.zones.flatMap(zone => {
23
30
  const interval = zone.downloadInterval ?? zone;
24
31
  const current = file.sourceZones.find(candidate => candidate.from === interval.from && candidate.to === interval.to);
25
- const deadlineAt = current && Number.isFinite(current.deadlineAt)
26
- ? current.deadlineAt + (Math.max(0, zone.from - current.from) - (zone.leadSeconds ?? 0)) * 1000 : current?.deadlineAt;
27
- if (!current || Object.keys(current).some(key => key !== "from" && key !== "to" && !Object.is(zone[key], key === "deadlineAt" ? deadlineAt : current[key]))) changed = true;
28
- return current ? [{ ...zone, ...current, from: zone.from, to: zone.to, deadlineAt }] : [];
32
+ if (!current) {
33
+ const stillWanted = file.sourceZones.some(candidate => candidate.from < interval.to && interval.from < candidate.to);
34
+ if (!stillWanted) changed = true;
35
+ return stillWanted ? [zone] : [];
36
+ }
37
+ const deadlineAt = Number.isFinite(current.deadlineAt)
38
+ ? current.deadlineAt + (Math.max(0, zone.from - current.from) - (zone.leadSeconds ?? 0)) * 1000 : current.deadlineAt;
39
+ if (Object.keys(current).some(key => key !== "from" && key !== "to" && !Object.is(zone[key], key === "deadlineAt" ? deadlineAt : current[key]))) changed = true;
40
+ return [{ ...zone, ...current, from: zone.from, to: zone.to, deadlineAt }];
29
41
  });
30
42
  this.#setZones(file, retained);
31
43
  for (const [statement, metadata] of file.metadata) {