@torrent-tv/proxy 2.9.126 → 2.9.127

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.9.127
2
+
3
+ - **New**: A read that hands the file over out of order now says so. A sequential read walks forwards, so each fragment either continues the piece before it or moves to the very next one; anything else means the bytes reaching the decoder are not the file's bytes in order. Measured 2026-08-09 on a 1080p file with an AC-3 track: the encoder ran at 7.7-9.3x, produced its first segment in 9.1 s, reached 00:02:19 of 02:29:58 — and the AC-3 decoder reported "new coupling strategy must be present in block 0", "exponent 26 is out-of-range" and "invalid coupling range" while the piece store showed no spills and 100% of reads served from memory. Video was being COPIED in the same run, so the viewer lost the picture and the sound together: one fault, not two. The bounds check added in 2.9.126 catches a fragment outside the shared pool and stayed silent throughout, so the bytes came from the pool legitimately and belonged somewhere else. This names which piece arrived where.
4
+
1
5
  ## 2.9.126
2
6
 
3
7
  - **Fix**: A read whose offset lies outside its piece pool ends short and says so, instead of taking the whole source down without a word. The pool is a growable `SharedArrayBuffer` shared with the torrent thread, and an offset only means anything against the buffer of the store that produced it; when the two disagreed, building the view threw `RangeError: Invalid typed array length: 8388608` — one piece — which the process-wide handler swallowed. Reads then stopped answering for good. Measured 2026-08-09: ffmpeg was fed cut-up frames and reported them as a broken AC-3 stream ("new coupling strategy must be present in block 0"), no segment could be closed because no audio frames were produced, and segment #305 was held for a minute eight times running while 76 seeders delivered 35 MB/s. The file was fine; the reads were not. The log line now carries the offset, the length and the pool's size, so a recurrence names its own cause instead of being reconstructed from a decoder's complaints.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.126",
3
+ "version": "2.9.127",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -37,6 +37,14 @@ export class TorrentWorkerClient {
37
37
  #caller;
38
38
  /** Receive-side handles for in-flight reads, keyed by request id. */
39
39
  #reads = new Map();
40
+
41
+ /**
42
+ * The last piece served to each open read, so a fragment arriving out of
43
+ * order can be named. Cleared when the read ends.
44
+ *
45
+ * @type {Map<number, number>}
46
+ */
47
+ #lastPieceByRead = new Map();
40
48
  /** Each torrent's piece pool, so a fragment can be read where it lies. */
41
49
  #poolBySource = new Map();
42
50
  /** Which pool an in-flight read belongs to, keyed by request id. */
@@ -104,6 +112,25 @@ export class TorrentWorkerClient {
104
112
  this.#worker.postMessage({ type: Event.FRAGMENT_DONE, id: message.id });
105
113
  break;
106
114
  }
115
+ // A sequential read walks the file forwards, so each fragment either
116
+ // continues the piece before it or moves to the very next one.
117
+ // Anything else means the bytes handed to the decoder are not the
118
+ // file's bytes in order — which is what a decoder complaining about
119
+ // its input has twice turned out to mean (2.9.126, and the AC-3
120
+ // failure of 2026-08-09: the encoder ran at 9.3x, the piece store
121
+ // reported no spills and 100% of reads from memory, and the decoder
122
+ // still saw "new coupling strategy must be present in block 0"). The
123
+ // bounds check catches a fragment outside the pool; this catches one
124
+ // inside it that belongs somewhere else.
125
+ const lastPiece = this.#lastPieceByRead.get(message.id);
126
+ if (lastPiece !== undefined && message.pieceIndex !== lastPiece && message.pieceIndex !== lastPiece + 1) {
127
+ logger.warn(
128
+ `torrent-worker: read ${message.id} jumped from piece ${lastPiece} to ` +
129
+ `${message.pieceIndex} (${message.length}B at pool offset ${message.offset}) — ` +
130
+ "the consumer is being handed the file out of order"
131
+ );
132
+ }
133
+ this.#lastPieceByRead.set(message.id, message.pieceIndex);
107
134
  const view = new Uint8Array(pool, message.offset, message.length);
108
135
 
109
136
  const reader = this.#fragmentReaders.get(message.id);
@@ -132,6 +159,7 @@ export class TorrentWorkerClient {
132
159
  break;
133
160
  }
134
161
  case Event.READ_END:
162
+ this.#lastPieceByRead.delete(message.id);
135
163
  this.#reads.get(message.id)?.close();
136
164
  this.#reads.delete(message.id);
137
165
  this.#fragmentReaders.get(message.id)?.close();
@@ -260,6 +288,7 @@ export class TorrentWorkerClient {
260
288
  void this.#caller.call(Command.CANCEL_READ, { readId }).catch(() => undefined);
261
289
  this.#reads.delete(readId);
262
290
  this.#poolByRead.delete(readId);
291
+ this.#lastPieceByRead.delete(readId);
263
292
  }
264
293
  });
265
294
  this.#reads.set(readId, receive);