@torrent-tv/proxy 2.98.17 → 2.98.18
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
|
@@ -1545,7 +1545,13 @@ export class TorrentPool {
|
|
|
1545
1545
|
wire.request = function (index, offset, length, callback) {
|
|
1546
1546
|
const queued = this.requests.length;
|
|
1547
1547
|
const answer = request.call(this, index, offset, length, callback);
|
|
1548
|
-
if (this.requests.length > queued)
|
|
1548
|
+
if (this.requests.length > queued) {
|
|
1549
|
+
const now = Date.now();
|
|
1550
|
+
timings.asked(index, offset, peer, now);
|
|
1551
|
+
// When this wire was asked for the block: how long it has held it is
|
|
1552
|
+
// what decides whether a second peer is asked (`duplicateTailFor`).
|
|
1553
|
+
this.requests.at(-1).askedAt = now;
|
|
1554
|
+
}
|
|
1549
1555
|
return answer;
|
|
1550
1556
|
};
|
|
1551
1557
|
wire.on("piece", (index, offset) => timings.arrived(index, offset, peer, Date.now()));
|
|
@@ -264,25 +264,42 @@ export function describePieceTail(torrent, pieceIndex) {
|
|
|
264
264
|
* @param {object} torrent
|
|
265
265
|
* @param {number} pieceIndex
|
|
266
266
|
* @param {object} [options]
|
|
267
|
+
* A BLOCK IS ASKED AGAIN ONLY ONCE IT IS LATE. A short tail is not by itself a
|
|
268
|
+
* delay: copied as soon as the tail was short, 12-16 blocks of nearly every
|
|
269
|
+
* piece were asked twice and 82 of 215 copies arrived after their piece was
|
|
270
|
+
* complete (r41, Home Assistant, torrent-tv/meta#178). So a block is asked of a
|
|
271
|
+
* second peer once its request to the first has been out `minAgeMs` — by
|
|
272
|
+
* default the second WebTorrent sizes a wire's queue to fill
|
|
273
|
+
* (`PIPELINE_MAX_DURATION`), a starting point to be checked in the field — and
|
|
274
|
+
* the earliest moment a held block becomes late is returned, so whoever calls
|
|
275
|
+
* this can look again then.
|
|
276
|
+
*
|
|
277
|
+
* @param {object} torrent
|
|
278
|
+
* @param {number} pieceIndex
|
|
279
|
+
* @param {object} [options]
|
|
267
280
|
* @param {number} [options.maxCopies] - How many wires a missing block may be
|
|
268
281
|
* in flight on at once: the first request and one copy beside it.
|
|
269
|
-
* @
|
|
282
|
+
* @param {number} [options.minAgeMs] - How long the first request must have been out.
|
|
283
|
+
* @param {number} [options.now] - Wall clock, ms.
|
|
284
|
+
* @returns {{ duplicated: number, missing: number, wires: number, unplaced: number, dueAt: number | null }}
|
|
270
285
|
* `duplicated` counts requests the library actually placed for a block that
|
|
271
|
-
* was already outstanding elsewhere; `unplaced` the blocks in flight on
|
|
272
|
-
* wire alone that no other wire could be asked for now
|
|
286
|
+
* was already outstanding elsewhere; `unplaced` the late blocks in flight on
|
|
287
|
+
* one wire alone that no other wire could be asked for now; `dueAt` when the
|
|
288
|
+
* first block held by one wire and not yet late becomes late, or null.
|
|
273
289
|
*/
|
|
274
|
-
export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
290
|
+
export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2, minAgeMs = 1000, now = Date.now() } = {}) {
|
|
275
291
|
const piece = torrent?.pieces?.[pieceIndex];
|
|
276
292
|
const buffer = Array.isArray(piece?._buffer) ? piece._buffer : null;
|
|
277
293
|
if (!piece || !buffer || typeof piece.cancel !== "function" || !canPlaceRequests(torrent)) {
|
|
278
|
-
return { duplicated: 0, missing: 0, wires: 0, unplaced: 0 };
|
|
294
|
+
return { duplicated: 0, missing: 0, wires: 0, unplaced: 0, dueAt: null };
|
|
279
295
|
}
|
|
280
296
|
const chunks = Number.isFinite(piece._chunks) ? piece._chunks : buffer.length;
|
|
281
297
|
// Most calls are for a piece still arriving in bulk; its own count says so
|
|
282
298
|
// without walking its blocks or every wire's queue.
|
|
283
299
|
if (Number.isFinite(piece._buffered) && chunks - piece._buffered > SHORT_TAIL_BLOCKS) {
|
|
284
|
-
return { duplicated: 0, missing: chunks - piece._buffered, wires: 0, unplaced: 0 };
|
|
300
|
+
return { duplicated: 0, missing: chunks - piece._buffered, wires: 0, unplaced: 0, dueAt: null };
|
|
285
301
|
}
|
|
302
|
+
|
|
286
303
|
const missing = [];
|
|
287
304
|
for (let index = 0; index < chunks; index += 1) {
|
|
288
305
|
if (!buffer[index]) {
|
|
@@ -291,7 +308,7 @@ export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
|
291
308
|
}
|
|
292
309
|
const candidates = wiresForPiece(torrent, pieceIndex);
|
|
293
310
|
if (missing.length === 0 || candidates.length === 0) {
|
|
294
|
-
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0 };
|
|
311
|
+
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0, dueAt: null };
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
// A tail this short is what a blocked read is actually waiting on, and a
|
|
@@ -300,26 +317,34 @@ export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
|
300
317
|
// bytes that are already coming — measured 2026-08-19, the tails a reader
|
|
301
318
|
// waits on are 2 to 14 blocks of 512.
|
|
302
319
|
if (missing.length > SHORT_TAIL_BLOCKS) {
|
|
303
|
-
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0 };
|
|
320
|
+
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0, dueAt: null };
|
|
304
321
|
}
|
|
305
322
|
|
|
306
|
-
// Which wires each missing block is in flight on,
|
|
307
|
-
// queues: the library keeps a block's reservation for one
|
|
308
|
-
// copy asked here is visible nowhere else.
|
|
309
|
-
|
|
323
|
+
// Which wires each missing block is in flight on, and since when, read from
|
|
324
|
+
// the wires' own queues: the library keeps a block's reservation for one
|
|
325
|
+
// wire only, and a copy asked here is visible nowhere else. A request with no
|
|
326
|
+
// time of its own was asked before anybody watched, and is old.
|
|
327
|
+
const holders = new Map(missing.map(block => [block, { wires: new Set(), since: Number.POSITIVE_INFINITY }]));
|
|
310
328
|
for (const wire of Array.isArray(torrent.wires) ? torrent.wires : []) {
|
|
311
329
|
for (const request of Array.isArray(wire?.requests) ? wire.requests : []) {
|
|
312
330
|
if (request?.piece !== pieceIndex) continue;
|
|
313
|
-
holders.get(Math.floor(request.offset / BLOCK_BYTES))
|
|
331
|
+
const holding = holders.get(Math.floor(request.offset / BLOCK_BYTES));
|
|
332
|
+
if (!holding) continue;
|
|
333
|
+
holding.wires.add(wire);
|
|
334
|
+
holding.since = Math.min(holding.since, Number.isFinite(request.askedAt) ? request.askedAt : Number.NEGATIVE_INFINITY);
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
337
|
|
|
317
|
-
let duplicated = 0, unplaced = 0;
|
|
338
|
+
let duplicated = 0, unplaced = 0, dueAt = null;
|
|
318
339
|
for (const block of missing) {
|
|
319
|
-
const held = holders.get(block);
|
|
340
|
+
const { wires: held, since } = holders.get(block);
|
|
320
341
|
// Not in flight anywhere: the library's own picker asks for it; a block
|
|
321
342
|
// nobody has been asked for is not a tail held by a slow peer.
|
|
322
343
|
if (held.size === 0 || held.size >= maxCopies) continue;
|
|
344
|
+
if (now - since < minAgeMs) {
|
|
345
|
+
dueAt = Math.min(dueAt ?? Number.POSITIVE_INFINITY, since + minAgeMs);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
323
348
|
let placed = false;
|
|
324
349
|
for (const wire of candidates) {
|
|
325
350
|
if (held.has(wire)) continue;
|
|
@@ -343,7 +368,7 @@ export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
|
343
368
|
}
|
|
344
369
|
if (!placed) unplaced += 1;
|
|
345
370
|
}
|
|
346
|
-
return { duplicated, missing: missing.length, wires: candidates.length, unplaced };
|
|
371
|
+
return { duplicated, missing: missing.length, wires: candidates.length, unplaced, dueAt };
|
|
347
372
|
}
|
|
348
373
|
|
|
349
374
|
/**
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
* peer becomes able or unable to give the piece; so it is looked at on those —
|
|
19
19
|
* a block arriving, a wire unchoking, a peer stating a piece, a wire joining
|
|
20
20
|
* or closing, a piece verified, a new map. Several in one turn of the event
|
|
21
|
-
* loop are one look. A
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* case nothing else happens in
|
|
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`).
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
29
|
import { duplicateTailFor } from "./fastest-wires.js";
|
|
@@ -41,12 +43,15 @@ export class TailRepeat {
|
|
|
41
43
|
#clear;
|
|
42
44
|
#scheduled = false;
|
|
43
45
|
#timer = null;
|
|
46
|
+
/** When the armed look runs, wall clock ms. */
|
|
47
|
+
#timerAt = Number.POSITIVE_INFINITY;
|
|
44
48
|
#disposed = false;
|
|
45
49
|
/** @type {Map<object, { onBlock: (index: number) => void, onPeer: () => void }>} */
|
|
46
50
|
#wires = new Map();
|
|
47
51
|
#onWire;
|
|
48
52
|
#onPoke;
|
|
49
53
|
#log;
|
|
54
|
+
#now;
|
|
50
55
|
/**
|
|
51
56
|
* Grow-only, for the log: block copies asked, looks taken, and blocks that
|
|
52
57
|
* arrived after their piece was complete — what the copies cost.
|
|
@@ -62,9 +67,11 @@ export class TailRepeat {
|
|
|
62
67
|
* @param {(work: () => void, ms: number) => unknown} [params.wake]
|
|
63
68
|
* @param {(handle: unknown) => void} [params.clear]
|
|
64
69
|
* @param {(line: string) => void} [params.log]
|
|
70
|
+
* @param {() => number} [params.now]
|
|
65
71
|
*/
|
|
66
|
-
constructor({ torrent, requiredPieces, duplicate = duplicateTailFor, schedule = setImmediate, wake = setTimeout, clear = clearTimeout, log = null }) {
|
|
72
|
+
constructor({ torrent, requiredPieces, duplicate = duplicateTailFor, schedule = setImmediate, wake = setTimeout, clear = clearTimeout, log = null, now = Date.now }) {
|
|
67
73
|
this.#log = log;
|
|
74
|
+
this.#now = now;
|
|
68
75
|
this.#torrent = torrent;
|
|
69
76
|
this.#requiredPieces = requiredPieces;
|
|
70
77
|
this.#duplicate = duplicate;
|
|
@@ -95,6 +102,19 @@ export class TailRepeat {
|
|
|
95
102
|
this.#timer = null;
|
|
96
103
|
}
|
|
97
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
|
+
|
|
98
118
|
#watch(wire) {
|
|
99
119
|
if (!wire || this.#wires.has(wire) || typeof wire.on !== "function") return;
|
|
100
120
|
const listeners = {
|
|
@@ -128,14 +148,16 @@ export class TailRepeat {
|
|
|
128
148
|
this.#scheduled = false;
|
|
129
149
|
if (this.#disposed) return;
|
|
130
150
|
this.totals.looks += 1;
|
|
131
|
-
|
|
151
|
+
const now = this.#now();
|
|
152
|
+
let unplaced = 0, dueAt = Number.POSITIVE_INFINITY;
|
|
132
153
|
const asked = [];
|
|
133
154
|
for (const { from, to } of this.#requiredPieces()) {
|
|
134
155
|
for (let index = from; index <= to; index += 1) {
|
|
135
156
|
if (this.#torrent.bitfield?.get?.(index)) continue;
|
|
136
|
-
const result = this.#duplicate(this.#torrent, index);
|
|
157
|
+
const result = this.#duplicate(this.#torrent, index, { minAgeMs: WIRE_QUEUE_SECONDS * 1000, now });
|
|
137
158
|
this.totals.duplicated += result.duplicated;
|
|
138
159
|
unplaced += result.unplaced;
|
|
160
|
+
if (Number.isFinite(result.dueAt)) dueAt = Math.min(dueAt, result.dueAt);
|
|
139
161
|
if (result.duplicated > 0) asked.push(`${index}:${result.duplicated}/${result.missing}`);
|
|
140
162
|
}
|
|
141
163
|
}
|
|
@@ -143,12 +165,7 @@ export class TailRepeat {
|
|
|
143
165
|
this.#log?.(`tail: asked a second peer for the last blocks of piece:blocks/missing ${asked.join(" ")}; ` +
|
|
144
166
|
`${this.totals.duplicated} block copies asked so far, ${this.totals.lateBlocks} block(s) arrived after their piece was complete`);
|
|
145
167
|
}
|
|
146
|
-
if (unplaced > 0
|
|
147
|
-
|
|
148
|
-
this.#timer = null;
|
|
149
|
-
this.poke();
|
|
150
|
-
}, WIRE_QUEUE_SECONDS * 1000);
|
|
151
|
-
this.#timer?.unref?.();
|
|
152
|
-
}
|
|
168
|
+
if (unplaced > 0) dueAt = Math.min(dueAt, now + WIRE_QUEUE_SECONDS * 1000);
|
|
169
|
+
this.#lookAgainAt(dueAt, now);
|
|
153
170
|
}
|
|
154
171
|
}
|