@torrent-tv/proxy 2.104.0 → 2.104.2

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.104.0",
3
+ "version": "2.104.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -1520,6 +1520,11 @@ export class TorrentPool {
1520
1520
  const context = this.#waitTraceByTorrent.get(torrent);
1521
1521
  if (context?.trace === true) context.emitWaitTrace?.(context.sourceKey, process, phase, fields);
1522
1522
  };
1523
+ if (waitTrace?.trace === true) {
1524
+ // WebTorrent assigns the store during _onStore, after client.add first
1525
+ // returns. Revisit this hook when its ready callback calls us.
1526
+ tracePieceStorePut(pieceStoreOf(torrent), trace);
1527
+ }
1523
1528
  const pool = this;
1524
1529
  const wrap = (method, process) => {
1525
1530
  const original = torrent[method];
@@ -1564,16 +1569,28 @@ export class TorrentPool {
1564
1569
  this.#waitTraceHooks.add(torrent);
1565
1570
  wrap("_startDiscovery", "W2");
1566
1571
  wrap("_onStore", "W6");
1567
- tracePieceStorePut(pieceStoreOf(torrent), trace);
1568
1572
  const verify = torrent._verifyPieces;
1569
1573
  if (typeof verify === "function") {
1570
1574
  torrent._verifyPieces = function (callback, ...args) {
1571
1575
  const startedAt = performance.now();
1572
1576
  trace("W5", "begin", { startedAt });
1573
- return verify.call(this, (...results) => {
1574
- trace("W5", "end", { startedAt, endedAt: performance.now(), outcome: "verified" });
1575
- callback?.(...results);
1576
- }, ...args);
1577
+ let finished = false;
1578
+ const finish = (outcome, fields = {}) => {
1579
+ if (finished) return;
1580
+ finished = true;
1581
+ const endedAt = performance.now();
1582
+ trace("W5", "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome, ...fields });
1583
+ };
1584
+ try {
1585
+ return verify.call(this, (...results) => {
1586
+ const error = results[0];
1587
+ finish(error ? "error" : "verified", error ? { error: String(error?.message ?? error) } : {});
1588
+ callback?.(...results);
1589
+ }, ...args);
1590
+ } catch (error) {
1591
+ finish("error", { error: String(error?.message ?? error) });
1592
+ throw error;
1593
+ }
1577
1594
  };
1578
1595
  }
1579
1596
  const timing = this.#swarmTimingByTorrent.get(torrent);
@@ -1,7 +1,9 @@
1
1
  /** Record the torrent worker's verified-piece storage write without changing its callback API. */
2
+ const tracedStores = new WeakSet();
3
+
2
4
  export function tracePieceStorePut(store, trace, now = () => performance.now()) {
3
5
  const put = store?.put;
4
- if (typeof put !== "function") return false;
6
+ if (!store || tracedStores.has(store) || typeof put !== "function") return false;
5
7
 
6
8
  store.put = function (index, bytes, callback) {
7
9
  const startedAt = now();
@@ -24,5 +26,6 @@ export function tracePieceStorePut(store, trace, now = () => performance.now())
24
26
  throw error;
25
27
  }
26
28
  };
29
+ tracedStores.add(store);
27
30
  return true;
28
31
  }
@@ -494,46 +494,63 @@ async function runCommand(command, params, id) {
494
494
  const statsStartedAt = performance.now();
495
495
  emitWaitTrace(params.sourceKey, "W10", "begin", { startedAt: statsStartedAt,
496
496
  fileIndex: params.fileIndex, forecast: params.forecast === true });
497
- const torrent = await knownTorrent(params.sourceKey);
498
- if (!torrent) {
499
- const whole = wholeSources.fileOf(params.sourceKey, params.fileIndex);
500
- emitWaitTrace(params.sourceKey, "W10", "end", { startedAt: statsStartedAt,
501
- endedAt: performance.now(), outcome: whole ? "whole-file" : "pending", fileIndex: params.fileIndex });
502
- return whole ? wholeFileStats(whole.length) : { pending: true };
503
- }
504
- // What this file's own interruptions demand, measured by the reader in
505
- // this thread. It travels with the stats because the caller asking for
506
- // them is the one that has to decide with them — the browser's smallest
507
- // safe buffer, and the speed a quality step must sustain. Null until a
508
- // second interruption has been seen: one wait shows no interval, and an
509
- // interval invented from one point is exactly what this work removes.
510
- const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
511
- const forecastAt = performance.now();
512
- const forecast = file && params.forecast === true ? downloadForecastOf(torrent) : null;
513
- const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
514
- const stats = pool.getFileStats(torrent, params.fileIndex);
515
- const ranges = [];
516
- if (forecast) for (const [piece, availableAt] of forecast.arrivals) {
517
- const start = piece * torrent.pieceLength;
518
- const end = Math.min(torrent.length, (piece + 1) * torrent.pieceLength);
519
- if (end > start) ranges.push({ start, end, availableAt });
520
- }
521
- emitWaitTrace(params.sourceKey, "W10", "end", { startedAt: statsStartedAt,
522
- endedAt: performance.now(), outcome: "ok", fileIndex: params.fileIndex,
523
- forecast: params.forecast === true });
524
- return {
525
- ...stats,
526
- ...(params.forecast === true ? { downloadForecast: {
527
- measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges
528
- } } : {}),
529
- supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4),
530
- // How long this answer waited for the arrival forecast, which the
531
- // caller says beside its own time (ViewerRequests, torrent-tv/meta#178).
532
- forecastMs
497
+ const finishStatsTrace = (outcome, fields = {}) => {
498
+ const endedAt = performance.now();
499
+ emitWaitTrace(params.sourceKey, "W10", "end", { startedAt: statsStartedAt, endedAt,
500
+ durationMs: endedAt - statsStartedAt, outcome, fileIndex: params.fileIndex, ...fields });
533
501
  };
502
+ try {
503
+ const torrent = await knownTorrent(params.sourceKey);
504
+ if (!torrent) {
505
+ const whole = wholeSources.fileOf(params.sourceKey, params.fileIndex);
506
+ finishStatsTrace(whole ? "whole-file" : "pending");
507
+ return whole ? wholeFileStats(whole.length) : { pending: true };
508
+ }
509
+ // What this file's own interruptions demand, measured by the reader in
510
+ // this thread. It travels with the stats because the caller asking for
511
+ // them is the one that has to decide with them — the browser's smallest
512
+ // safe buffer, and the speed a quality step must sustain. Null until a
513
+ // second interruption has been seen: one wait shows no interval, and an
514
+ // interval invented from one point is exactly what this work removes.
515
+ const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
516
+ const forecastAt = performance.now();
517
+ const forecast = file && params.forecast === true ? downloadForecastOf(torrent) : null;
518
+ const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
519
+ const stats = pool.getFileStats(torrent, params.fileIndex);
520
+ const ranges = [];
521
+ if (forecast) for (const [piece, availableAt] of forecast.arrivals) {
522
+ const start = piece * torrent.pieceLength;
523
+ const end = Math.min(torrent.length, (piece + 1) * torrent.pieceLength);
524
+ if (end > start) ranges.push({ start, end, availableAt });
525
+ }
526
+ finishStatsTrace("ok", { forecast: params.forecast === true });
527
+ return {
528
+ ...stats,
529
+ ...(params.forecast === true ? { downloadForecast: {
530
+ measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges
531
+ } } : {}),
532
+ supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4),
533
+ // How long this answer waited for the arrival forecast, which the
534
+ // caller says beside its own time (ViewerRequests, torrent-tv/meta#178).
535
+ forecastMs
536
+ };
537
+ } catch (error) {
538
+ finishStatsTrace("error", { error: String(error?.message ?? error) });
539
+ throw error;
540
+ }
534
541
  }
535
542
 
536
543
  case Command.PRIORITY_MAP: {
544
+ const startedAt = performance.now();
545
+ emitWaitTrace(params.sourceKey, "W7", "begin", { startedAt, fileIndex: params.fileIndex });
546
+ let traceFinished = false;
547
+ const finishPriorityTrace = (outcome, fields = {}) => {
548
+ if (traceFinished) return;
549
+ traceFinished = true;
550
+ const endedAt = performance.now();
551
+ emitWaitTrace(params.sourceKey, "W7", "end", { startedAt, endedAt,
552
+ durationMs: endedAt - startedAt, outcome, fileIndex: params.fileIndex, ...fields });
553
+ };
537
554
  // When this handler began, on the clock the main thread posted by: the
538
555
  // post to here holds the transfer and this thread's queue together
539
556
  // (torrent-tv/meta#177).
@@ -542,20 +559,23 @@ async function runCommand(command, params, id) {
542
559
  // only update an existing source, never revive one after its viewer left.
543
560
  const zones = Array.isArray(params.zones) ? params.zones : [];
544
561
  // Nor may one for a file held whole, which has nothing left to fetch.
545
- const torrent = await knownTorrent(params.sourceKey);
546
- if (!torrent) {
547
- return { appliedMs: null, receivedAt, repliedAt: performance.timeOrigin + performance.now() };
562
+ try {
563
+ const torrent = await knownTorrent(params.sourceKey);
564
+ if (!torrent) {
565
+ finishPriorityTrace("source-not-ready");
566
+ return { appliedMs: null, receivedAt, repliedAt: performance.timeOrigin + performance.now() };
567
+ }
568
+ // What applying a map costs this thread, told to whoever sent it: every
569
+ // zone becomes a window of the register and the swarm is told again
570
+ // (torrent-tv/meta#166).
571
+ ensureArrivalsWired(params.sourceKey, torrent);
572
+ pool.applyPriorityMap(torrent, params.fileIndex, zones, params.durationSeconds);
573
+ torrent.emit("priority-map-changed", params.fileIndex);
574
+ finishPriorityTrace("applied");
575
+ } catch (error) {
576
+ finishPriorityTrace("error", { error: String(error?.message ?? error) });
577
+ throw error;
548
578
  }
549
- // What applying a map costs this thread, told to whoever sent it: every
550
- // zone becomes a window of the register and the swarm is told again
551
- // (torrent-tv/meta#166).
552
- const startedAt = performance.now();
553
- emitWaitTrace(params.sourceKey, "W7", "begin", { fileIndex: params.fileIndex });
554
- ensureArrivalsWired(params.sourceKey, torrent);
555
- pool.applyPriorityMap(torrent, params.fileIndex, zones, params.durationSeconds);
556
- torrent.emit("priority-map-changed", params.fileIndex);
557
- emitWaitTrace(params.sourceKey, "W7", "end", { startedAt, endedAt: performance.now(),
558
- fileIndex: params.fileIndex });
559
579
  return { appliedMs: performance.now() - startedAt, receivedAt, repliedAt: performance.timeOrigin + performance.now() };
560
580
  }
561
581