@torrent-tv/proxy 2.79.0 → 2.80.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 +1580 -1558
- package/bin/cli.js +20 -1
- package/package.json +1 -1
- package/server.js +16 -0
- package/services/demand/DemandRegister.js +20 -0
- package/services/download/SwarmSelection.js +4 -1
- package/services/encode/EncodePlan.js +159 -31
- package/services/encode/SegmentStore.js +29 -0
- package/services/encode/run-budget.js +8 -0
- package/services/hls-session-manager.js +25 -8
- package/services/orchestrators/EncodeOrchestrator.js +27 -5
- package/services/piece-store/piece-lru.js +91 -31
- package/services/piece-store/shared-piece-store.js +1531 -1512
- package/services/priority/PriorityMap.js +57 -101
- package/services/priority/PriorityOrchestrator.js +149 -0
- package/services/torrent-pool.js +104 -0
- package/services/torrent-worker/client.js +20 -0
- package/services/torrent-worker/piece-reader.js +57 -217
- package/services/torrent-worker/pool-adapter.js +17 -0
- package/services/torrent-worker/protocol.js +10 -0
- package/services/torrent-worker/worker.js +11 -0
- package/services/usrsctp-state.js +35 -6
- package/test/demand-register.test.js +11 -0
- package/test/encode-plan.test.js +65 -0
- package/test/piece-lru.test.js +61 -0
- package/test/piece-store-eviction.test.js +30 -0
- package/test/priority-map-download.test.js +156 -0
- package/test/priority-map.test.js +50 -77
- package/test/read-window.test.js +46 -29
- package/test/usrsctp-state.test.js +18 -2
- package/test/read-bands.test.js +0 -140
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What the download is told when the priority map changes.
|
|
3
|
+
*
|
|
4
|
+
* The map states seconds of film against a number. This is the one place those
|
|
5
|
+
* seconds become bytes, and the one place the map's own scale — as many bands
|
|
6
|
+
* as the film needs — is fitted onto the five levels the register has. Both
|
|
7
|
+
* were written on 2026-09-05 to replace the windows the reads themselves used
|
|
8
|
+
* to declare: fifteen reads on one file were fifteen windows on a piece store
|
|
9
|
+
* holding sixteen pieces, and half of all evictions then took a piece a reader
|
|
10
|
+
* had said it wanted.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { TorrentPool } from "../services/torrent-pool.js";
|
|
16
|
+
import { demandFor, forgetTorrent } from "../services/download/registry.js";
|
|
17
|
+
import { Urgency } from "../services/demand/index.js";
|
|
18
|
+
import { mapForViewer } from "../services/priority/PriorityMap.js";
|
|
19
|
+
|
|
20
|
+
/** A torrent that is nothing but one file of a known length. */
|
|
21
|
+
function torrentOf({ length = 1_000_000, offset = 0 } = {}) {
|
|
22
|
+
return {
|
|
23
|
+
infoHash: `hash-${Math.random().toString(36).slice(2)}`,
|
|
24
|
+
pieceLength: 1024,
|
|
25
|
+
files: [{ offset, length, name: "film.mkv" }],
|
|
26
|
+
_selections: { _items: [] },
|
|
27
|
+
_select() {},
|
|
28
|
+
_deselect() {},
|
|
29
|
+
critical() {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The pool's method under test, called without building a pool. */
|
|
34
|
+
const applyPriorityMap = TorrentPool.prototype.applyPriorityMap;
|
|
35
|
+
|
|
36
|
+
test("seconds of film become bytes of the file", () => {
|
|
37
|
+
const torrent = torrentOf({ length: 1000 });
|
|
38
|
+
try {
|
|
39
|
+
applyPriorityMap.call(null, torrent, 0, [
|
|
40
|
+
{ from: 0, to: 100, priority: 5 },
|
|
41
|
+
{ from: 100, to: 200, priority: 3 }
|
|
42
|
+
], 200);
|
|
43
|
+
|
|
44
|
+
const { register } = demandFor(torrent);
|
|
45
|
+
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
46
|
+
assert.equal(stated.length, 2);
|
|
47
|
+
const first = stated.find((one) => one.byteStart === 0);
|
|
48
|
+
assert.equal(first.byteEnd, 499, "half the film is half the file");
|
|
49
|
+
const second = stated.find((one) => one.byteStart === 500);
|
|
50
|
+
assert.equal(second.byteEnd, 999, "the last byte of the file, not one past it");
|
|
51
|
+
} finally {
|
|
52
|
+
forgetTorrent(torrent);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a zone is stated at the level it means, not at the level of its position", () => {
|
|
57
|
+
const torrent = torrentOf();
|
|
58
|
+
try {
|
|
59
|
+
// A viewer 300 s into a 3000 s film: one band behind them that nobody is
|
|
60
|
+
// approaching, then bands of decreasing urgency ahead.
|
|
61
|
+
const zones = mapForViewer({
|
|
62
|
+
atSeconds: 300,
|
|
63
|
+
durationSeconds: 3000,
|
|
64
|
+
allowanceSeconds: 10
|
|
65
|
+
});
|
|
66
|
+
assert.ok(zones.length > 5, "the map should be finer than the register's levels");
|
|
67
|
+
|
|
68
|
+
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
69
|
+
const { register } = demandFor(torrent);
|
|
70
|
+
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
71
|
+
|
|
72
|
+
const near = stated.filter((one) => one.urgency === Urgency.NEAR);
|
|
73
|
+
assert.equal(near.length, 1, "exactly one band is where the viewer is");
|
|
74
|
+
assert.equal(near[0].byteStart, Math.floor((300 / 3000) * 1_000_000));
|
|
75
|
+
|
|
76
|
+
const behind = stated.filter((one) => one.urgency === Urgency.BEHIND);
|
|
77
|
+
assert.equal(behind.length, 1, "what nobody is approaching is the one thing wanted last");
|
|
78
|
+
assert.equal(behind[0].byteStart, 0, "and it is the stretch behind the viewer");
|
|
79
|
+
|
|
80
|
+
assert.ok(
|
|
81
|
+
stated.some((one) => one.urgency === Urgency.AHEAD),
|
|
82
|
+
"the lead being built was stated at no level of its own"
|
|
83
|
+
);
|
|
84
|
+
assert.ok(
|
|
85
|
+
stated.every((one) => one.urgency !== Urgency.BLOCKED),
|
|
86
|
+
"the map claimed BLOCKED, which only a reader stopped on those bytes may say"
|
|
87
|
+
);
|
|
88
|
+
} finally {
|
|
89
|
+
forgetTorrent(torrent);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a viewer who has stopped the picture makes the whole film wanted last", () => {
|
|
94
|
+
const torrent = torrentOf();
|
|
95
|
+
try {
|
|
96
|
+
const zones = mapForViewer({
|
|
97
|
+
atSeconds: 300,
|
|
98
|
+
durationSeconds: 3000,
|
|
99
|
+
allowanceSeconds: 10,
|
|
100
|
+
playing: false
|
|
101
|
+
});
|
|
102
|
+
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
103
|
+
|
|
104
|
+
const { register } = demandFor(torrent);
|
|
105
|
+
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
106
|
+
assert.equal(stated.length, 1, "a viewer going nowhere makes one band of the whole film");
|
|
107
|
+
// Wanted last ABSOLUTELY, not relative to this film's own map. The two
|
|
108
|
+
// speculative levels are withheld across every torrent at once while
|
|
109
|
+
// anything urgent is missing anywhere, so this has to lose to a film
|
|
110
|
+
// somebody is actually watching — and judged against itself alone it would
|
|
111
|
+
// be the most urgent thing there is.
|
|
112
|
+
assert.equal(stated[0].urgency, Urgency.BEHIND);
|
|
113
|
+
} finally {
|
|
114
|
+
forgetTorrent(torrent);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a map with fewer bands than the last one leaves nothing stated behind", () => {
|
|
119
|
+
const torrent = torrentOf();
|
|
120
|
+
try {
|
|
121
|
+
applyPriorityMap.call(null, torrent, 0, [
|
|
122
|
+
{ from: 0, to: 100, priority: 5 },
|
|
123
|
+
{ from: 100, to: 200, priority: 4 },
|
|
124
|
+
{ from: 200, to: 300, priority: 3 }
|
|
125
|
+
], 300);
|
|
126
|
+
const { register } = demandFor(torrent);
|
|
127
|
+
assert.equal(
|
|
128
|
+
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
129
|
+
3
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 300, priority: 5 }], 300);
|
|
133
|
+
assert.equal(
|
|
134
|
+
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
135
|
+
1,
|
|
136
|
+
"bands the map no longer has went on being asked for"
|
|
137
|
+
);
|
|
138
|
+
} finally {
|
|
139
|
+
forgetTorrent(torrent);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a file of unknown length or duration is not guessed at", () => {
|
|
144
|
+
const torrent = torrentOf();
|
|
145
|
+
try {
|
|
146
|
+
applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 100, priority: 5 }], 0);
|
|
147
|
+
applyPriorityMap.call(null, torrent, 7, [{ from: 0, to: 100, priority: 5 }], 300);
|
|
148
|
+
const { register } = demandFor(torrent);
|
|
149
|
+
assert.equal(
|
|
150
|
+
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
151
|
+
0
|
|
152
|
+
);
|
|
153
|
+
} finally {
|
|
154
|
+
forgetTorrent(torrent);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
@@ -10,77 +10,6 @@ import test from "node:test";
|
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import { mapForViewer, mergeMaps, inWorkingOrder } from "../services/priority/PriorityMap.js";
|
|
12
12
|
|
|
13
|
-
test("a viewer's own position is the most urgent thing there is", () => {
|
|
14
|
-
const map = mapForViewer({
|
|
15
|
-
atSeconds: 100,
|
|
16
|
-
durationSeconds: 1000,
|
|
17
|
-
allowanceSeconds: 8,
|
|
18
|
-
encodeSpeedX: 2
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
assert.equal(map[0].from, 100);
|
|
22
|
-
assert.equal(map[0].to, 108, "above realtime, only the measured allowance");
|
|
23
|
-
for (const zone of map.slice(1)) {
|
|
24
|
-
assert.ok(zone.priority < map[0].priority);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("a machine that cannot keep up needs more encoders, not a longer first zone", () => {
|
|
29
|
-
// An encoder starting where the viewer stands stays ahead of them for
|
|
30
|
-
// `held x s / (1 - s)`, so the SLOWER the machine the shorter that is — and
|
|
31
|
-
// the film is held by more encoders rather than by one working further.
|
|
32
|
-
const slow = mapForViewer({
|
|
33
|
-
atSeconds: 0,
|
|
34
|
-
durationSeconds: 1000,
|
|
35
|
-
allowanceSeconds: 10,
|
|
36
|
-
encodeSpeedX: 0.25
|
|
37
|
-
});
|
|
38
|
-
const quicker = mapForViewer({
|
|
39
|
-
atSeconds: 0,
|
|
40
|
-
durationSeconds: 1000,
|
|
41
|
-
allowanceSeconds: 10,
|
|
42
|
-
encodeSpeedX: 0.5
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
assert.ok(
|
|
46
|
-
slow[0].to - slow[0].from < quicker[0].to - quicker[0].from,
|
|
47
|
-
"the slower machine holds the viewer for less"
|
|
48
|
-
);
|
|
49
|
-
assert.ok(slow.length > quicker.length, "so more encoders are needed to hold the film");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("the first zone is exactly what one encoder can hold", () => {
|
|
53
|
-
// `held x s / (1 - s)`: ten seconds held at half speed is ten seconds held,
|
|
54
|
-
// and the encoder is caught exactly there. A longer first zone would stall
|
|
55
|
-
// the viewer inside it; a shorter one would start the next encoder nearer,
|
|
56
|
-
// where its own bound is tighter.
|
|
57
|
-
const map = mapForViewer({
|
|
58
|
-
atSeconds: 0,
|
|
59
|
-
durationSeconds: 100,
|
|
60
|
-
allowanceSeconds: 10,
|
|
61
|
-
encodeSpeedX: 0.5
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
assert.equal(map[0].to, 10);
|
|
65
|
-
assert.equal(map[1].to, 30, "the next holds three times as long, because they arrive later");
|
|
66
|
-
assert.equal(map[2].to, 70);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("a viewer holding nothing is told the whole of it is urgent", () => {
|
|
70
|
-
// With nothing held, no partition holds them: the first encoder stays ahead
|
|
71
|
-
// for zero seconds. Slicing the film into equal slivers would pretend
|
|
72
|
-
// otherwise.
|
|
73
|
-
const map = mapForViewer({
|
|
74
|
-
atSeconds: 0,
|
|
75
|
-
durationSeconds: 1000,
|
|
76
|
-
allowanceSeconds: 0,
|
|
77
|
-
encodeSpeedX: 0.25
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
assert.equal(map.length, 1);
|
|
81
|
-
assert.equal(map[0].to, 1000);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
13
|
test("the rest of the track is still wanted, and wanted last", () => {
|
|
85
14
|
const map = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 4, encodeSpeedX: 2 });
|
|
86
15
|
|
|
@@ -93,12 +22,6 @@ test("the rest of the track is still wanted, and wanted last", () => {
|
|
|
93
22
|
}
|
|
94
23
|
});
|
|
95
24
|
|
|
96
|
-
test("nothing is measured yet: the middle zone is left out rather than invented", () => {
|
|
97
|
-
const map = mapForViewer({ atSeconds: 0, durationSeconds: 100, allowanceSeconds: 4, encodeSpeedX: 0 });
|
|
98
|
-
|
|
99
|
-
assert.equal(map.length, 2, "what must be ready, and the rest");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
25
|
test("two viewers merge to the highest priority per second, with no overlaps", () => {
|
|
103
26
|
const first = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
|
|
104
27
|
const second = mapForViewer({ atSeconds: 500, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
|
|
@@ -137,3 +60,53 @@ test("within one priority the earliest film goes first — that is where somebod
|
|
|
137
60
|
|
|
138
61
|
assert.deepEqual(order.map((zone) => zone.from), [0, 100, 900]);
|
|
139
62
|
});
|
|
63
|
+
|
|
64
|
+
test("the nearer a viewer is to a second, the higher its number", () => {
|
|
65
|
+
// The number is a reading of how far they still have to travel, and nothing
|
|
66
|
+
// else. That is what makes two viewers comparable at all.
|
|
67
|
+
const map = mapForViewer({ atSeconds: 300, durationSeconds: 3000, allowanceSeconds: 10 });
|
|
68
|
+
const ahead = map.filter((zone) => zone.from >= 300);
|
|
69
|
+
|
|
70
|
+
assert.equal(ahead[0].from, 300, "it starts where they are");
|
|
71
|
+
assert.equal(ahead[0].to, 310, "and the first band is the measured allowance");
|
|
72
|
+
for (let index = 1; index < ahead.length; index += 1) {
|
|
73
|
+
assert.ok(ahead[index].priority < ahead[index - 1].priority, "further off is less urgent");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("the bands widen, so any film is described by a handful of them", () => {
|
|
78
|
+
// Near the viewer the difference between now and ten seconds away decides
|
|
79
|
+
// what is made first; twenty minutes out it changes nothing.
|
|
80
|
+
const map = mapForViewer({ atSeconds: 0, durationSeconds: 3000, allowanceSeconds: 10 });
|
|
81
|
+
|
|
82
|
+
assert.ok(map.length <= 12, `a fifty-minute film in ${map.length} bands`);
|
|
83
|
+
assert.equal(map[0].to - map[0].from, 10);
|
|
84
|
+
assert.equal(map[1].to - map[1].from, 20);
|
|
85
|
+
assert.equal(map[2].to - map[2].from, 40);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("what is behind a viewer is wanted, and wanted last", () => {
|
|
89
|
+
const map = mapForViewer({ atSeconds: 300, durationSeconds: 3000, allowanceSeconds: 10 });
|
|
90
|
+
const behind = map.find((zone) => zone.from === 0);
|
|
91
|
+
|
|
92
|
+
assert.ok(behind, "it is still in the map");
|
|
93
|
+
assert.equal(behind.to, 300);
|
|
94
|
+
assert.ok(
|
|
95
|
+
map.filter((zone) => zone.from >= 300).every((zone) => zone.priority > behind.priority),
|
|
96
|
+
"and everything anybody is approaching outranks it"
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a viewer who has stopped the picture is going nowhere", () => {
|
|
101
|
+
// Nothing is nearer to them than anything else, so nothing in the film is
|
|
102
|
+
// wanted sooner than the rest — and the work goes to whoever is watching.
|
|
103
|
+
const map = mapForViewer({
|
|
104
|
+
atSeconds: 300,
|
|
105
|
+
durationSeconds: 3000,
|
|
106
|
+
allowanceSeconds: 10,
|
|
107
|
+
playing: false
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
assert.equal(map.length, 1);
|
|
111
|
+
assert.deepEqual({ from: map[0].from, to: map[0].to }, { from: 0, to: 3000 });
|
|
112
|
+
});
|
package/test/read-window.test.js
CHANGED
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
* seeks. Nothing after that could outrank it: measured on a 4.7 GB film, a seek
|
|
9
9
|
* to 89.1% waited 93 s while the swarm fetched 2.47 GB in file order.
|
|
10
10
|
*
|
|
11
|
-
* Now a read
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Now a read claims only the piece it is STOPPED on, and gives it back when it
|
|
12
|
+
* ends. What should be downloaded ahead of a viewer is the priority map's
|
|
13
|
+
* answer, stated once for the whole file by the side that knows where the
|
|
14
|
+
* viewers are; a read is consumption, not a forecast. Fifteen reads on one file
|
|
15
|
+
* used to declare fifteen windows on a piece store holding sixteen pieces, and
|
|
16
|
+
* half of all evictions then took a piece a reader had declared (field
|
|
17
|
+
* 2026-09-05).
|
|
18
|
+
*
|
|
19
|
+
* These tests pin what is left: a read that is not stopped claims nothing, a
|
|
20
|
+
* read that is stopped claims the pieces it is waiting for and nothing beyond
|
|
21
|
+
* them, and every claim is given back.
|
|
14
22
|
*/
|
|
15
23
|
|
|
16
24
|
import test from "node:test";
|
|
@@ -119,12 +127,13 @@ test("the window is bounded and clamped to the end of the read", () => {
|
|
|
119
127
|
);
|
|
120
128
|
});
|
|
121
129
|
|
|
122
|
-
test("
|
|
123
|
-
// 8000 pieces of 1 KB
|
|
124
|
-
// of the file is
|
|
130
|
+
test("a read that is not stopped claims nothing", async () => {
|
|
131
|
+
// 8000 pieces of 1 KB, every one of them present, and the read asks as ffmpeg
|
|
132
|
+
// does: to the last byte of the file. Nothing is missing, so this read is
|
|
133
|
+
// waiting for nothing, so it wants nothing of the swarm — what lies ahead of
|
|
134
|
+
// it belongs to the priority map.
|
|
125
135
|
const { torrent, store, directory } = await recordingTorrent({ pieceCount: 8000 });
|
|
126
136
|
try {
|
|
127
|
-
// Read only the first two pieces, but ask as ffmpeg does: to the last byte.
|
|
128
137
|
const iterator = readFragments({
|
|
129
138
|
torrent,
|
|
130
139
|
fileIndex: 0,
|
|
@@ -136,15 +145,11 @@ test("an open-ended read does not claim the whole file at once", async () => {
|
|
|
136
145
|
const first = await iterator.next();
|
|
137
146
|
first.value.release();
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
claimed,
|
|
144
|
-
WINDOW_PIECES,
|
|
145
|
-
`the reader claimed ${claimed} pieces of the file instead of its window`
|
|
148
|
+
assert.deepEqual(
|
|
149
|
+
torrent.held,
|
|
150
|
+
[],
|
|
151
|
+
"the read declared a window of its own, which is the forecast that has to come from the map"
|
|
146
152
|
);
|
|
147
|
-
assert.equal(selects[0].stream, true, "the claim must be a stream selection, so it can be counted");
|
|
148
153
|
|
|
149
154
|
await iterator.return();
|
|
150
155
|
} finally {
|
|
@@ -187,35 +192,47 @@ test("an abandoned read leaves nothing selected", async () => {
|
|
|
187
192
|
}
|
|
188
193
|
});
|
|
189
194
|
|
|
190
|
-
test("two readers add up, and one leaving takes only its own", async () => {
|
|
191
|
-
|
|
195
|
+
test("two stopped readers add up, and one leaving takes only its own", async () => {
|
|
196
|
+
// Nothing has arrived yet, so both readers are stopped on their first piece
|
|
197
|
+
// and each states it. Two claims, each withdrawn on its own.
|
|
198
|
+
let arrived = false;
|
|
199
|
+
const { torrent, store, directory } = await recordingTorrent({
|
|
200
|
+
pieceCount: 8000,
|
|
201
|
+
present: () => arrived
|
|
202
|
+
});
|
|
192
203
|
try {
|
|
193
204
|
const head = readFragments({
|
|
194
205
|
torrent, fileIndex: 0, start: 0, end: 8000 * PIECE - 1,
|
|
195
|
-
cancellation: { isCancelled: () => false }
|
|
206
|
+
cancellation: { isCancelled: () => false },
|
|
207
|
+
windowBytes: WINDOW_PIECES * PIECE
|
|
196
208
|
});
|
|
197
209
|
const tail = readFragments({
|
|
198
210
|
torrent, fileIndex: 0, start: 4000 * PIECE, end: 8000 * PIECE - 1,
|
|
199
|
-
cancellation: { isCancelled: () => false }
|
|
211
|
+
cancellation: { isCancelled: () => false },
|
|
212
|
+
windowBytes: WINDOW_PIECES * PIECE
|
|
200
213
|
});
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
const headPending = head.next();
|
|
215
|
+
const tailPending = tail.next();
|
|
216
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
203
217
|
|
|
204
|
-
// Not a count: each reader states several bands by level, and two readers
|
|
205
|
-
// wanting the same pieces are one instruction. What matters is that both
|
|
206
|
-
// are represented and that leaving removes only what leaving should.
|
|
207
218
|
const pieceOf = (range) => Number(range.split("-")[0]);
|
|
208
219
|
const held = [...torrent.held];
|
|
209
|
-
assert.ok(held.some((range) => pieceOf(range) < 4000), "the head reader
|
|
210
|
-
assert.ok(held.some((range) => pieceOf(range) >= 4000), "the tail reader
|
|
220
|
+
assert.ok(held.some((range) => pieceOf(range) < 4000), "the head reader claimed nothing");
|
|
221
|
+
assert.ok(held.some((range) => pieceOf(range) >= 4000), "the tail reader claimed nothing");
|
|
222
|
+
|
|
223
|
+
// Let both through to a yield, so ending them runs their `finally` at once.
|
|
224
|
+
arrived = true;
|
|
225
|
+
torrent.emit("verified", 0);
|
|
226
|
+
torrent.emit("verified", 4000);
|
|
227
|
+
(await headPending).value.release();
|
|
228
|
+
(await tailPending).value.release();
|
|
211
229
|
|
|
212
230
|
await tail.return();
|
|
213
231
|
const after = [...torrent.held];
|
|
214
232
|
assert.ok(
|
|
215
|
-
after.
|
|
216
|
-
"the
|
|
233
|
+
after.every((range) => pieceOf(range) < 4000),
|
|
234
|
+
"the tail reader left its claim behind"
|
|
217
235
|
);
|
|
218
|
-
assert.ok(after.length < held.length, "the tail reader took nothing away when it left");
|
|
219
236
|
|
|
220
237
|
await head.return();
|
|
221
238
|
assert.deepEqual(torrent.held, [], "the last reader left something behind");
|
|
@@ -62,7 +62,7 @@ async function waitFor(check) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
test("
|
|
65
|
+
test("the deadline is counted outside the process gdb stops", async () => {
|
|
66
66
|
const { spawnProcess, calls } = makeSpawn("state=8 rwnd=95890\n");
|
|
67
67
|
const lines = [];
|
|
68
68
|
const reader = createUsrsctpStateReader({
|
|
@@ -74,7 +74,23 @@ test("gdb is invoked attached to this process with the bundled script", async ()
|
|
|
74
74
|
assert.equal(started, true);
|
|
75
75
|
await waitFor(() => lines.length > 0);
|
|
76
76
|
assert.equal(calls.length, 1);
|
|
77
|
-
|
|
77
|
+
// `timeout` is a separate process, so it keeps counting while gdb holds every
|
|
78
|
+
// thread of this one. A `setTimeout` here cannot fire — measured in the field
|
|
79
|
+
// on 2026-09-05, where gdb held the proxy for four minutes and the guard set
|
|
80
|
+
// for fifteen seconds never ran.
|
|
81
|
+
assert.equal(calls[0].command, "timeout");
|
|
82
|
+
assert.deepEqual(calls[0].args, [
|
|
83
|
+
"-s",
|
|
84
|
+
"KILL",
|
|
85
|
+
"15",
|
|
86
|
+
"gdb",
|
|
87
|
+
"-q",
|
|
88
|
+
"-batch",
|
|
89
|
+
"-p",
|
|
90
|
+
"4242",
|
|
91
|
+
"-x",
|
|
92
|
+
SCTPSTATE_SCRIPT_PATH
|
|
93
|
+
]);
|
|
78
94
|
assert.match(lines[0], /state=8 rwnd=95890/);
|
|
79
95
|
assert.match(lines[0], /test wedge/);
|
|
80
96
|
});
|
package/test/read-bands.test.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Claiming what a reader wants in four bands instead of one.
|
|
3
|
-
*
|
|
4
|
-
* Until now a reader claimed one band at one urgency, and WebTorrent's own
|
|
5
|
-
* whole-file selection sat under it at priority 0 — so "what the viewer reaches
|
|
6
|
-
* in seconds" and "the rest of the film" were indistinguishable to the picker.
|
|
7
|
-
* The bands separate them, and their widths come from what has been measured
|
|
8
|
-
* about this file on this swarm rather than from numbers chosen here.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { bandWidthsFrom, bandsFrom, sameBands } from "../services/torrent-worker/piece-reader.js";
|
|
14
|
-
import { Urgency } from "../services/demand/Urgency.js";
|
|
15
|
-
|
|
16
|
-
const PIECE = 8 * 1024 * 1024;
|
|
17
|
-
|
|
18
|
-
test("the urgent band is the one being read, and the lead bands come behind it in order", () => {
|
|
19
|
-
const bands = bandsFrom({
|
|
20
|
-
urgent: { from: 100, to: 103 },
|
|
21
|
-
pieceIndex: 100,
|
|
22
|
-
firstPiece: 0,
|
|
23
|
-
lastPiece: 1000,
|
|
24
|
-
widths: { near: 5, far: 20 }
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
assert.deepEqual(
|
|
28
|
-
bands,
|
|
29
|
-
[
|
|
30
|
-
{ from: 100, to: 103, urgency: Urgency.NEAR },
|
|
31
|
-
{ from: 104, to: 108, urgency: Urgency.AHEAD },
|
|
32
|
-
{ from: 109, to: 128, urgency: Urgency.AHEAD }
|
|
33
|
-
],
|
|
34
|
-
"no gap, no overlap, and each band strictly less urgent than the one in front"
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("no band is speculative while the reader is still walking the file", () => {
|
|
39
|
-
const bands = bandsFrom({
|
|
40
|
-
urgent: { from: 0, to: 1 },
|
|
41
|
-
pieceIndex: 0,
|
|
42
|
-
firstPiece: 0,
|
|
43
|
-
lastPiece: 100,
|
|
44
|
-
widths: { near: 2, far: 2 }
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
assert.ok(
|
|
48
|
-
bands.every((band) => band.urgency <= Urgency.AHEAD),
|
|
49
|
-
"a band at 0 would be indistinguishable from the background fill of the whole torrent"
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("what was never downloaded behind the viewer is not asked for until the lead reaches the end", () => {
|
|
54
|
-
const stillFilling = bandsFrom({
|
|
55
|
-
urgent: { from: 50, to: 52 },
|
|
56
|
-
pieceIndex: 50,
|
|
57
|
-
firstPiece: 0,
|
|
58
|
-
lastPiece: 1000,
|
|
59
|
-
widths: { near: 4, far: 8 }
|
|
60
|
-
});
|
|
61
|
-
assert.equal(
|
|
62
|
-
stillFilling.length,
|
|
63
|
-
3,
|
|
64
|
-
"the film ahead is not covered yet, so the past must not compete with it"
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const covered = bandsFrom({
|
|
68
|
-
urgent: { from: 50, to: 52 },
|
|
69
|
-
pieceIndex: 50,
|
|
70
|
-
firstPiece: 0,
|
|
71
|
-
lastPiece: 60,
|
|
72
|
-
widths: { near: 4, far: 8 }
|
|
73
|
-
});
|
|
74
|
-
assert.deepEqual(
|
|
75
|
-
covered[covered.length - 1],
|
|
76
|
-
{ from: 0, to: 49, urgency: Urgency.BEHIND },
|
|
77
|
-
"once there is nothing left ahead, the gap behind the viewer is worth filling"
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("a swarm with no surplus produces no far band, because there is nothing to get ahead with", () => {
|
|
82
|
-
const widths = bandWidthsFrom({
|
|
83
|
-
worstWaitSec: 2,
|
|
84
|
-
medianIntervalSec: 10,
|
|
85
|
-
downloadBytesPerSec: 1_000_000,
|
|
86
|
-
consumeBytesPerSec: 1_000_000,
|
|
87
|
-
pieceLength: PIECE,
|
|
88
|
-
basePieces: 4
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
assert.equal(widths.far, 0, "download rate equals consumption, so no lead can be built");
|
|
92
|
-
assert.equal(widths.measured, true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("the near band covers the worst interruption this reader has actually met", () => {
|
|
96
|
-
// 4 s of a film eaten at 2 MB/s is 8 MB, which is one piece here.
|
|
97
|
-
const widths = bandWidthsFrom({
|
|
98
|
-
worstWaitSec: 4,
|
|
99
|
-
medianIntervalSec: 30,
|
|
100
|
-
downloadBytesPerSec: 3_000_000,
|
|
101
|
-
consumeBytesPerSec: 2_000_000,
|
|
102
|
-
pieceLength: PIECE,
|
|
103
|
-
basePieces: 4
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
assert.equal(widths.near, 1, "worst wait × consumption, in pieces");
|
|
107
|
-
// A surplus of 1 MB/s held for 30 s is 30 MB, which is four pieces here.
|
|
108
|
-
assert.equal(widths.far, 4, "surplus × the interval it usually runs for, in pieces");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("with nothing measured yet both lead bands fall back to the reader's own window", () => {
|
|
112
|
-
const widths = bandWidthsFrom({
|
|
113
|
-
worstWaitSec: null,
|
|
114
|
-
medianIntervalSec: null,
|
|
115
|
-
downloadBytesPerSec: 5_000_000,
|
|
116
|
-
consumeBytesPerSec: 0,
|
|
117
|
-
pieceLength: PIECE,
|
|
118
|
-
basePieces: 6
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
assert.deepEqual(widths, { near: 6, far: 6, measured: false });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("an unchanged claim is recognised, so it is not released and re-made on every read", () => {
|
|
125
|
-
const bands = [
|
|
126
|
-
{ from: 1, to: 2, urgency: Urgency.NEAR },
|
|
127
|
-
{ from: 3, to: 9, urgency: Urgency.AHEAD }
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
assert.equal(sameBands(bands, [...bands.map((band) => ({ ...band }))]), true);
|
|
131
|
-
assert.equal(sameBands(bands, [{ from: 1, to: 2, urgency: Urgency.NEAR }]), false);
|
|
132
|
-
assert.equal(
|
|
133
|
-
sameBands(bands, [
|
|
134
|
-
{ from: 1, to: 2, urgency: Urgency.NEAR },
|
|
135
|
-
{ from: 3, to: 9, urgency: Urgency.BEHIND }
|
|
136
|
-
]),
|
|
137
|
-
false,
|
|
138
|
-
"the same range at another urgency is a different claim"
|
|
139
|
-
);
|
|
140
|
-
});
|