@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
|
@@ -665,14 +665,17 @@ export class SharedPieceStore {
|
|
|
665
665
|
* range to declare.
|
|
666
666
|
*/
|
|
667
667
|
get wantedBytes() {
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
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 (
|
|
676
|
+
if (required.readers > 0) {
|
|
674
677
|
this.#widestSeenPieces = Math.max(this.#widestSeenPieces, required.widestPieces);
|
|
675
|
-
const pieces = Math.max(MIN_RESIDENT_PIECES,
|
|
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.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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 =
|
|
74
|
-
const
|
|
75
|
-
const
|
|
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
|
|
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 (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
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,
|
|
130
|
+
const result = entry.selection.reconcile({ speculativeAllowed, firstClass });
|
|
129
131
|
stated += result.stated;
|
|
130
132
|
withdrawn += result.withdrawn;
|
|
131
133
|
}
|