@torrent-tv/proxy 2.107.51 → 2.107.53

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.
@@ -1,171 +0,0 @@
1
- /**
2
- * @file Ask a second peer for the last blocks of a piece somebody is waiting
3
- * for, when one slow peer holds them (torrent-tv/meta#178).
4
- *
5
- * Field r40 (Home Assistant, 2026-10-14, a cold 1080p MKV): the first run's
6
- * input waited 10.0 s, last for piece 8, whose slowest block was 4.5 s in
7
- * flight from one peer; pieces 14-16 waited 10.0-11.5 s on one peer each. A
8
- * block is reserved for one wire, and the piece completes when its slowest
9
- * holder delivers (`duplicateTailFor`). Nothing had asked a second peer since
10
- * the blocked-read path that did so was removed (`0e38ec3`, torrent-tv/meta#95).
11
- *
12
- * Which pieces: those of the urgent first class the swarm was last told
13
- * (`SwarmSelection.requiredPieces`) — what somebody waits for now. A map that
14
- * withdraws them, or an input that is ready, takes them out, and nothing more
15
- * is asked for them.
16
- *
17
- * When: a tail changes when a block arrives, a wire's queue moves on, or a
18
- * peer becomes able or unable to give the piece; so it is looked at on those —
19
- * a block arriving, a wire unchoking, a peer stating a piece, a wire joining
20
- * or closing, a piece verified, a new map. Several in one turn of the event
21
- * loop are one look. A block is asked again only once its request has been out
22
- * a wire's queue time (`duplicateTailFor`), so a block not yet late is looked
23
- * at again when it becomes late, and a late one no other wire could take is
24
- * looked at again a queue time later — once, in case nothing else happens in
25
- * between. WebTorrent sizes a wire's queue to one second of its speed
26
- * (`PIPELINE_MAX_DURATION`).
27
- */
28
-
29
- import { duplicateTailFor } from "./fastest-wires.js";
30
-
31
- /** WebTorrent's `PIPELINE_MAX_DURATION`: a wire is given one second of its speed to fetch. */
32
- const WIRE_QUEUE_SECONDS = 1;
33
-
34
- /** What a wire says, besides a block arriving, that can change who could be asked for a piece. */
35
- const WIRE_EVENTS = ["unchoke", "have", "bitfield"];
36
-
37
- export class TailRepeat {
38
- #torrent;
39
- #requiredPieces;
40
- #duplicate;
41
- #schedule;
42
- #wake;
43
- #clear;
44
- #scheduled = false;
45
- #timer = null;
46
- /** When the armed look runs, wall clock ms. */
47
- #timerAt = Number.POSITIVE_INFINITY;
48
- #disposed = false;
49
- /** @type {Map<object, { onBlock: (index: number) => void, onPeer: () => void }>} */
50
- #wires = new Map();
51
- #onWire;
52
- #onPoke;
53
- #log;
54
- #now;
55
- /**
56
- * Grow-only, for the log: block copies asked, looks taken, and blocks that
57
- * arrived after their piece was complete — what the copies cost.
58
- */
59
- totals = { duplicated: 0, looks: 0, lateBlocks: 0 };
60
-
61
- /**
62
- * @param {object} params
63
- * @param {object} params.torrent
64
- * @param {() => Array<{ from: number, to: number }>} params.requiredPieces
65
- * @param {typeof duplicateTailFor} [params.duplicate]
66
- * @param {(work: () => void) => void} [params.schedule] - Runs work once this turn of the event loop is over.
67
- * @param {(work: () => void, ms: number) => unknown} [params.wake]
68
- * @param {(handle: unknown) => void} [params.clear]
69
- * @param {(line: string) => void} [params.log]
70
- * @param {() => number} [params.now]
71
- */
72
- constructor({ torrent, requiredPieces, duplicate = duplicateTailFor, schedule = setImmediate, wake = setTimeout, clear = clearTimeout, log = null, now = Date.now }) {
73
- this.#log = log;
74
- this.#now = now;
75
- this.#torrent = torrent;
76
- this.#requiredPieces = requiredPieces;
77
- this.#duplicate = duplicate;
78
- this.#schedule = schedule;
79
- this.#wake = wake;
80
- this.#clear = clear;
81
- this.#onWire = (wire) => this.#watch(wire);
82
- this.#onPoke = () => this.poke();
83
- torrent.on?.("wire", this.#onWire);
84
- torrent.on?.("verified", this.#onPoke);
85
- for (const wire of Array.isArray(torrent.wires) ? torrent.wires : []) this.#watch(wire);
86
- }
87
-
88
- /** Look at the tails once this turn of the event loop is over. */
89
- poke() {
90
- if (this.#scheduled || this.#disposed) return;
91
- this.#scheduled = true;
92
- this.#schedule(() => this.#look());
93
- }
94
-
95
- /** Stop watching: the torrent is going. */
96
- dispose() {
97
- this.#disposed = true;
98
- this.#torrent.removeListener?.("wire", this.#onWire);
99
- this.#torrent.removeListener?.("verified", this.#onPoke);
100
- for (const wire of [...this.#wires.keys()]) this.#forget(wire);
101
- if (this.#timer !== null) this.#clear(this.#timer);
102
- this.#timer = null;
103
- }
104
-
105
- /** One look armed at a time, at the earliest moment asked for. */
106
- #lookAgainAt(at, now) {
107
- if (!Number.isFinite(at) || at >= this.#timerAt) return;
108
- if (this.#timer !== null) this.#clear(this.#timer);
109
- this.#timerAt = at;
110
- this.#timer = this.#wake(() => {
111
- this.#timer = null;
112
- this.#timerAt = Number.POSITIVE_INFINITY;
113
- this.poke();
114
- }, Math.max(0, at - now));
115
- this.#timer?.unref?.();
116
- }
117
-
118
- #watch(wire) {
119
- if (!wire || this.#wires.has(wire) || typeof wire.on !== "function") return;
120
- const listeners = {
121
- onBlock: (index) => {
122
- // A block of a piece already verified is a copy that came second; the
123
- // block that completes a piece arrives before the piece is verified.
124
- if (Number.isInteger(index) && this.#torrent.bitfield?.get?.(index)) this.totals.lateBlocks += 1;
125
- this.poke();
126
- },
127
- onPeer: () => this.poke()
128
- };
129
- this.#wires.set(wire, listeners);
130
- wire.on("piece", listeners.onBlock);
131
- for (const event of WIRE_EVENTS) wire.on(event, listeners.onPeer);
132
- wire.once?.("close", () => {
133
- this.#forget(wire);
134
- this.poke();
135
- });
136
- this.poke();
137
- }
138
-
139
- #forget(wire) {
140
- const listeners = this.#wires.get(wire);
141
- if (!listeners) return;
142
- wire.removeListener?.("piece", listeners.onBlock);
143
- for (const event of WIRE_EVENTS) wire.removeListener?.(event, listeners.onPeer);
144
- this.#wires.delete(wire);
145
- }
146
-
147
- #look() {
148
- this.#scheduled = false;
149
- if (this.#disposed) return;
150
- this.totals.looks += 1;
151
- const now = this.#now();
152
- let unplaced = 0, dueAt = Number.POSITIVE_INFINITY;
153
- const asked = [];
154
- for (const { from, to } of this.#requiredPieces()) {
155
- for (let index = from; index <= to; index += 1) {
156
- if (this.#torrent.bitfield?.get?.(index)) continue;
157
- const result = this.#duplicate(this.#torrent, index, { minAgeMs: WIRE_QUEUE_SECONDS * 1000, now });
158
- this.totals.duplicated += result.duplicated;
159
- unplaced += result.unplaced;
160
- if (Number.isFinite(result.dueAt)) dueAt = Math.min(dueAt, result.dueAt);
161
- if (result.duplicated > 0) asked.push(`${index}:${result.duplicated}/${result.missing}`);
162
- }
163
- }
164
- if (asked.length > 0) {
165
- this.#log?.(`tail: asked a second peer for the last blocks of piece:blocks/missing ${asked.join(" ")}; ` +
166
- `${this.totals.duplicated} block copies asked so far, ${this.totals.lateBlocks} block(s) arrived after their piece was complete`);
167
- }
168
- if (unplaced > 0) dueAt = Math.min(dueAt, now + WIRE_QUEUE_SECONDS * 1000);
169
- this.#lookAgainAt(dueAt, now);
170
- }
171
- }