@torrent-tv/proxy 2.76.4 → 2.76.6

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,14 @@
1
+ ## 2.76.6
2
+
3
+ - **New**: The proxy says which films it is already downloading, in the same health poll the browser makes before it picks one. A viewer sent to the proxy that has their film costs it the encode and nothing else — the torrent is there, its pieces are there, and where the output is identical they share that too — while the same viewer sent anywhere else starts the download from nothing. Answered from the torrent thread, which is the only truthful record: the main thread keeps stand-ins that are cleared on shutdown alone, so they name films this proxy let go of hours ago. A proxy that cannot answer is scored on its machine as before.
4
+ - **Chore**: Two doc comments in the torrent pool's interface described methods that had been moved or renamed out from under them, leaving the methods below without one. Each is back over its own.
5
+
6
+ ## 2.76.5
7
+
8
+ - **Chore**: What encoding costs this machine is a class of its own (`services/quality/EncodeCost.js`), the third part taken out of the session manager. One subject — seconds of work per second of video — asked about a picture being re-encoded, a soundtrack, a copy, everything running beside the rung being judged, and turned into the list of heights the machine can hold. It holds what an encoder taught it, since it is the only thing that reads those figures, and it is given what it cannot work out for itself: which sessions belong to one file, the host's own readings, how many encoders are running, and what the file costs merely by being fetched. The host is asked at the moment of the question rather than copied when the object is made — the share of the machine that is free is re-read every few seconds, and a copy would price every later rung against a machine that has gone. The manager keeps the three methods that LEARN those costs; moving them is the next step.
9
+ - **Fix**: A rung measured running below realtime is withdrawn even on a host whose startup benchmark is missing. That measurement is the strongest evidence there is about a rung — it ran, and it could not keep up — and it was being discarded along with the prediction it does not depend on. Found by a check written while the arithmetic was being moved.
10
+ - **Chore**: What a session's encoding is doing, as one state, is answered where the run states are defined (`encode/encode-run-state.js`) instead of by three functions in the manager. The boundary of the new directory is a lint rule, like every layer before it, and the rule was checked to fire rather than assumed to.
11
+
1
12
  ## 2.76.4
2
13
 
3
14
  - **Fix**: An encoder was started and killed every five seconds, each one producing 0-2 segments, for as long as anybody watched. Measured on the addon host: a run given no end carries a `to` below its `from` — which is how "no end" is written everywhere here — and two places read that as a number instead. The plan's test for "is anybody waiting for what this run was given" said no, so it was stopped as unwanted; and the stretch it claimed in the coverage map collapsed to a single segment, so the plan saw the rest of the film as free and started another encoder one number along. The two together are the loop. The rule is stated once now (`endOfRun`) and read from that one place, including where a run's own ending is judged.
package/bin/cli.js CHANGED
@@ -473,8 +473,20 @@ try {
473
473
  onSignal(sessionId, signal) {
474
474
  webRtcManager?.handleSignal(sessionId, signal);
475
475
  },
476
- onHealthRequest() {
477
- return collectHealthMetrics();
476
+ async onHealthRequest() {
477
+ // Which films this proxy holds travels with the health poll the browser
478
+ // already makes before it picks one: a viewer sent to a proxy that is
479
+ // downloading their film costs it the encode and nothing else, while the
480
+ // same viewer sent anywhere else starts the download from nothing. Asked
481
+ // of the worker, so a film this proxy let go of is not claimed.
482
+ let holds = [];
483
+ try {
484
+ holds = (await started?.torrentPool?.heldTorrents?.()) ?? [];
485
+ } catch {
486
+ // silent-ok: a proxy that cannot say what it holds is scored on its
487
+ // machine alone, which is what every proxy was scored on until now.
488
+ }
489
+ return { metrics: collectHealthMetrics(), holds };
478
490
  },
479
491
  // Whether this host could sustain a file it has only been told about. The
480
492
  // same arithmetic the first offer uses, against this host's own startup
package/biome.json CHANGED
@@ -49,6 +49,38 @@
49
49
  }
50
50
  }
51
51
  },
