@torrent-tv/proxy 2.55.6 → 2.55.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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2.55.8
2
+
3
+ - **Fix**: The proxy died twice in one evening (2026-08-22, 15:50:12 and 16:12:21 UTC) with no stop order given, and both deaths are the same fault. The torrent worker thread ends itself when its event loop drains — every recurring interval there is unref'd, upload is disabled by default, and idle peer connections close about half a minute after the traffic stops — so when a viewer paused or left, the thread finished ~35 s later on its own and Node began tearing it down. That teardown touched memory already freed or overwritten (SIGSEGV inside `uv_timer_stop`, reached through `PerIsolatePlatformData::Shutdown`; two core dumps captured identical stacks), and a fault in any thread kills the whole process instantly — HTTP server, tunnel and data channels together, with no log line and no way to restart anything from inside. The HA supervisor restarted the container each time (~15 s), but the browser's reconnect ladder had already given up by then. The worker now keeps ONE interval accounted for (no `.unref()`): an empty tick every 5 s costs nothing, the event loop can never drain while the process lives, and the teardown path — with whatever structure is corrupted inside it — stays unreachable, regardless of which native module is guilty; the three earlier crashes of this family (2026-08-18..21) stay documented under roadmap item 1.
4
+
5
+ ## 2.55.7
6
+
7
+ - **Fix**: The subtitle push subscription is now recorded under the torrent pool's own key, not the browser's registry key — the two are different strings whenever a source was added by a `.torrent` file (a `.torrent` and a magnet for the same film are different request bytes, hashed into different registry keys, but the SAME infohash) and were silently different in every other case too: a registry key is `sha1(sourceType:source)`, one per API session; the pool's key is `torrent:<infohash>`, deliberately shared across a magnet and a `.torrent` for the same content (item 10). The diagnostic logging added in 2.55.6 caught it directly, field case 2026-08-22: cues were found and logged repeatedly, and every push answered `found no subscribed channel` — the subscription and the publish had never been able to agree on a key, for any torrent, since the push feature shipped in 2.55.5. `data-channel-handler.js` now resolves the browser's registry key through `sourceRegistry` to `(sourceType, source)` and runs it through the same `deriveSourceKey` the pool itself uses, at the one point both keys are in hand — the subscribe intercept, before the request is even forwarded.
8
+
1
9
  ## 2.55.6
2
10
 
3
11
  - **Chore**: Every step of the subtitle push chain now logs on success, not only on failure. Field report 2026-08-22, playing `Minions.and.Monsters.1080p.mkv`: a track was switched on over a minute after the seed fetch found nothing (`bytes=7`, an empty `WEBVTT` — expected, the torrent had barely started), and no cues appeared. The proxy log carried no evidence either way — `warmActiveFiles` posted `Event.SUBTITLE_CUES_READY` silently, `publishSubtitleCues` sent (or found no subscriber for) a push silently, and `subscribeSubtitles` registered a channel silently. Confirmed separately by reading WebTorrent's own source that `verified` fires on every live piece completion (`_markVerified` inside `store.put`'s callback in `torrent.js`, not only at startup), so the event source itself is real; what could not be told apart without these lines is subscription, discovery, and delivery. Logs now name each: `subtitle push: channel subscribed to …`, `… cue(s) found, posting to main thread`, `… sent N cue(s) … to M/T channel(s)` (or `found no subscribed channel`).
package/bin/cli.js CHANGED
@@ -300,6 +300,7 @@ try {
300
300
  });
301
301
  app = started.app;
302
302
  actualPort = started.port;
303
+ const sourceRegistry = started.sourceRegistry;
303
304
  const directBaseUrl = explicitBaseUrl || `http://${bindHost}:${actualPort}`;
304
305
 
305
306
  // A native fault writes the whole address space out — 4.18 GB each on the
