@torrent-tv/proxy 2.98.5 → 2.98.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.98.5",
3
+ "version": "2.98.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -200,7 +200,7 @@ export async function startProxyServer({
200
200
  // The ONE forward reference here, and it is safe by construction: this fires
201
201
  // only once the worker thread is up and a piece has verified, which is long
202
202
  // after the declarations below have run.
203
- onPiecesArrived: ({ sourceKey, fileIndexes }) => {
203
+ onPiecesArrived: ({ sourceKey, fileIndexes, arrived }) => {
204
204
  for (const fileIndex of fileIndexes) {
205
205
  mediaReads.bytesChanged(sourceKey, fileIndex);
206
206
  probeReads.bytesChanged(sourceKey, fileIndex);
@@ -211,7 +211,9 @@ export async function startProxyServer({
211
211
  // is read again now; nothing was recorded for that wait.
212
212
  keyframeTables.readAgainIfUnanswered({ sourceKey, fileIndex, logName: String(fileIndex) });
213
213
  }
214
- outputParts.encodeInputs?.bytesChanged();
214
+ // Which bytes arrived, when the event names them: an input waiting for
215
+ // named bytes is tried again only when some of them have come.
216
+ outputParts.encodeInputs?.bytesChanged(Array.isArray(arrived) ? { sourceKey, ranges: arrived } : null);
215
217
  }
216
218
  });
217
219
  const selectedPort = await getPort({
@@ -259,10 +259,35 @@ export class EncodeInputs {
259
259
  return this.take(output, from, to);
260
260
  }
261
261
 
262
- bytesChanged() {
262
+ /**
263
+ * Bytes of a source changed: every waiting input is tried again, except one
264
+ * that waits for named bytes none of which have come. Tried at every piece
265
+ * of the file, a seek's input was prepared 28 times in 8.5 s on Home
266
+ * Assistant, each time asking the torrent thread which ranges it holds
267
+ * (171 ms on average), and the try that found its bytes could start only
268
+ * after the one in flight (r34, torrent-tv/meta#178).
269
+ *
270
+ * @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> } | null} [arrived] -
271
+ * The bytes that arrived, inclusive, when the event names them.
272
+ */
273
+ bytesChanged(arrived = null) {
263
274
  this.#revision++;
264
275
  for (const request of this.#requests.values()) {
265
- if (!request.pending && request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
276
+ if (request.pending) {
277
+ // The try in flight may have read before these bytes came. What it
278
+ // waits for is known only when it answers, so they are kept for that
279
+ // answer; bytes not named make it try again whatever it answers.
280
+ if (!arrived) request.unnamedArrival = true;
281
+ else (request.arrivedMeanwhile ??= []).push(arrived);
282
+ continue;
283
+ }
284
+ if (request.result?.kind === "result" || request.result?.kind === "terminal") continue;
285
+ if (arrived && request.result?.kind === "needs-bytes" && !awaitedIn(request.result, arrived)) {
286
+ // Nothing it waits for has come: it stands as tried at this revision.
287
+ request.revision = this.#revision;
288
+ continue;
289
+ }
290
+ this.#prepare(request);
266
291
  }
267
292
  }
268
293
 
@@ -324,6 +349,8 @@ export class EncodeInputs {
324
349
  request.pending = true;
325
350
  request.revision = this.#revision;
326
351
  request.memoryRevision = this.#memoryRevision;
352
+ request.arrivedMeanwhile = null;
353
+ request.unnamedArrival = false;
327
354
  // Where the time from the request to its input goes, across every attempt:
328
355
  // field 2026-10-10, a picture's request took 38 s before its copy began and
329
356
  // nothing said on what (torrent-tv/meta#166).
@@ -365,12 +392,29 @@ export class EncodeInputs {
365
392
  request.pending = false;
366
393
  if (request.result?.kind !== "needs-memory") this.#wanted.delete(request.key);
367
394
  const changed = request.result?.kind === "needs-memory"
368
- ? request.memoryRevision !== this.#memoryRevision : request.revision !== this.#revision;
395
+ ? request.memoryRevision !== this.#memoryRevision : this.#arrivedFor(request);
369
396
  if (this.#requests.get(request.key) === request && changed &&
370
397
  request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
371
398
  });
372
399
  }
373
400
 
401
+ /**
402
+ * Whether bytes came during a try that the try may have missed. A try that
403
+ * waits for named bytes is repeated only when some of them came, or when an
404
+ * event named nothing; the others stand it as tried at this revision. In r36
405
+ * a held-range check took 344 ms, and a piece of the file arriving during it
406
+ * started another (torrent-tv/meta#178).
407
+ */
408
+ #arrivedFor(request) {
409
+ if (request.revision === this.#revision) return false;
410
+ const arrivals = request.arrivedMeanwhile ?? [];
411
+ request.arrivedMeanwhile = null;
412
+ if (request.unnamedArrival || request.result?.kind !== "needs-bytes" ||
413
+ arrivals.some(arrived => awaitedIn(request.result, arrived))) return true;
414
+ request.revision = this.#revision;
415
+ return false;
416
+ }
417
+
374
418
  /** The reservation a packet input makes: all of it, or nothing. */
375
419
  #reserveFor(request) {
376
420
  return async bytes => {
@@ -486,7 +530,9 @@ export class EncodeInputs {
486
530
  // found pieces missing, and the run was refused for good (torrent-tv/meta#178).
487
531
  if (this.#heldRanges !== null && !await this.#present(chosen.sources, held, request.timing)) {
488
532
  const [first] = chosen.sources;
489
- return { kind: "needs-bytes", sourceKey: first.sourceKey, fileIndex: first.fileIndex, ranges: first.input.ranges };
533
+ return { kind: "needs-bytes", sourceKey: first.sourceKey, fileIndex: first.fileIndex, ranges: first.input.ranges,
534
+ // Every source of the stretch, so the bytes of any of them wake it.
535
+ awaiting: chosen.sources.map(source => ({ sourceKey: source.sourceKey, fileIndex: source.fileIndex, ranges: source.input.ranges })) };
490
536
  }
491
537
  if (this.#requests.get(request.key) !== request) return undefined;
492
538
  this.#wanted.set(request.key, { wanted: chosen.bytes, required: chosen.bytes });
@@ -527,6 +573,21 @@ export class EncodeInputs {
527
573
  }
528
574
  }
529
575
 
576
+ /**
577
+ * Whether some of the bytes an input waits for are among those that arrived.
578
+ *
579
+ * @param {{ sourceKey: string, fileIndex: number, ranges: Array<[number, number]>,
580
+ * awaiting?: Array<{ sourceKey: string, fileIndex: number, ranges: Array<[number, number]> }> }} waiting
581
+ * @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> }} arrived
582
+ * @returns {boolean}
583
+ */
584
+ function awaitedIn(waiting, arrived) {
585
+ const sources = waiting.awaiting ?? [waiting];
586
+ return sources.some(source => source.sourceKey === arrived.sourceKey && Array.isArray(source.ranges) &&
587
+ arrived.ranges.some(range => range.fileIndex === source.fileIndex &&
588
+ source.ranges.some(([start, end]) => range.start <= end && range.end >= start)));
589
+ }
590
+
530
591
  /**
531
592
  * Where a request has spent its time since it was asked, as one clause of its
532
593
  * log line. The resolutions include the reads of the file they made, and those
@@ -1300,6 +1300,7 @@ export class TorrentPool {
1300
1300
  // depend on facts that are gone by the time it is said.
1301
1301
  if (Array.isArray(zones) && zones.length === 0) {
1302
1302
  withdrawPriorityMap(torrent, fileIndex);
1303
+ reconcileAll();
1303
1304
  // The ends of the file go with it. They are kept for as long as the file
1304
1305
  // is open, and this is what says it is not.
1305
1306
  // And this may have been the last thing anybody wanted of this torrent.
@@ -1346,6 +1347,11 @@ export class TorrentPool {
1346
1347
  // Bands the map no longer has: a viewer moved on, and what they had wanted
1347
1348
  // is not wanted by anybody now.
1348
1349
  withdrawPriorityMap(torrent, fileIndex, ordered.length);
1350
+ // THE SWARM IS TOLD WHEN THE MAP IS APPLIED, not at the next piece verified
1351
+ // or the pool's timer: on Home Assistant a seek's map was applied and the
1352
+ // swarm went on fetching the old first class for 1.16 s, until the next
1353
+ // piece happened to be verified (r34, torrent-tv/meta#178).
1354
+ reconcileAll();
1349
1355
  // WHAT THE SWARM WAS ACTUALLY TOLD, said on change and never on a timer.
1350
1356
  // The map was applied in silence: that it had been BUILT was visible in the
1351
1357
  // encoding's own line, and that the download had received it was visible
@@ -254,7 +254,8 @@ export class TorrentWorkerClient {
254
254
  this.#onPiecesArrived({
255
255
  sourceKey: message.sourceKey,
256
256
  fileIndexes: Array.isArray(message.fileIndexes) ? message.fileIndexes : [],
257
- ...(typeof message.reason === "string" ? { reason: message.reason } : {})
257
+ ...(typeof message.reason === "string" ? { reason: message.reason } : {}),
258
+ ...(Array.isArray(message.arrived) ? { arrived: message.arrived } : {})
258
259
  });
259
260
  break;
260
261
  default:
@@ -881,12 +881,36 @@ function describePieceBuffers() {
881
881
  * @param {object} torrent
882
882
  * @returns {void}
883
883
  */
884
- function announceArrivals(sourceKey, torrent, reason = "verified") {
884
+ function announceArrivals(sourceKey, torrent, reason = "verified", piece = null) {
885
885
  const fileIndexes = [...demandFor(torrent).register.files()];
886
886
  if (fileIndexes.length === 0) {
887
887
  return;
888
888
  }
889
- parentPort.postMessage({ type: Event.PIECES_CHANGED, sourceKey, fileIndexes, reason });
889
+ parentPort.postMessage({ type: Event.PIECES_CHANGED, sourceKey, fileIndexes, reason,
890
+ ...(Number.isSafeInteger(piece) ? { arrived: bytesOfPiece(torrent, piece) } : {}) });
891
+ }
892
+
893
+ /**
894
+ * The bytes of one piece, as each file it covers counts them: what a reader
895
+ * waiting for named bytes compares against, so a piece of another part of the
896
+ * file wakes nobody (torrent-tv/meta#178).
897
+ *
898
+ * @param {object} torrent
899
+ * @param {number} piece
900
+ * @returns {Array<{ fileIndex: number, start: number, end: number }>} Inclusive.
901
+ */
902
+ function bytesOfPiece(torrent, piece) {
903
+ const pieceLength = Number(torrent.pieceLength);
904
+ const length = Number(torrent.length);
905
+ if (!(pieceLength > 0) || !(length > 0)) return [];
906
+ const start = piece * pieceLength;
907
+ const end = Math.min(length, start + pieceLength) - 1;
908
+ const arrived = [];
909
+ (torrent.files ?? []).forEach((file, fileIndex) => {
910
+ const from = Math.max(start, file.offset), to = Math.min(end, file.offset + file.length - 1);
911
+ if (to >= from) arrived.push({ fileIndex, start: from - file.offset, end: to - file.offset });
912
+ });
913
+ return arrived;
890
914
  }
891
915
 
892
916
 
@@ -917,7 +941,7 @@ function ensureArrivalsWired(sourceKey, torrent) {
917
941
  }
918
942
  arrivalsWired.add(torrent);
919
943
  holdingsChanged();
920
- torrent.on("verified", () => announceArrivals(sourceKey, torrent));
944
+ torrent.on("verified", (piece) => announceArrivals(sourceKey, torrent, "verified", piece));
921
945
  torrent.on("piece-withdrawn", () => announceArrivals(sourceKey, torrent, "withdrawn"));
922
946
  torrent.once("close", () => {
923
947
  if (resolvedTorrents.get(sourceKey) !== torrent) return;