@torrent-tv/proxy 2.39.1 → 2.40.1
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 +11 -0
- package/package.json +4 -1
- package/services/torrent-worker/fastest-wires.js +80 -0
- package/services/torrent-worker/piece-reader.js +1114 -755
- package/test/read-bands.test.js +133 -0
- package/test/tail-duplication.test.js +123 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 2.40.1
|
|
2
|
+
|
|
3
|
+
- **Fix**: The patched `utp-native` now replaces every copy in the tree, not just the top one. Installed at this package's own level, it left `webtorrent/node_modules/utp-native` untouched — and Node resolves from the requiring module outward, so WebTorrent went on loading the published build with the defect in it. The crash of 2026-08-19 21:03 names that exact path in frame 2, and every earlier one did too: the substitution shipped in 2.36.2 was never once in the loading path. `overrides` in this package's manifest now redirects the whole tree, npm applies it because a global install makes this package the root, and the addon image additionally deletes any nested copy and FAILS THE BUILD if a surviving `utp_native.node` belongs to another package. A silent fallback to the broken one is what made a fix that changed nothing look like a fix that worked.
|
|
4
|
+
|
|
5
|
+
## 2.40.0
|
|
6
|
+
|
|
7
|
+
- **New**: What a reader wants is claimed in four bands of decreasing urgency instead of one, and which way it claims is decided per read so the two can be compared on real viewing. Until now there was one band at priority 1 with WebTorrent's own whole-file selection at 0 beneath it, so "what the viewer reaches in seconds" and "the rest of the film" were the same thing to the picker. The bands are: what the viewer reaches in seconds, anchored at the first piece not already held; the near lead; the far lead; and, only once the lead has covered everything to the end of the file, whatever was never downloaded BEHIND the position — which a backward seek needs and which must never compete with the picture being watched. Priorities are 4, 3, 2, 1 and none of them zero, because zero is where the library's own background fill sits, and they are distinct because the library deliberately shuffles selections of equal non-zero priority against each other.
|
|
8
|
+
- **New**: The widths of the lead bands are derived from what has been measured about this file on this swarm, not chosen. The near band covers the worst interruption this reader has actually met (`worst wait × the rate the consumer is taking bytes at`); the far band covers what the swarm can put ahead of the viewer between interruptions (`(download rate - consumption) × the median interval between them`). A swarm with no surplus produces no far band, which is right: there is nothing to get ahead with. The consumption rate is measured by the reader as it hands bytes over, so nothing has to be passed in or assumed about who is reading. Until two interruptions have been seen there are no figures and both bands fall back to the reader's own window, and the log says which of the two it is.
|
|
9
|
+
- **New**: Every wait line names the mode it happened under and the bands as they stood (`mode=bands p4:340-352 p3:353-370 p2:371-500`), and the periodic supply summary compares the two arms directly — `flat N waits median Xms worst Yms, bands M waits median Zms worst Wms` — appearing only once both have samples. `TORRENT_TV_READ_MODE=flat|bands` pins one arm for a deployment that wants no alternation.
|
|
10
|
+
- **New**: A wait is also recorded against whether the blocked piece was steered onto another peer at all, and the summary states both — `steered N waits median Xms, unsteered M waits median Yms`. The steering itself has been logged per wait since 2.29.0; what could not be read from it was what it bought, which is a difference between waits and not a property of one.
|
|
11
|
+
|
|
1
12
|
## 2.39.1
|
|
2
13
|
|
|
3
14
|
- **Fix**: The block duplication of 2.39.0 is removed, because measured against what a viewer actually feels it never paid. 2.39.0 was shipped on a measurement of the median wait for a piece; the quantity that matters is the seconds the picture stands still, and measured on that (2026-08-19, a reader paced at the film's own byte rate with an eight-second lead, arms alternated per position) it is neutral at best and costly at worst. On a well-seeded film every arm read 67 MB in 59 s and stopped for at most 2.6 s — nothing to improve. With the download capped just above the film's rate, which is what a home line IS whenever the swarm can fill it, duplication made the picture stop for **28.8 s against 12.8 s** at one position and left the other unchanged: under one shared budget a duplicate spends the very bytes it is trying to go around. The regime where it could pay — several slow peers, each with capacity of its own — could not be reproduced: the thin-swarm candidate turned out to have no live swarm at all (4 peers, 179.5 s of a 209 s run spent stopped). A lever with no measured gain and a measured cost does not stay on by default. Narrowing the read window to the blocked piece was tried in the same experiment and is not shipped for the same reason: it was never better and reached **44.8 s against 12.8 s**. What stays is the measurement that decided it (`research/tail-duplication-2026-08-19.md`) and the `tail …` line from 2.38.1, which is what will say whether a real thin swarm ever behaves differently.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@torrent-tv/proxy",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.1",
|
|
4
4
|
"description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"publishConfig": {
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@biomejs/biome": "^2.5.7"
|
|
45
|
+
},
|
|
46
|
+
"overrides": {
|
|
47
|
+
"utp-native": "npm:@torrent-tv/utp-native@2.5.3-ttv.1"
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -197,3 +197,83 @@ export function describePieceTail(torrent, pieceIndex) {
|
|
|
197
197
|
outstanding.sort((left, right) => left.bytesPerSecond - right.bytesPerSecond);
|
|
198
198
|
return { chunks, missing, outstanding };
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Ask a second wire for blocks the reader is still waiting on.
|
|
203
|
+
*
|
|
204
|
+
* WebTorrent reserves each block for exactly one wire, so once `Piece.reserve()`
|
|
205
|
+
* answers -1 there is no request left to place and the read ends when the
|
|
206
|
+
* holder of the last block delivers it — however fast the rest of the swarm is.
|
|
207
|
+
* The library's own remedy is `_hotswap`, and it does precisely the right
|
|
208
|
+
* thing: `piece.cancel(blockIndex)` frees the reservation while leaving the
|
|
209
|
+
* first wire's request in flight, so a second wire can be asked for the same
|
|
210
|
+
* block and whichever arrives first wins. What it will not do is apply that to
|
|
211
|
+
* these tails. It is gated on speed — the held-up wire must be under 48 KB/s
|
|
212
|
+
* (`3 * BLOCK_LENGTH`) and at least twice as slow as the asker — and measured
|
|
213
|
+
* on a real swarm 2026-08-19, the tails a reader waits on sit at 109, 317 and
|
|
214
|
+
* 870 KB/s. Peers the library rightly considers good, because for bulk
|
|
215
|
+
* downloading they are; the gate is about throughput across a torrent and knows
|
|
216
|
+
* nothing about a reader blocked on one piece now.
|
|
217
|
+
*
|
|
218
|
+
* Speed is not even what is wrong with them: two blocks — 32 KB — on a wire
|
|
219
|
+
* measured at 109 KB/s is 0.3 s of transfer, and that read waited 4.6 s. The
|
|
220
|
+
* blocks are not travelling slowly, they are queued behind that wire's other
|
|
221
|
+
* work, which no average speed can show.
|
|
222
|
+
*
|
|
223
|
+
* The cost is bounded by the same measurement: the tails were 2 to 14 blocks of
|
|
224
|
+
* 512, so 32 to 224 KB against the 8 MiB piece they hold up. At most one
|
|
225
|
+
* duplicate is asked per candidate wire per call, and the caller repeats every
|
|
226
|
+
* half second, so a longer tail is covered across attempts rather than in one
|
|
227
|
+
* burst.
|
|
228
|
+
*
|
|
229
|
+
* Safe against the completion path: a block that arrives twice is dropped by
|
|
230
|
+
* `Piece.set`, which writes only into an empty slot, and a piece already
|
|
231
|
+
* flushed answers `false` from `Piece.init` so the second arrival returns
|
|
232
|
+
* before `flush()` is reached.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} torrent
|
|
235
|
+
* @param {number} pieceIndex
|
|
236
|
+
* @returns {{ duplicated: number, missing: number, wires: number }}
|
|
237
|
+
* `duplicated` counts requests the library actually placed for a block that
|
|
238
|
+
* was already outstanding elsewhere.
|
|
239
|
+
*/
|
|
240
|
+
export function duplicateTailFor(torrent, pieceIndex) {
|
|
241
|
+
const piece = torrent?.pieces?.[pieceIndex];
|
|
242
|
+
const buffer = Array.isArray(piece?._buffer) ? piece._buffer : null;
|
|
243
|
+
if (!piece || !buffer || typeof piece.cancel !== "function") {
|
|
244
|
+
return { duplicated: 0, missing: 0, wires: 0 };
|
|
245
|
+
}
|
|
246
|
+
const chunks = Number.isFinite(piece._chunks) ? piece._chunks : buffer.length;
|
|
247
|
+
const missing = [];
|
|
248
|
+
for (let index = 0; index < chunks; index += 1) {
|
|
249
|
+
if (!buffer[index]) {
|
|
250
|
+
missing.push(index);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const candidates = wiresForPiece(torrent, pieceIndex);
|
|
254
|
+
if (missing.length === 0 || candidates.length === 0) {
|
|
255
|
+
return { duplicated: 0, missing: missing.length, wires: candidates.length };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let duplicated = 0;
|
|
259
|
+
// One block per wire: that is what the pipelines can usefully take at once,
|
|
260
|
+
// and it needs no number of its own.
|
|
261
|
+
for (let index = 0; index < Math.min(missing.length, candidates.length); index += 1) {
|
|
262
|
+
// Freeing the reservation is what makes the block askable again; the
|
|
263
|
+
// request already in flight is deliberately left alone, because the point
|
|
264
|
+
// is to have two of them.
|
|
265
|
+
piece.cancel(missing[index]);
|
|
266
|
+
// `false` is hotswap: the library's own swap must not run on top of this,
|
|
267
|
+
// or it would free a THIRD wire's block as well.
|
|
268
|
+
if (torrent._request(candidates[index], pieceIndex, false) === true) {
|
|
269
|
+
duplicated += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// The wire's pipeline is full. The block stays in the piece's cancellation
|
|
273
|
+
// stack and will be handed to whoever asks next, which is harmless — it is
|
|
274
|
+
// already in flight elsewhere — but there is no point asking the remaining
|
|
275
|
+
// wires, whose pipelines are no emptier.
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
return { duplicated, missing: missing.length, wires: candidates.length };
|
|
279
|
+
}
|