@torrent-tv/proxy 2.9.40 → 2.9.42

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,13 @@
1
+ ## 2.9.42
2
+
3
+ - **Fix**: Torrents failed to load with a proxy crash — the REAL root cause (2.9.41 misdiagnosed it). WebTorrent 2.8.5's `Torrent._onTorrentId` does `arr2hex(parsedTorrent.infoHash)`, but `parse-torrent` returns `infoHash` as a hex **string**. `uint8-util` **2.3.x** rewrote `arr2hex` to require a TypedArray (`Buffer.from(data.buffer …)`); a string's `.buffer` is `undefined` → `Buffer.from(undefined)` → `ERR_INVALID_ARG_TYPE` thrown in a detached microtask. `uint8-util` 2.2.x iterated the argument and tolerated a string, so it only broke once the addon's unpinned global `npm install` pulled 2.3.x. It hit **every** torrent (v1/v2/hybrid alike — `arr2hex` is always called). Diagnosed by reproducing `client.add` inside the addon container and isolating `arr2hex('<hex>')` throwing on 2.3.2 but not 2.2.6. Fix: update **WebTorrent 2.8.5 → 3.x**, where the maintainer replaced that line with `parsedTorrent.infoHash?.substring(0, 7)` (no `arr2hex` on the string) — a proper dependency-forward fix, not a version pin, so `uint8-util`/`parse-torrent` stay current. Verified: the exact broken combo (webtorrent 3.0.16 + uint8-util 2.3.2 + parse-torrent 11.0.23) now adds cleanly, and the full API the proxy uses (`select`/`deselect`/`critical`/`_critical`/`wires`/`throttleUpload`/`createReadStream`/`destroy({destroyStore})`) is unchanged in 3.x.
4
+ - **Fix**: Removed the 2.9.41 infohash pre-validation. It was based on the wrong diagnosis ("v2-only torrent") — the failing torrents were normal v1 — and it wrongly rejected legitimate v2/hybrid sources. WebTorrent (post-bump) handles v1, v2 and hybrid itself. The last-resort `uncaughtException`/`unhandledRejection` guard from 2.9.41 is kept as defense-in-depth.
5
+
6
+ ## 2.9.41
7
+
8
+ - **Fix**: A malformed or v2-only torrent source no longer crashes the whole proxy in a restart loop. WebTorrent's `Torrent._onTorrentId` does `arr2hex(parsedTorrent.infoHash)` assuming a BitTorrent v1 infohash exists; a v2-only / hybrid magnet (or a corrupt source) parses with `infoHash === undefined`, so that becomes `Buffer.from(undefined)` and throws in a microtask that bypasses the client `error` event — taking down the node and every viewer on it (observed: `ERR_INVALID_ARG_TYPE` → tunnel reconnect loop; the WebRTC session died ~6 s in as the process restarted under it). Two fixes: the torrent-add path now **pre-validates the infohash** with `parse-torrent` and rejects a source without a valid v1 40-hex infohash as a clean error the browser can show; and the process gained a **last-resort `uncaughtException`/`unhandledRejection` guard** that logs the full stack and keeps serving, so no single bad torrent can ever crash-loop the proxy. NOT a regression from the download-performance work (2.9.40) — those paths don't touch torrent parsing; it is a pre-existing crash surfaced by an unusual source.
9
+ - **New**: Longer idle retention so a brief absence resumes instead of restarting. The HLS transcode session idle TTL is raised from 2 min to **10 min** and the torrent-data idle TTL from 5 min to **15 min**. A viewer who pauses, backgrounds the tab, or turns the phone off for a few minutes now resumes without a cold ffmpeg restart and without re-downloading already-fetched data — the warm session also widens the seamless auto-reconnect window. An idle ffmpeg stops producing at the look-ahead cap, so the longer session TTL costs retained segments on disk rather than sustained CPU; the global disk cap still evicts torrent data earlier under pressure, and active playback keeps refreshing both timers so neither expires mid-watch.
10
+
1
11
  ## 2.9.40
2
12
 