@@ -421,6 +422,10 @@ try {
421
422
  dataChannelHandler = createDataChannelHandler({
422
423
  proxyPort: actualPort,
423
424
  onLog: (message) => logger.info(message),
425
+ // Resolves a browser's registry sourceKey to the torrent pool's own key
426
+ // (the content's infohash) so the subtitle push subscription and the
427
+ // pool's own publish agree on what a source is called. See server.js.
428
+ sourceRegistry,
424
429
  // Lets a stuck send queue ask the transport what it is doing. Late-bound:
425
430
  // the manager is created below, with this handler already in hand.
426
431
  getTransportSnapshot: (sessionId) => webRtcManager?.getTransportSnapshot(sessionId) ?? null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.55.6",
3
+ "version": "2.55.8",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
package/server.js CHANGED
@@ -290,6 +290,14 @@ export async function startProxyServer({
290
290
  await app.listen({ host, port: selectedPort });
291
291
  return {
292
292
  app,
293
- port: selectedPort
293
+ port: selectedPort,
294
+ // The browser only ever knows a source by its REGISTRY key (a hash of the
295
+ // raw request bytes, scoped to one API session) — never the torrent
296
+ // pool's own key (the content's infohash, shared across a magnet and a
297
+ // `.torrent` naming the same film). The subtitle push subscription is
298
+ // recorded from a browser request and published from the pool's side, so
299
+ // resolving one into the other is what lets the two ends agree on what
300
+ // they are both calling "sourceKey".
301
+ sourceRegistry
294
302
  };
295
303
  }
@@ -43,6 +43,8 @@
43
43
 
44
44
  /** @import { DataChannel } from 'node-datachannel' */
45
45
 
46
+ import { deriveSourceKey } from "./torrent-source-key.js";
47
+
46
48
  /**
47
49
  * Configuration for the data channel handler.
48
50
  *
@@ -299,7 +301,7 @@ export function encodeFrame(idBytes, bytes, done) {
299
301
  return frame;
300
302
  }
301
303
 
302
- export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapshot }) {
304
+ export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapshot, sourceRegistry }) {
303
305
  /**
304
306
  * Channels currently interested in one file's subtitle cues, keyed by
305
307
  * `sourceKey:fileIndex`. Populated the moment a browser asks for an
@@ -579,13 +581,34 @@ export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapsho
579
581
  // this walks incrementally). `fileIndex` alone would also scope this to
580
582
  // the wrong grain for the real case — a torrent can carry several playable
581
583
  // files — so the pair is what a push is ever addressed to.
584
+ //
585
+ // The browser's `sourceKey` is a REGISTRY key — a hash of the raw request
586
+ // bytes, one per (magnet-or-.torrent, this API session). The torrent pool
587
+ // publishes under its OWN key — the content's infohash, deliberately the
588
+ // SAME for a magnet and a `.torrent` naming the same film, so the two
589
+ // share one swarm (item 10). The two are different strings for the same
590
+ // torrent whenever a source was added by its `.torrent` file (a `.torrent`
591
+ // and a magnet are different request bytes, same infohash) — subscribing
592
+ // under the registry key found no publisher for that reason, not because
593
+ // nothing was ever read: field case 2026-08-22, cues were found and
594
+ // logged, every push answered "found no subscribed channel". Resolved to
595
+ // the pool's key here, the one place both are in hand.
582
596
  if (path === "/api/subtitles" && typeof query === "string") {
583
597
  const params = new URLSearchParams(query);
584
- const sourceKey = params.get("sourceKey");
598
+ const registrySourceKey = params.get("sourceKey");
585
599
  const fileIndex = Number(params.get("fileIndex"));
586
600
  const hasTrackIndex = params.get("trackIndex") !== null && params.get("trackIndex") !== "";
587
- if (sourceKey && Number.isInteger(fileIndex) && hasTrackIndex) {
588
- subscribeSubtitles(sourceKey, fileIndex, channel);
601
+ if (registrySourceKey && Number.isInteger(fileIndex) && hasTrackIndex) {
602
+ const record = sourceRegistry?.get(registrySourceKey);
603
+ if (record) {
604
+ try {
605
+ const poolSourceKey = await deriveSourceKey(record.sourceType, record.source);
606
+ subscribeSubtitles(poolSourceKey, fileIndex, channel);
607
+ } catch (error) {
608
+ log(`[dc] subtitle push: could not resolve ${registrySourceKey.slice(0, 8)} to a pool key: ` +
609
+ `${error instanceof Error ? error.message : error}`);
610
+ }
611
+ }
589
612
  }
590
613
  }
591
614
 
@@ -590,4 +590,31 @@ setInterval(() => {
590
590
  }
591
591
  }, SUBTITLE_WARMUP_INTERVAL_MS).unref();
592
592
 
593
+ /**
594
+ * Keeps this thread alive ON PURPOSE — the one interval left accounted for
595
+ * (no `.unref()`).
596
+ *
597
+ * Every other recurring handle here is unref'd, upload is disabled by
598
+ * default, and idle peer connections close about half a minute after the
599
+ * traffic stops — so once nothing is being read, every handle can be gone
600
+ * at once, the event loop drains, and this thread ends BY ITSELF. Node then
601
+ * tears the isolate down, that teardown touches memory some native module
602
+ * has already freed, and the fault (SIGSEGV inside `uv_timer_stop`, reached
603
+ * through `PerIsolatePlatformData::Shutdown`) kills the whole process at
604
+ * once — HTTP server, tunnel, data channels — before any JS handler runs.
605
+ * Field evidence: two deaths on 2026-08-22 (15:50:12 and 16:12:21 UTC),
606
+ * each ~35 s after the last byte of traffic, identical core dumps;
607
+ * same crash family as the utp-native faults of 2026-08-18..21
608
+ * (`research/worker-thread-drain-crash-2026-08-22.md`).
609
+ *
610
+ * An empty repeating interval costs nothing, keeps the loop from draining
611
+ * while the process lives, and thereby keeps that teardown path — and the
612
+ * corrupted structure inside it — unreachable, whichever module is guilty.
613
+ */
614
+ const WORKER_KEEPALIVE_INTERVAL_MS = 5_000;
615
+
616
+ setInterval(() => {
617
+ void process.uptime();
618
+ }, WORKER_KEEPALIVE_INTERVAL_MS);
619
+
593
620
  log("torrent worker started");