@torrent-tv/proxy 2.95.5 → 2.95.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.95.5",
3
+ "version": "2.95.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": {
@@ -665,14 +665,17 @@ export class SharedPieceStore {
665
665
  * range to declare.
666
666
  */
667
667
  get wantedBytes() {
668
- // Download preferences may use spare RAM, but cannot become a required
669
- // floor or a retained reading window after the map has been withdrawn.
670
- const demand = this.#lru.demand();
668
+ // MEMORY HOLDS WHAT IS READ; WHAT IS DOWNLOADED AHEAD HAS THE DISK. Download
669
+ // preferences order eviction and nothing else. Counted here, the map of a
670
+ // whole film asked for the whole film, the adaptive budget handed over all
671
+ // the memory the machine had free, and once the swarm was told the whole
672
+ // band the store filled 4.4 GB in five minutes and the kernel killed the
673
+ // process (Home Assistant, 2026-10-07, torrent-tv/meta#95).
671
674
  const required = this.#lru.demand({ memoryRequiredOnly: true });
672
675
  const pinned = this.#lru.pinnedCount;
673
- if (demand.readers > 0) {
676
+ if (required.readers > 0) {
674
677
  this.#widestSeenPieces = Math.max(this.#widestSeenPieces, required.widestPieces);
675
- const pieces = Math.max(MIN_RESIDENT_PIECES, demand.unionPieces, demand.widestPieces, pinned);
678
+ const pieces = Math.max(MIN_RESIDENT_PIECES, required.unionPieces, required.widestPieces, pinned);
676
679
  // Plus room to absorb what arrives while a write is finishing. Asking for
677
680
  // exactly what the readers want leaves no free place ever, so every
678
681
  // arrival evicts one of them — measured 2026-09-02: `6 reader(s) want 23
@@ -17,14 +17,28 @@
17
17
  * vendored 2.8.5: selections are sorted by priority only when one is inserted,
18
18
  * and `shufflePriority` then moves the selection just served to the back of the
19
19
  * whole non-zero group. Distinct numbers therefore order the list once and
20
- * round-robin it afterwards. So the ordering is kept HERE, by choosing what to
21
- * state at all, and the library is given only the distinction it honours:
22
- * non-zero for what is wanted now, zero for the speculative tail.
20
+ * round-robin it afterwards. The library honours one distinction: non-zero
21
+ * before zero. A wire with nothing left to take from the non-zero selections
22
+ * goes on to the zero ones, and within a selection it takes the earliest piece
23
+ * it holds.
24
+ *
25
+ * **So everything wanted is stated, and only the first class is non-zero.**
26
+ * The first class is the most urgent level that is still missing anything, at
27
+ * its highest priority, whatever its deadlines. Everything else that is not
28
+ * speculative is stated at zero, so it is fetched only by a wire that would
29
+ * otherwise stand idle. Stating only the earliest deadline, as this did after
30
+ * #95, left every peer that held none of that segment's pieces without a
31
+ * request: on Home Assistant, with three peers each holding every third piece
32
+ * and one complete slow peer, twelve segments took 184 s at 0.61 MiB/s against
33
+ * 18.3 s at 6.1 MiB/s when the whole band was stated
34
+ * (`research/download-translation-2026-10-07.md`, torrent-tv/meta#95).
35
+ * Speculative levels stay withheld while anything urgent is missing anywhere,
36
+ * because the link is shared.
23
37
  */
24
38
 
25
39
  import {
40
+ isConditional,
26
41
  piecesOf,
27
- selectionPriority,
28
42
  Urgency,
29
43
  urgencyName
30
44
  } from "../demand/index.js";
@@ -68,39 +82,40 @@ export class SwarmSelection {
68
82
  * torrent is still waiting for something urgent. The registry works it out
69
83
  * once and hands the same answer to every selection, because the link is
70
84
  * shared and the question is not a per-torrent one.
85
+ * @param {{ urgency: number, priority: number } | null} [options.firstClass] -
86
+ * The most urgent missing level and its highest priority across every
87
+ * torrent. Without it, this torrent's own.
71
88
  * @returns {{ stated: number, withdrawn: number }}
72
89
  */
73
- reconcile({ speculativeAllowed = true, maximumUrgency = Infinity, minimumPriority = 1, latestDeadlineAt = Infinity } = {}) {
74
- const windows=this.#register.windows();
75
- const missing=windows.filter(window=>!this.#isSatisfied(window));
76
- const urgency=missing.length?Math.min(...missing.map(window=>window.urgency)):null;
77
- const eligible=missing.filter(window=>window.urgency===urgency);
78
- const priority=eligible.length?Math.max(...eligible.map(window=>window.priority)):null;
79
- const ranked=eligible.filter(window=>window.priority===priority);
80
- const deadlineAt=ranked.length?Math.min(...ranked.map(window=>window.deadlineAt)):Infinity;
81
- const active=urgency===null||urgency>maximumUrgency||priority<minimumPriority||deadlineAt>latestDeadlineAt||(!speculativeAllowed&&urgency>=Urgency.TAIL)?[]:
82
- ranked.filter(window=>window.deadlineAt===deadlineAt);
90
+ reconcile({ speculativeAllowed = !this.hasUrgentMissing(), firstClass = null } = {}) {
91
+ const missing = this.#register.windows().filter(window => !this.#isSatisfied(window));
92
+ const first = firstClass ?? this.missingBand();
83
93
  /** @type {Map<string, { from: number, to: number, priority: number }>} */
84
94
  const wanted = new Map();
85
- for (const window of active) {
95
+ for (const priority of [1, 0]) {
96
+ const ranges = [];
97
+ for (const window of missing) {
98
+ if (!speculativeAllowed && isConditional(window.urgency)) continue;
99
+ // Speculative levels are never the first class: the library keeps
100
+ // zero-priority selections last and never rotates them above.
101
+ const inFirstClass = first !== null && !isConditional(window.urgency)
102
+ && window.urgency === first.urgency && window.priority >= first.priority;
103
+ if ((inFirstClass ? 1 : 0) !== priority) continue;
86
104
  const range = this.#piecesFor(window);
87
- if (!range) {
88
- continue;
89
- }
90
- // Merged by range and priority, not by claimant: two readers wanting
91
- // the same pieces are one instruction to the swarm.
92
- const key = `${range.from}-${range.to}-${selectionPriority(window.urgency)}`;
93
- wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(window.urgency) });
105
+ if (range) ranges.push(range);
106
+ }
107
+ // Merged within one priority: two readers wanting the same or adjacent
108
+ // pieces are one instruction, and an unchanged map restates nothing.
109
+ ranges.sort((a, b) => a.from - b.from);
110
+ const union = [];
111
+ for (const range of ranges) {
112
+ const previous = union.at(-1);
113
+ if (previous && range.from <= previous.to + 1) previous.to = Math.max(previous.to, range.to);
114
+ else union.push({ from: range.from, to: range.to, priority });
115
+ }
116
+ for (const range of union) wanted.set(`${range.from}-${range.to}-${priority}`, range);
94
117
  }
95
118
 
96
- // Public selections merge adjacent ranges. Publish their union so an
97
- // unchanged map does not repeatedly replace the library's merged range.
98
- const ordered=[...wanted.values()].sort((a,b)=>a.from-b.from);
99
- wanted.clear();
100
- const union=[];
101
- for(const range of ordered){const previous=union.at(-1);if(previous&&range.from<=previous.to+1)previous.to=Math.max(previous.to,range.to);else union.push({...range});}
102
- for(const range of union)wanted.set(`${range.from}-${range.to}-${range.priority}`,range);
103
-
104
119
  let withdrawn = 0;
105
120
  for (const [key, range] of [...this.#stated]) {
106
121
  if (wanted.has(key)) {
@@ -116,16 +116,18 @@ export function reconcileAll() {
116
116
  if (publishingSelections) return { torrents: live.size, speculativeAllowed: false, stated: 0, withdrawn: 0 };
117
117
  const entries = [...live];
118
118
  const speculativeAllowed = !entries.some((entry) => entry.selection.hasUrgentMissing());
119
- const bands=entries.map(entry=>entry.selection.missingBand()).filter(Boolean);
120
- const maximumUrgency=bands.length?Math.min(...bands.map(band=>band.urgency)):Infinity;
121
- const minimumPriority=bands.length?Math.max(...bands.filter(band=>band.urgency===maximumUrgency).map(band=>band.priority)):1;
122
- const latestDeadlineAt=bands.length?Math.min(...bands.filter(band=>band.urgency===maximumUrgency&&band.priority===minimumPriority).map(band=>band.deadlineAt)):Infinity;
119
+ // The first class across every torrent: the most urgent missing level and
120
+ // its highest priority. Each selection states it non-zero and the rest zero.
121
+ const bands = entries.map(entry => entry.selection.missingBand()).filter(Boolean);
122
+ const urgency = bands.length ? Math.min(...bands.map(band => band.urgency)) : null;
123
+ const firstClass = urgency === null ? null
124
+ : { urgency, priority: Math.max(...bands.filter(band => band.urgency === urgency).map(band => band.priority)) };
123
125
  let stated = 0;
124
126
  let withdrawn = 0;
125
127
  publishingSelections = true;
126
128
  try {
127
129
  for (const entry of entries) {
128
- const result = entry.selection.reconcile({ speculativeAllowed, maximumUrgency, minimumPriority, latestDeadlineAt });
130
+ const result = entry.selection.reconcile({ speculativeAllowed, firstClass });
129
131
  stated += result.stated;
130
132
  withdrawn += result.withdrawn;
131
133
  }