@torrent-tv/proxy 2.56.0 → 2.57.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 +17 -0
- package/bin/cli.js +494 -478
- package/package.json +1 -1
- package/services/container-index/matroska-subtitles.js +83 -4
- package/services/data-channel-handler.js +1133 -949
- package/services/delivery-probe.js +374 -255
- package/services/packet-witness.js +784 -406
- package/services/subtitle-defaults.js +28 -2
- package/test/delivery-probe.test.js +158 -78
- package/test/packet-witness-ring.test.js +236 -0
- package/test/packet-witness.test.js +148 -120
- package/test/subtitle-track-numbering.test.js +111 -3
- package/test/wedge-certainty.test.js +131 -0
|
@@ -1,120 +1,148 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The packet witness's gating rules and command construction.
|
|
3
|
-
*
|
|
4
|
-
* Roadmap item 10: a send queue wedged longer than ~30 s must start a bounded
|
|
5
|
-
* tcpdump on its own, because the 2026-08-24 episode proved no counter above
|
|
6
|
-
* the wire can name the cause. Everything here is the part that decides WHEN
|
|
7
|
-
* and WITH WHAT ARGUMENTS — the spawning itself is thin glue around these.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
buildTcpdumpArgs,
|
|
15
|
-
createPacketWitness,
|
|
16
|
-
isWitnessCapture,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
assert.equal(normalizeRemoteAddress("
|
|
43
|
-
assert.equal(normalizeRemoteAddress(""), null);
|
|
44
|
-
assert.equal(normalizeRemoteAddress(
|
|
45
|
-
assert.equal(normalizeRemoteAddress(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"-
|
|
58
|
-
"
|
|
59
|
-
"-
|
|
60
|
-
"
|
|
61
|
-
"-
|
|
62
|
-
"
|
|
63
|
-
"-
|
|
64
|
-
|
|
65
|
-
"-
|
|
66
|
-
String(
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
assert.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
assert.
|
|
120
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The packet witness's gating rules and command construction.
|
|
3
|
+
*
|
|
4
|
+
* Roadmap item 10: a send queue wedged longer than ~30 s must start a bounded
|
|
5
|
+
* tcpdump on its own, because the 2026-08-24 episode proved no counter above
|
|
6
|
+
* the wire can name the cause. Everything here is the part that decides WHEN
|
|
7
|
+
* and WITH WHAT ARGUMENTS — the spawning itself is thin glue around these.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
buildTcpdumpArgs,
|
|
15
|
+
createPacketWitness,
|
|
16
|
+
isWitnessCapture,
|
|
17
|
+
isWitnessRingFile,
|
|
18
|
+
normalizeRemoteAddress,
|
|
19
|
+
shouldStartCapture,
|
|
20
|
+
WITNESS_RING_FILE_MB,
|
|
21
|
+
WITNESS_RING_FILES,
|
|
22
|
+
WITNESS_RTO_CEILING_SECONDS,
|
|
23
|
+
WITNESS_TAIL_SECONDS
|
|
24
|
+
} from "../services/packet-witness.js";
|
|
25
|
+
|
|
26
|
+
test("an IPv4 literal survives unchanged", () => {
|
|
27
|
+
assert.equal(normalizeRemoteAddress("192.168.178.57"), "192.168.178.57");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("an IPv6 literal survives unchanged", () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
normalizeRemoteAddress("2001:1c00:a603:2100:a129:a1a1:7f07:3f0b"),
|
|
33
|
+
"2001:1c00:a603:2100:a129:a1a1:7f07:3f0b"
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a zone suffix is stripped", () => {
|
|
38
|
+
assert.equal(normalizeRemoteAddress("fe80::2aca:8001:3ba6:f16f%18"), "fe80::2aca:8001:3ba6:f16f");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("hostnames, garbage and shell text are rejected", () => {
|
|
42
|
+
assert.equal(normalizeRemoteAddress("homeassistant.local"), null);
|
|
43
|
+
assert.equal(normalizeRemoteAddress("8.8.8.8; rm -rf /"), null);
|
|
44
|
+
assert.equal(normalizeRemoteAddress("999.1.1.1"), null);
|
|
45
|
+
assert.equal(normalizeRemoteAddress(""), null);
|
|
46
|
+
assert.equal(normalizeRemoteAddress(undefined), null);
|
|
47
|
+
assert.equal(normalizeRemoteAddress(42), null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the tcpdump command line is bounded and filtered to one peer", () => {
|
|
51
|
+
const args = buildTcpdumpArgs({
|
|
52
|
+
host: "2001:db8::1",
|
|
53
|
+
port: 9090,
|
|
54
|
+
filePrefix: "/data/packet-witness.e2b5ef39.1787600000.pcap"
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(args, [
|
|
57
|
+
"-n",
|
|
58
|
+
"-S",
|
|
59
|
+
"-s",
|
|
60
|
+
"128",
|
|
61
|
+
"-i",
|
|
62
|
+
"any",
|
|
63
|
+
"-w",
|
|
64
|
+
"/data/packet-witness.e2b5ef39.1787600000.pcap",
|
|
65
|
+
"-C",
|
|
66
|
+
String(WITNESS_RING_FILE_MB),
|
|
67
|
+
"-W",
|
|
68
|
+
String(WITNESS_RING_FILES),
|
|
69
|
+
"udp",
|
|
70
|
+
"and",
|
|
71
|
+
"port",
|
|
72
|
+
"9090",
|
|
73
|
+
"and",
|
|
74
|
+
"host",
|
|
75
|
+
"2001:db8::1"
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("rotation is by size, so the ring files cannot collapse onto one name", () => {
|
|
80
|
+
// `-G` with a name carrying no strftime field overwrote a single file, which
|
|
81
|
+
// is why both field captures held 28 s instead of the intended 120.
|
|
82
|
+
const args = buildTcpdumpArgs({ port: 9090, filePrefix: "/data/ring.pcap" });
|
|
83
|
+
assert.equal(args.includes("-G"), false);
|
|
84
|
+
assert.equal(args[args.indexOf("-C") + 1], String(WITNESS_RING_FILE_MB));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("with no peer named the ring records every peer on the port", () => {
|
|
88
|
+
const args = buildTcpdumpArgs({ port: 9090, filePrefix: "/data/ring.pcap" });
|
|
89
|
+
assert.equal(args.includes("host"), false);
|
|
90
|
+
assert.deepEqual(args.slice(-4), ["udp", "and", "port", "9090"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("the tail outlasts three retransmission timeouts", () => {
|
|
94
|
+
assert.equal(WITNESS_TAIL_SECONDS, WITNESS_RTO_CEILING_SECONDS * 3);
|
|
95
|
+
// A zero-window probe is due once per timeout, so a window shorter than the
|
|
96
|
+
// ceiling could not tell silence from a probe that had not come round yet.
|
|
97
|
+
assert.ok(WITNESS_TAIL_SECONDS > WITNESS_RTO_CEILING_SECONDS);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("ring files are told apart from the copies kept as evidence", () => {
|
|
101
|
+
assert.equal(isWitnessRingFile("packet-witness-ring.pcap"), true);
|
|
102
|
+
assert.equal(isWitnessRingFile("packet-witness-ring.pcap3"), true);
|
|
103
|
+
assert.equal(isWitnessRingFile("packet-witness.e2b5ef39.1787600000.before1.pcap"), false);
|
|
104
|
+
assert.equal(isWitnessCapture("packet-witness-ring.pcap3"), false);
|
|
105
|
+
assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.before1.pcap"), true);
|
|
106
|
+
assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.tail.pcap1"), true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("one capture runs at a time", () => {
|
|
110
|
+
assert.equal(shouldStartCapture({ running: true, lastStartedAt: Date.now() }), false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("the first capture of a process is always allowed", () => {
|
|
114
|
+
assert.equal(shouldStartCapture({ running: false, lastStartedAt: 0, now: 1000 }), true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a capture within the cooldown is refused, one after it is allowed", () => {
|
|
118
|
+
const state = { running: false, lastStartedAt: 10_000 };
|
|
119
|
+
assert.equal(shouldStartCapture({ ...state, now: 10_000 + 599_999 }), false);
|
|
120
|
+
assert.equal(shouldStartCapture({ ...state, now: 10_000 + 600_000 }), true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("capture files are recognised by name, rotations included", () => {
|
|
124
|
+
assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.pcap"), true);
|
|
125
|
+
assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.pcap20260825T120000"), true);
|
|
126
|
+
assert.equal(isWitnessCapture("core.WorkerThread.81.1787600000"), false);
|
|
127
|
+
assert.equal(isWitnessCapture("proxy.log"), false);
|
|
128
|
+
assert.equal(isWitnessCapture("../packet-witness.x.pcap"), false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a trigger without a usable remote endpoint is refused without touching anything", () => {
|
|
132
|
+
const lines = [];
|
|
133
|
+
const witness = createPacketWitness({
|
|
134
|
+
log: (message) => lines.push(message),
|
|
135
|
+
dir: "",
|
|
136
|
+
port: 9090
|
|
137
|
+
});
|
|
138
|
+
const started = witness.maybeCapture({
|
|
139
|
+
sessionId: "e2b5ef39-0000",
|
|
140
|
+
tag: "e2b5ef39",
|
|
141
|
+
label: "proxy",
|
|
142
|
+
remote: null,
|
|
143
|
+
queuedBytes: 160_000_000,
|
|
144
|
+
stuckForMs: 31_000
|
|
145
|
+
});
|
|
146
|
+
assert.equal(started, false);
|
|
147
|
+
assert.deepEqual(lines, []);
|
|
148
|
+
});
|
|
@@ -37,6 +37,10 @@ const ID_TRACK_NUMBER = 0xd7;
|
|
|
37
37
|
const ID_TRACK_TYPE = 0x83;
|
|
38
38
|
const ID_CODEC_ID = 0x86;
|
|
39
39
|
const ID_LANGUAGE = 0x22b59c;
|
|
40
|
+
const ID_LANGUAGE_BCP47 = 0x22b59d;
|
|
41
|
+
const ID_FLAG_ENABLED = 0xb9;
|
|
42
|
+
const ID_FLAG_FORCED = 0x55aa;
|
|
43
|
+
const ID_FLAG_HEARING_IMPAIRED = 0x55ab;
|
|
40
44
|
const ID_CUES = 0x1c53bb6b;
|
|
41
45
|
const ID_CUE_POINT = 0xbb;
|
|
42
46
|
const ID_CUE_TIME = 0xb3;
|
|
@@ -95,13 +99,26 @@ function uint32Element(id, value) {
|
|
|
95
99
|
return element(id, payload);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
function trackEntry({ number, type, codecId, language }) {
|
|
99
|
-
|
|
102
|
+
function trackEntry({ number, type, codecId, language, flags = {}, languageBcp47 = null }) {
|
|
103
|
+
const parts = [
|
|
100
104
|
uintElement(ID_TRACK_NUMBER, number),
|
|
101
105
|
uintElement(ID_TRACK_TYPE, type),
|
|
102
106
|
stringElement(ID_CODEC_ID, codecId),
|
|
103
107
|
stringElement(ID_LANGUAGE, language)
|
|
104
|
-
]
|
|
108
|
+
];
|
|
109
|
+
if (languageBcp47 !== null) {
|
|
110
|
+
parts.push(stringElement(ID_LANGUAGE_BCP47, languageBcp47));
|
|
111
|
+
}
|
|
112
|
+
for (const [id, value] of [
|
|
113
|
+
[ID_FLAG_ENABLED, flags.enabled],
|
|
114
|
+
[ID_FLAG_FORCED, flags.forced],
|
|
115
|
+
[ID_FLAG_HEARING_IMPAIRED, flags.hearingImpaired]
|
|
116
|
+
]) {
|
|
117
|
+
if (value !== undefined) {
|
|
118
|
+
parts.push(uintElement(id, value ? 1 : 0));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return element(ID_TRACK_ENTRY, Buffer.concat(parts));
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
/**
|
|
@@ -260,3 +277,94 @@ test("two walks of one file at the same time read each cluster once", async () =
|
|
|
260
277
|
);
|
|
261
278
|
forgetSubtitles(sourceKey);
|
|
262
279
|
});
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* A file whose subtitle tracks carry the flags RFC 9559 defines for them: one
|
|
283
|
+
* forced, one for viewers who cannot hear, one the file marks unusable, and one
|
|
284
|
+
* writing its language as RFC 5646 alongside the three-letter code.
|
|
285
|
+
*
|
|
286
|
+
* @returns {Buffer}
|
|
287
|
+
*/
|
|
288
|
+
function fileWithFlags() {
|
|
289
|
+
const info = element(ID_INFO, uintElement(ID_TIMESTAMP_SCALE, 1_000_000));
|
|
290
|
+
const tracks = element(ID_TRACKS, Buffer.concat([
|
|
291
|
+
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "und" }),
|
|
292
|
+
trackEntry({ number: 2, type: 17, codecId: "S_TEXT/UTF8", language: "rus", flags: { forced: true } }),
|
|
293
|
+
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: "eng", flags: { hearingImpaired: true } }),
|
|
294
|
+
trackEntry({ number: 4, type: 17, codecId: "S_TEXT/UTF8", language: "fre", flags: { enabled: false } }),
|
|
295
|
+
trackEntry({ number: 5, type: 17, codecId: "S_TEXT/ASS", language: "por", languageBcp47: "pt-BR" })
|
|
296
|
+
]));
|
|
297
|
+
const seekEntry = (targetId, position) => element(ID_SEEK, Buffer.concat([
|
|
298
|
+
element(ID_SEEK_ID, idBytes(targetId)),
|
|
299
|
+
uint32Element(ID_SEEK_POSITION, position)
|
|
300
|
+
]));
|
|
301
|
+
const seekHeadWith = (infoAt, tracksAt) => element(ID_SEEK_HEAD, Buffer.concat([
|
|
302
|
+
seekEntry(ID_INFO, infoAt),
|
|
303
|
+
seekEntry(ID_TRACKS, tracksAt)
|
|
304
|
+
]));
|
|
305
|
+
const headLength = seekHeadWith(0, 0).length;
|
|
306
|
+
const segmentPayload = Buffer.concat([
|
|
307
|
+
seekHeadWith(headLength, headLength + info.length),
|
|
308
|
+
info,
|
|
309
|
+
tracks
|
|
310
|
+
]);
|
|
311
|
+
const ebml = element(ID_EBML, Buffer.from([0x42, 0x86, 0x81, 0x01]));
|
|
312
|
+
return Buffer.concat([ebml, element(ID_SEGMENT, segmentPayload)]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
test("the flags the file states about a track are read, not guessed from its name", async () => {
|
|
316
|
+
const plan = await readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
317
|
+
|
|
318
|
+
const forced = plan.tracks.find((track) => track.trackNumber === 2);
|
|
319
|
+
assert.equal(forced.isForced, true, "FlagForced 0x55AA");
|
|
320
|
+
assert.equal(forced.isHearingImpaired, false);
|
|
321
|
+
|
|
322
|
+
const sdh = plan.tracks.find((track) => track.trackNumber === 3);
|
|
323
|
+
assert.equal(sdh.isHearingImpaired, true, "FlagHearingImpaired 0x55AB");
|
|
324
|
+
assert.equal(sdh.isForced, false);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("a track the file marks unusable is not offered, but is still counted", async () => {
|
|
328
|
+
// FlagEnabled (0xB9): "Set to 1 if the track is usable." Track 4 says 0, so
|
|
329
|
+
// it is not offered — but it KEEPS its place in the numbering, because ffmpeg
|
|
330
|
+
// keeps it: `matroskadec.c` parses MATROSKA_ID_TRACKFLAGENABLED as EBML_NONE,
|
|
331
|
+
// reading the element and storing nothing, so the stream is created and gets
|
|
332
|
+
// its own `0:s:N`. Dropping it here would shift every track after it.
|
|
333
|
+
const plan = await readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
334
|
+
|
|
335
|
+
assert.equal(plan.tracks.some((track) => track.trackNumber === 4), false, "not offered for extraction");
|
|
336
|
+
const counted = plan.declared.find((track) => track.trackNumber === 4);
|
|
337
|
+
assert.ok(counted, "still declared, so the numbering does not move");
|
|
338
|
+
assert.equal(counted.isEnabled, false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("an unusable track keeps its place, so the tracks after it keep theirs", async () => {
|
|
342
|
+
const plan = await readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
343
|
+
|
|
344
|
+
// s:0 forced, s:1 SDH, s:2 the unusable one, s:3 the Brazilian track.
|
|
345
|
+
assert.deepEqual(
|
|
346
|
+
plan.tracks.map((track) => [track.trackNumber, track.declaredIndex]),
|
|
347
|
+
[[2, 0], [3, 1], [5, 3]]
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("the list ffmpeg is lined up against still speaks ffmpeg's language codes", async () => {
|
|
352
|
+
// `declared` exists to be paired with the `-i` banner, which prints the
|
|
353
|
+
// three-letter code; reporting "pt-BR" there would break the pairing and cost
|
|
354
|
+
// the FlagDefault reading with it.
|
|
355
|
+
const plan = await readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
356
|
+
|
|
357
|
+
const declared = plan.declared.find((track) => track.trackNumber === 5);
|
|
358
|
+
assert.equal(declared.language, "por");
|
|
359
|
+
assert.equal(declared.languageBcp47, "pt-BR");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("where the file writes RFC 5646, that is the language", async () => {
|
|
363
|
+
// "If this element is used, then any Language elements used in the same
|
|
364
|
+
// TrackEntry MUST be ignored."
|
|
365
|
+
const plan = await readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
366
|
+
|
|
367
|
+
const track = plan.tracks.find((entry) => entry.trackNumber === 5);
|
|
368
|
+
assert.equal(track.language, "pt-BR", "not the three-letter por");
|
|
369
|
+
assert.equal(track.languageBcp47, "pt-BR");
|
|
370
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { wedgeIsCertain } from "../services/data-channel-handler.js";
|
|
5
|
+
import { PROBE_INTERVAL_MS } from "../services/delivery-probe.js";
|
|
6
|
+
|
|
7
|
+
const MEGABYTE = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
test("a queue draining at the rate this link was measured at is not a wedge", () => {
|
|
10
|
+
// 8 MB at 16 MB/s is half a second of draining. Half a second in is normal.
|
|
11
|
+
const verdict = wedgeIsCertain({
|
|
12
|
+
queuedBytes: 8 * MEGABYTE,
|
|
13
|
+
bytesPerSecond: 16 * MEGABYTE,
|
|
14
|
+
flatForMs: 200
|
|
15
|
+
});
|
|
16
|
+
assert.equal(verdict.certain, false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("the counter standing still past the queue's own drain time is a wedge", () => {
|
|
20
|
+
const verdict = wedgeIsCertain({
|
|
21
|
+
queuedBytes: 8 * MEGABYTE,
|
|
22
|
+
bytesPerSecond: 16 * MEGABYTE,
|
|
23
|
+
flatForMs: 4000
|
|
24
|
+
});
|
|
25
|
+
assert.equal(verdict.certain, true);
|
|
26
|
+
assert.equal(verdict.needMs, 500);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a big queue on a slow link is given the time it genuinely needs", () => {
|
|
30
|
+
// 67 MB at 1 MB/s is 67 seconds of honest draining — the field's own numbers
|
|
31
|
+
// for the wedged channel, at a rate a thin link could really be running at.
|
|
32
|
+
const patient = wedgeIsCertain({
|
|
33
|
+
queuedBytes: 67 * MEGABYTE,
|
|
34
|
+
bytesPerSecond: MEGABYTE,
|
|
35
|
+
flatForMs: 30_000
|
|
36
|
+
});
|
|
37
|
+
assert.equal(patient.certain, false);
|
|
38
|
+
const later = wedgeIsCertain({
|
|
39
|
+
queuedBytes: 67 * MEGABYTE,
|
|
40
|
+
bytesPerSecond: MEGABYTE,
|
|
41
|
+
flatForMs: 70_000
|
|
42
|
+
});
|
|
43
|
+
assert.equal(later.certain, true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("the same queue on the link the field actually had is called quickly", () => {
|
|
47
|
+
// 67 MB at 18 MB/s is under four seconds. The field episode stood still for
|
|
48
|
+
// 3217 s; the previous rule waited a flat 30 s before recording anything.
|
|
49
|
+
const verdict = wedgeIsCertain({
|
|
50
|
+
queuedBytes: 67 * MEGABYTE,
|
|
51
|
+
bytesPerSecond: 18 * MEGABYTE,
|
|
52
|
+
flatForMs: 5000
|
|
53
|
+
});
|
|
54
|
+
assert.equal(verdict.certain, true);
|
|
55
|
+
assert.ok(verdict.needMs < 30_000);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a tiny queue still waits for one round of probes", () => {
|
|
59
|
+
const verdict = wedgeIsCertain({
|
|
60
|
+
queuedBytes: 512,
|
|
61
|
+
bytesPerSecond: 16 * MEGABYTE,
|
|
62
|
+
flatForMs: PROBE_INTERVAL_MS - 1
|
|
63
|
+
});
|
|
64
|
+
assert.equal(verdict.certain, false);
|
|
65
|
+
assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("nothing queued is not a wedge however long the counter has been still", () => {
|
|
69
|
+
const verdict = wedgeIsCertain({
|
|
70
|
+
queuedBytes: 0,
|
|
71
|
+
bytesPerSecond: 16 * MEGABYTE,
|
|
72
|
+
flatForMs: 600_000
|
|
73
|
+
});
|
|
74
|
+
assert.equal(verdict.certain, false);
|
|
75
|
+
assert.equal(verdict.needMs, null);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("with no rate measured the answer is that nothing can be said", () => {
|
|
79
|
+
const verdict = wedgeIsCertain({
|
|
80
|
+
queuedBytes: 8 * MEGABYTE,
|
|
81
|
+
bytesPerSecond: 0,
|
|
82
|
+
flatForMs: 600_000
|
|
83
|
+
});
|
|
84
|
+
assert.equal(verdict.certain, false);
|
|
85
|
+
assert.equal(verdict.needMs, null);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a pause no longer than this link's own longest healthy pause is not a wedge", () => {
|
|
89
|
+
// A retransmission timeout stops the accepted-byte counter dead: a full send
|
|
90
|
+
// buffer accepts nothing. If this connection has already paused 3 s while
|
|
91
|
+
// healthy, a 3 s pause says nothing.
|
|
92
|
+
const verdict = wedgeIsCertain({
|
|
93
|
+
queuedBytes: 8 * MEGABYTE,
|
|
94
|
+
bytesPerSecond: 18 * MEGABYTE,
|
|
95
|
+
flatForMs: 2500,
|
|
96
|
+
longestHealthyFlatMs: 3000
|
|
97
|
+
});
|
|
98
|
+
assert.equal(verdict.certain, false);
|
|
99
|
+
assert.equal(verdict.needMs, 3000);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a pause longer than any this link has shown is a wedge", () => {
|
|
103
|
+
const verdict = wedgeIsCertain({
|
|
104
|
+
queuedBytes: 8 * MEGABYTE,
|
|
105
|
+
bytesPerSecond: 18 * MEGABYTE,
|
|
106
|
+
flatForMs: 3500,
|
|
107
|
+
longestHealthyFlatMs: 3000
|
|
108
|
+
});
|
|
109
|
+
assert.equal(verdict.certain, true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("the quiet-probe rate cannot be what the queue is divided by", () => {
|
|
113
|
+
// With the browser's buffer full, the only traffic is the probe: three
|
|
114
|
+
// channels, a few dozen bytes, twice a second. Dividing 8 MB by that gives
|
|
115
|
+
// six hours, and the wedge would never be called. The BEST rate seen is what
|
|
116
|
+
// the watcher keeps, so this case must not arise — pinned here as the
|
|
117
|
+
// arithmetic that made it matter.
|
|
118
|
+
const wrong = wedgeIsCertain({
|
|
119
|
+
queuedBytes: 8 * MEGABYTE,
|
|
120
|
+
bytesPerSecond: 360,
|
|
121
|
+
flatForMs: 60_000
|
|
122
|
+
});
|
|
123
|
+
assert.equal(wrong.certain, false);
|
|
124
|
+
assert.ok(wrong.needMs > 6 * 60 * 60 * 1000 - 1);
|
|
125
|
+
const right = wedgeIsCertain({
|
|
126
|
+
queuedBytes: 8 * MEGABYTE,
|
|
127
|
+
bytesPerSecond: 18 * MEGABYTE,
|
|
128
|
+
flatForMs: 60_000
|
|
129
|
+
});
|
|
130
|
+
assert.equal(right.certain, true);
|
|
131
|
+
});
|