52
+ {
53
+ "includes": ["services/quality/**"],
54
+ "linter": {
55
+ "rules": {
56
+ "style": {
57
+ "noRestrictedImports": {
58
+ "level": "error",
59
+ "options": {
60
+ "patterns": [
61
+ {
62
+ "group": [
63
+ "../hls-session-manager.js",
64
+ "../controllers/**",
65
+ "../orchestrators/**",
66
+ "../container/**",
67
+ "../demand/**",
68
+ "../download/**",
69
+ "../piece-store/**",
70
+ "../segment-formats/**",
71
+ "../torrent-worker/**",
72
+ "../viewer/**",
73
+ "../output/**"
74
+ ],
75
+ "message": "What encoding costs is arithmetic over readings. Which sessions belong to one file, how many encoders run, what a file costs to fetch — all of it is passed in, so this can be asked without a session manager, a torrent or a disk."
76
+ }
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ },
52
84
  {
53
85
  "includes": ["services/download/**"],
54
86
  "linter": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.76.4",
3
+ "version": "2.76.6",
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
@@ -378,6 +378,10 @@ export async function startProxyServer({
378
378
  // recorded from a browser request and published from the pool's side, so
379
379
  // resolving one into the other is what lets the two ends agree on what
380
380
  // they are both calling "sourceKey".
381
- sourceRegistry
381
+ sourceRegistry,
382
+ // Which films this host holds, for the health poll the browser makes before
383
+ // it picks a proxy. The pool is on the worker thread and this is the way to
384
+ // it from the process that answers that poll.
385
+ torrentPool
382
386
  };
383
387
  }
@@ -486,3 +486,70 @@ export function declaredEdges() {
486
486
  export function declaredContainment() {
487
487
  return Object.entries(PARENT).map(([state, parent]) => ({ state, parent }));
488
488
  }
489
+
490
+ /**
491
+ * Every run this session has going, earliest first.
492
+ *
493
+ * A session holds as many as the machine affords, because one output can be
494
+ * watched from more than one place: a viewer who jumps back gets a run of their
495
+ * own rather than dragging the picture away from a viewer watching ahead.
496
+ *
497
+ * @param {{ runs?: Set<object> }} session
498
+ * @returns {object[]}
499
+ */
500
+ export function runsOf(session) {
501
+ const runs = session?.runs instanceof Set ? [...session.runs] : [];
502
+ return runs.sort((left, right) => left.from - right.from);
503
+ }
504
+
505
+ /**
506
+ * The runs of this session that still have a process.
507
+ *
508
+ * @param {{ runs?: Set<object> }} session
509
+ * @returns {object[]}
510
+ */
511
+ export function liveRunsOf(session) {
512
+ return runsOf(session).filter((run) => run.isAlive);
513
+ }
514
+
515
+ /**
516
+ * What this session's encoding is doing, as one state.
517
+ *
518
+ * The most advanced state among its live runs: a session with anything
519
+ * producing IS producing, whatever else it has going. A session that has never
520
+ * started a run, or whose runs have all ended, is at the table's own initial
521
+ * state — which is what "nothing is encoding" means.
522
+ *
523
+ * @param {{ runs?: Set<object> }} session
524
+ * @returns {string}
525
+ */
526
+ export function runStateOf(session) {
527
+ let answer = INITIAL_RUN_STATE;
528
+ let rank = -1;
529
+ for (const run of runsOf(session)) {
530
+ const at = RUN_STATE_ORDER.indexOf(run.state);
531
+ if (at > rank) {
532
+ rank = at;
533
+ answer = run.state;
534
+ }
535
+ }
536
+ return answer;
537
+ }
538
+
539
+ /**
540
+ * How advanced a run state is, for reducing several runs to one answer.
541
+ *
542
+ * Producing outranks starting, and both outrank a run that has ended: what a
543
+ * caller asking "what is this session's encoding doing" wants to know is
544
+ * whether anything is being made, not what the quietest of them is up to.
545
+ */
546
+ const RUN_STATE_ORDER = [
547
+ ENCODE_RUN_STATE.ENDED_FAILED,
548
+ ENCODE_RUN_STATE.ENDED_COMPLETE,
549
+ ENCODE_RUN_STATE.STOPPED,
550
+ ENCODE_RUN_STATE.IDLE,
551
+ ENCODE_RUN_STATE.RETRY_WAIT,
552
+ ENCODE_RUN_STATE.SUSPENDED,
553
+ ENCODE_RUN_STATE.STARTING,
554
+ ENCODE_RUN_STATE.PRODUCING
555
+ ];