3
13
  - **New**: Adaptive upload throttle. Seeding to the BitTorrent swarm does not help our viewer (we deliver over our own channel) — it is pure uplink cost and the riskiest legal act — so the client-wide upload limit now defaults to **off** (`throttleUpload(0)`, was WebTorrent's unlimited default) and is raised only when needed. A 5 s adjuster sets: **0** when no file has an active reader (stop seeding entirely once nothing is being watched); a low **floor** (50 KB/s) while a reader is active (a token upload so tit-for-tat does not choke us to zero); and a **boost** (512 KB/s) only when a torrent is starving (download barely trickling while it still needs data) AND its wires show reciprocity choke (≥2 peers we want data from are choking us) — earning unchoke slots to un-starve the download. The policy is a pure function (`decideUploadLimit`, unit-tested); each change is logged for field tuning. Client-wide limit (one active torrent is the norm today).
package/bin/cli.js CHANGED
@@ -26,6 +26,22 @@ import { logger } from "../utils/logger.js";
26
26
  const require = createRequire(import.meta.url);
27
27
  const { version: PROXY_VERSION } = require("../package.json");
28
28
 
29
+ // Last-resort process guard. This proxy runs UNTRUSTED torrents through
30
+ // WebTorrent, which can throw ASYNCHRONOUSLY on a malformed source — e.g. a
31
+ // v2-only / hybrid magnet crashes `arr2hex(parsedTorrent.infoHash)` deep inside
32
+ // `Torrent._onTorrentId` (undefined v1 infohash), in a microtask that bypasses
33
+ // the client "error" event. Without this, one bad torrent takes down the whole
34
+ // node and every viewer on it, and the addon restarts in a crash loop. Log the
35
+ // full stack and keep serving: the offending session fails on its own; everyone
36
+ // else is unaffected. (The add path also pre-validates the infohash, so this is
37
+ // a backstop for anything not caught there.)
38
+ process.on("uncaughtException", (error) => {
39
+ logger.error(`uncaughtException (kept alive): ${error?.stack ?? error}`);
40
+ });
41
+ process.on("unhandledRejection", (reason) => {
42
+ logger.error(`unhandledRejection (kept alive): ${reason instanceof Error ? reason.stack : String(reason)}`);
43
+ });
44
+
29
45
  const program = new Command();
30
46
 
31
47
  const HELP_EXAMPLES = `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.40",
3
+ "version": "2.9.42",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -29,7 +29,7 @@
29
29
  "franc": "^6.2.0",
30
30
  "get-port": "^7.1.0",
31
31
  "node-datachannel": "^0.32.0",
32
- "webtorrent": "^2.8.4",
32
+ "webtorrent": "^3.0.16",
33
33
  "ws": "^8.18.2"
34
34
  }
35
35
  }
@@ -55,10 +55,13 @@ const SEEK_SETTLE_MS = 1_200;
55
55
  // burst, so a still-moving scrubber cannot delay a genuine seek forever.
56
56
  const SEEK_SETTLE_MAX_MS = 2_500;
57
57
  // Idle TTL: a session is disposed this long after the last segment/playlist
58
- // access. Kept short so an ffmpeg process does not keep burning CPU after the
59
- // viewer stops or navigates away. Active playback refreshes the timer on every
60
- // segment fetch, so it never expires mid-watch.
61
- const DEFAULT_SESSION_TTL_MS = 120 * 1000;
58
+ // access. Long enough that a viewer who pauses, backgrounds the tab, or briefly
59
+ // turns the phone off can resume WITHOUT a cold ffmpeg restart (the warm session
60
+ // also backs the seamless auto-reconnect). ffmpeg stops producing at the
61
+ // look-ahead cap when idle, so a lingering session costs retained segments on
62
+ // disk, not sustained CPU. Active playback refreshes the timer on every segment
63
+ // fetch, so it never expires mid-watch.
64
+ const DEFAULT_SESSION_TTL_MS = 10 * 60 * 1000;
62
65
  const DEFAULT_STARTUP_WAIT_MS = 5_000;
63
66
  // Realtime budget — runtime downswitch (software encoder only). Periodically
64
67
  // check each active software-transcode session's ffmpeg `speed`; when it stays
@@ -20,9 +20,11 @@ const WEBTORRENT_STORE_ROOT = path.join(os.tmpdir(), "webtorrent");
20
20
 
21
21
  // How long a torrent may sit with zero active file readers before it is
22
22
  // removed (with its on-disk store). Generous so brief gaps between ffmpeg
23
- // range reads — or a short pause do not evict an in-use torrent; a longer
24
- // idle (viewer gone) frees the disk. Re-requesting re-adds (re-downloads) it.
25
- const TORRENT_IDLE_TTL_MS = 300_000;
23
+ // range reads — a pause, a backgrounded tab, or a phone turned off for a few
24
+ // minutes do not evict an in-use torrent's already-downloaded data, so a
25
+ // resume plays from disk instead of re-downloading. A longer idle (viewer truly
26
+ // gone) frees the disk; the global disk cap still evicts earlier under pressure.
27
+ const TORRENT_IDLE_TTL_MS = 15 * 60 * 1000;
26
28
 
27
29
  // Bytes ahead of a read position to mark CRITICAL on each range request. In
28
30
  // WebTorrent, `critical` does NOT reorder the sequential piece scan — it enables
@@ -424,6 +426,7 @@ export class TorrentPool {
424
426
  }
425
427
 
426
428
  const torrentId = decodeTorrentSource(sourceType, source);
429
+
427
430
  const promise = new Promise((resolve, reject) => {
428
431
  const onError = (error) => {
429
432
  this.client.off("error", onError);