@torrent-tv/proxy 2.89.17 → 2.89.19
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/README.md +14 -3
- package/package.json +10 -2
- package/server.js +0 -11
- package/utils/client-log-file.js +147 -42
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2377
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -151
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/client-logs-route.test.js +0 -34
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A proxy that needs nothing from a swarm is not in that swarm.
|
|
3
|
-
*
|
|
4
|
-
* WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
|
|
5
|
-
* `_addIncomingPeer` checks that the torrent is neither destroyed nor paused
|
|
6
|
-
* and registers the peer. A proxy with its port mapped is reachable, so on a
|
|
7
|
-
* popular torrent connections arrive and are never turned away — field
|
|
8
|
-
* 2026-09-11: 249 connected at the start of one viewing, 596 at the end, 15 862
|
|
9
|
-
* more queued, on a file complete for three quarters of an hour.
|
|
10
|
-
*
|
|
11
|
-
* The rule is not a limit on connections. While anybody is reading, every one is
|
|
12
|
-
* worth keeping: the one that has delivered nothing yet may deliver next.
|
|
13
|
-
*
|
|
14
|
-
* AND IT IS ASKED OF WHAT HAS BEEN STATED, never of a count of readers. The
|
|
15
|
-
* first version asked every five seconds whether anybody was READING, and a
|
|
16
|
-
* torrent added three seconds earlier answered no — its edges still being read,
|
|
17
|
-
* its plan still being built. It left the swarm with 741 connections let go,
|
|
18
|
-
* and nothing rejoined it: rejoining waited for a reader, and the reader was
|
|
19
|
-
* waiting for the header the swarm had been fetching. Playback did not start at
|
|
20
|
-
* all.
|
|
21
|
-
*
|
|
22
|
-
* The question now is whether anything is WANTED of it, which the priority map,
|
|
23
|
-
* the ends of an open file and a stopped read all answer — and which is true
|
|
24
|
-
* from the moment a torrent is opened.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import test from "node:test";
|
|
28
|
-
import assert from "node:assert/strict";
|
|
29
|
-
import {
|
|
30
|
-
isWanted,
|
|
31
|
-
leaveSwarm,
|
|
32
|
-
rejoinSwarm,
|
|
33
|
-
stateFileEdges,
|
|
34
|
-
swarmDecisionFor
|
|
35
|
-
} from "../services/torrent/torrent-pool.js";
|
|
36
|
-
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
37
|
-
import { Urgency } from "../services/torrent/demand/index.js";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* A torrent that records what was done to it. Only the surface the rule
|
|
41
|
-
* touches: `paused` is the library's own flag, and the peers are what the pause
|
|
42
|
-
* does not close by itself.
|
|
43
|
-
*
|
|
44
|
-
* @param {number} peerCount
|
|
45
|
-
* @returns {object}
|
|
46
|
-
*/
|
|
47
|
-
function torrentWith(peerCount) {
|
|
48
|
-
const peers = new Map();
|
|
49
|
-
for (let index = 0; index < peerCount; index += 1) {
|
|
50
|
-
peers.set(String(index), {
|
|
51
|
-
wire: { downloaded: 0 },
|
|
52
|
-
destroy() {
|
|
53
|
-
peers.delete(String(index));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
|
|
59
|
-
files: [{ length: 1024 }, { length: 2048 }],
|
|
60
|
-
paused: false,
|
|
61
|
-
wires: [...peers.values()].map((peer) => peer.wire),
|
|
62
|
-
_peers: peers,
|
|
63
|
-
pause() {
|
|
64
|
-
this.paused = true;
|
|
65
|
-
},
|
|
66
|
-
resume() {
|
|
67
|
-
this.paused = false;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
test("letting a swarm go pauses the torrent and closes what the pause does not", () => {
|
|
73
|
-
const torrent = torrentWith(6);
|
|
74
|
-
|
|
75
|
-
const closed = leaveSwarm(torrent);
|
|
76
|
-
|
|
77
|
-
assert.equal(torrent.paused, true, "the library's own word for this was not used");
|
|
78
|
-
assert.equal(closed, 6);
|
|
79
|
-
assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("a swarm already let go is not let go twice", () => {
|
|
83
|
-
const torrent = torrentWith(4);
|
|
84
|
-
leaveSwarm(torrent);
|
|
85
|
-
assert.equal(leaveSwarm(torrent), 0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("the reader that comes back takes the swarm with it", () => {
|
|
89
|
-
const torrent = torrentWith(3);
|
|
90
|
-
leaveSwarm(torrent);
|
|
91
|
-
|
|
92
|
-
assert.equal(rejoinSwarm(torrent), true);
|
|
93
|
-
assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
|
|
94
|
-
assert.equal(rejoinSwarm(torrent), false, "a torrent already in its swarm was resumed again");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("a torrent whose metadata has not arrived is wanted, whatever is stated", () => {
|
|
98
|
-
// THE FIELD FAILURE OF 2026-09-11, as a question rather than as a mechanism.
|
|
99
|
-
// A torrent being added has no list of files, so nothing can name a byte of
|
|
100
|
-
// it — and it is being fetched precisely because somebody asked for it.
|
|
101
|
-
const torrent = torrentWith(103);
|
|
102
|
-
torrent.files = [];
|
|
103
|
-
assert.equal(isWanted(torrent), true);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a torrent with files and nothing stated for them is wanted by nobody", () => {
|
|
107
|
-
const torrent = torrentWith(4);
|
|
108
|
-
try {
|
|
109
|
-
assert.equal(isWanted(torrent), false);
|
|
110
|
-
} finally {
|
|
111
|
-
forgetTorrent(torrent);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("anything stated makes it wanted, and the last withdrawal ends that", () => {
|
|
116
|
-
const torrent = torrentWith(4);
|
|
117
|
-
try {
|
|
118
|
-
// The ends of a file being opened, which is the first thing said about a
|
|
119
|
-
// torrent anybody has picked and the reason it stays in its swarm through
|
|
120
|
-
// the seconds when nothing else can say anything about it.
|
|
121
|
-
stateFileEdges(torrent, 0, Urgency.TAIL);
|
|
122
|
-
assert.equal(isWanted(torrent), true);
|
|
123
|
-
|
|
124
|
-
const { register } = demandFor(torrent);
|
|
125
|
-
register.withdraw("file-edges:0:head");
|
|
126
|
-
assert.equal(isWanted(torrent), true, "one end of it is still wanted");
|
|
127
|
-
register.withdraw("file-edges:0:tail");
|
|
128
|
-
assert.equal(isWanted(torrent), false);
|
|
129
|
-
} finally {
|
|
130
|
-
forgetTorrent(torrent);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("a torrent that has been destroyed is wanted by nobody", () => {
|
|
135
|
-
const torrent = torrentWith(1);
|
|
136
|
-
torrent.destroyed = true;
|
|
137
|
-
assert.equal(isWanted(torrent), false);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("a torrent something is wanted of keeps its swarm and is off the clock", () => {
|
|
141
|
-
assert.deepEqual(
|
|
142
|
-
swarmDecisionFor({ wanted: true, everWanted: true }),
|
|
143
|
-
{ swarm: "take", onTheClock: false }
|
|
144
|
-
);
|
|
145
|
-
assert.deepEqual(
|
|
146
|
-
swarmDecisionFor({ wanted: true, everWanted: false }),
|
|
147
|
-
{ swarm: "take", onTheClock: false }
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("a swarm is let go on a DEPARTURE, never on a beginning", () => {
|
|
152
|
-
// The whole of the 2.83.1 failure in one line: a torrent that has never been
|
|
153
|
-
// wanted is one being opened — its metadata has just landed, the file list is
|
|
154
|
-
// on its way to the person choosing, and nothing has had a chance to state
|
|
155
|
-
// anything about it yet. Taking its swarm away there destroyed 741
|
|
156
|
-
// connections it needed a minute later.
|
|
157
|
-
assert.deepEqual(
|
|
158
|
-
swarmDecisionFor({ wanted: false, everWanted: false }),
|
|
159
|
-
{ swarm: "leave alone", onTheClock: true }
|
|
160
|
-
);
|
|
161
|
-
assert.deepEqual(
|
|
162
|
-
swarmDecisionFor({ wanted: false, everWanted: true }),
|
|
163
|
-
{ swarm: "let go", onTheClock: true }
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("whatever happens to the swarm, an unwanted torrent is on the idle clock", () => {
|
|
168
|
-
// Including the one nobody has ever wanted: a file list fetched and never
|
|
169
|
-
// played used to be held for the life of the process, because the only thing
|
|
170
|
-
// that started the clock was a reader letting go.
|
|
171
|
-
for (const everWanted of [true, false]) {
|
|
172
|
-
assert.equal(swarmDecisionFor({ wanted: false, everWanted }).onTheClock, true);
|
|
173
|
-
}
|
|
174
|
-
});
|
package/test/swarm-reach.test.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The line describing a swarm answers the question it is asked.
|
|
3
|
-
*
|
|
4
|
-
* It could not. `routes/api/sources/stats/get.js` printed `peers=N` beside
|
|
5
|
-
* `wires=?`, which reads as two quantities of which one is unknown — while
|
|
6
|
-
* WebTorrent's `numPeers` IS `wires.length` (`lib/torrent.js:254`, the same in
|
|
7
|
-
* 2.8.5 and 3.0.21), so the first was the connection count and the second was a
|
|
8
|
-
* field that has never printed anything, because the torrent lives on a worker
|
|
9
|
-
* thread where that property does not exist.
|
|
10
|
-
*
|
|
11
|
-
* What was missing is the other half: how many peers the client HOLDS but is
|
|
12
|
-
* not connected to, and what the tracker said the swarm has. Measured
|
|
13
|
-
* 2026-08-21 on `JUFD665.mp4`, the tracker answered `seeders=5` at 13:40:30 and
|
|
14
|
-
* the first wire arrived at 13:44:47 — 4 min 17 s in which the stats line
|
|
15
|
-
* repeated unchanged every two seconds while the answer ("offered, not
|
|
16
|
-
* connected") was already in the process.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
|
|
22
|
-
import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
|
|
23
|
-
|
|
24
|
-
test("connected and known are separate numbers", () => {
|
|
25
|
-
const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
|
|
26
|
-
assert.deepEqual(describeSwarmReach(torrent), {
|
|
27
|
-
connectedPeers: 2,
|
|
28
|
-
deliveringPeers: 0,
|
|
29
|
-
knownPeers: 7,
|
|
30
|
-
queuedPeers: 4
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("offered but not connected is distinguishable from nobody offered", () => {
|
|
35
|
-
// The two shapes the field produced. They need opposite investigations, and
|
|
36
|
-
// one line has to tell them apart.
|
|
37
|
-
const offeredNotConnected = describeSwarmReach({ wires: [], _peersLength: 5, _numQueued: 5 });
|
|
38
|
-
const nobodyOffered = describeSwarmReach({ wires: [], _peersLength: 0, _numQueued: 0 });
|
|
39
|
-
assert.equal(offeredNotConnected.connectedPeers, 0);
|
|
40
|
-
assert.equal(nobodyOffered.connectedPeers, 0);
|
|
41
|
-
assert.notEqual(offeredNotConnected.knownPeers, nobodyOffered.knownPeers);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("internals that are gone say nothing rather than breaking the poll", () => {
|
|
45
|
-
// `_peersLength` and `_numQueued` are not WebTorrent's published interface.
|
|
46
|
-
// The browser polls this every two seconds, so a version that drops them must
|
|
47
|
-
// cost the field and not the answer.
|
|
48
|
-
assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
|
|
49
|
-
connectedPeers: 1,
|
|
50
|
-
deliveringPeers: 0,
|
|
51
|
-
knownPeers: null,
|
|
52
|
-
queuedPeers: null
|
|
53
|
-
});
|
|
54
|
-
assert.deepEqual(describeSwarmReach({}), {
|
|
55
|
-
connectedPeers: 0,
|
|
56
|
-
deliveringPeers: 0,
|
|
57
|
-
knownPeers: null,
|
|
58
|
-
queuedPeers: null
|
|
59
|
-
});
|
|
60
|
-
assert.deepEqual(describeSwarmReach(null), {
|
|
61
|
-
connectedPeers: 0,
|
|
62
|
-
deliveringPeers: 0,
|
|
63
|
-
knownPeers: null,
|
|
64
|
-
queuedPeers: null
|
|
65
|
-
});
|
|
66
|
-
const throwing = {
|
|
67
|
-
wires: [],
|
|
68
|
-
get _peersLength() {
|
|
69
|
-
throw new Error("destroyed");
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
assert.deepEqual(describeSwarmReach(throwing), {
|
|
73
|
-
connectedPeers: 0,
|
|
74
|
-
deliveringPeers: 0,
|
|
75
|
-
knownPeers: null,
|
|
76
|
-
queuedPeers: null
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("the wait for a first peer is a measured quantity", () => {
|
|
81
|
-
// The field case: added 13:40:30.357, first wire 13:44:47.123.
|
|
82
|
-
assert.equal(secondsToFirstPeer(1000, 258_766), 257.766);
|
|
83
|
-
assert.equal(secondsToFirstPeer(1000, 1000), 0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("no peer yet is not a duration", () => {
|
|
87
|
-
assert.equal(secondsToFirstPeer(1000, null), null);
|
|
88
|
-
assert.equal(secondsToFirstPeer(null, 2000), null);
|
|
89
|
-
// A clock that went backwards is not a negative wait; it is no reading.
|
|
90
|
-
assert.equal(secondsToFirstPeer(2000, 1000), null);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("the best tracker answer wins, not the most recent", () => {
|
|
94
|
-
// A live tracker says 500, a dead one answers 0 two seconds later. Keeping
|
|
95
|
-
// the last would print "nobody offered" about a swarm of five hundred, which
|
|
96
|
-
// inverts the one distinction this figure is carried for.
|
|
97
|
-
const answers = [
|
|
98
|
-
{ seeders: 500, leechers: 40 },
|
|
99
|
-
{ seeders: 0, leechers: 0 }
|
|
100
|
-
];
|
|
101
|
-
assert.deepEqual(bestAnnounce(answers), { seeders: 500, leechers: 40, trackers: 2 });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("trackers that answered are counted even when none knew anything", () => {
|
|
105
|
-
assert.deepEqual(bestAnnounce([{ seeders: null, leechers: null }]), {
|
|
106
|
-
seeders: null,
|
|
107
|
-
leechers: null,
|
|
108
|
-
trackers: 1
|
|
109
|
-
});
|
|
110
|
-
// No tracker has answered at all — a different state from "answered, knows
|
|
111
|
-
// nobody", and the line says so.
|
|
112
|
-
assert.deepEqual(bestAnnounce([]), { seeders: null, leechers: null, trackers: 0 });
|
|
113
|
-
assert.deepEqual(bestAnnounce(null), { seeders: null, leechers: null, trackers: 0 });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("leechers travel with the seeder count they were reported beside", () => {
|
|
117
|
-
const answers = [
|
|
118
|
-
{ seeders: 2, leechers: 99 },
|
|
119
|
-
{ seeders: 7, leechers: 3 }
|
|
120
|
-
];
|
|
121
|
-
assert.equal(bestAnnounce(answers).leechers, 3);
|
|
122
|
-
// Including when the winning answer gave no leecher count: carrying the
|
|
123
|
-
// previous tracker's figure forward would present two trackers' numbers as
|
|
124
|
-
// one reading.
|
|
125
|
-
assert.deepEqual(bestAnnounce([{ seeders: 2, leechers: 99 }, { seeders: 7, leechers: null }]), {
|
|
126
|
-
seeders: 7,
|
|
127
|
-
leechers: null,
|
|
128
|
-
trackers: 2
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The one place that speaks to WebTorrent.
|
|
3
|
-
*
|
|
4
|
-
* Driven against a stub torrent shaped like the vendored 2.8.5: a selection
|
|
5
|
-
* list it can be asked about, a bitfield saying what has arrived, and the
|
|
6
|
-
* private `_select`/`_deselect` the real one exposes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
|
|
12
|
-
import { DemandRegister } from "../services/torrent/demand/DemandRegister.js";
|
|
13
|
-
import { Urgency } from "../services/torrent/demand/Urgency.js";
|
|
14
|
-
import { SwarmSelection } from "../services/torrent/download/SwarmSelection.js";
|
|
15
|
-
|
|
16
|
-
const PIECE = 1024;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} [params]
|
|
20
|
-
* @param {number[]} [params.have] - Pieces that have arrived.
|
|
21
|
-
* @param {number} [params.files] - How many files, each ten pieces long.
|
|
22
|
-
* @returns {object}
|
|
23
|
-
*/
|
|
24
|
-
function stubTorrent({ have = [], files = 1 } = {}) {
|
|
25
|
-
const arrived = new Set(have);
|
|
26
|
-
const items = [];
|
|
27
|
-
return {
|
|
28
|
-
pieceLength: PIECE,
|
|
29
|
-
store: null,
|
|
30
|
-
files: Array.from({ length: files }, (unused, index) => ({
|
|
31
|
-
offset: index * 10 * PIECE,
|
|
32
|
-
length: 10 * PIECE
|
|
33
|
-
})),
|
|
34
|
-
bitfield: { get: (index) => arrived.has(index) },
|
|
35
|
-
_critical: [],
|
|
36
|
-
_selections: { _items: items },
|
|
37
|
-
calls: { select: [], deselect: [], critical: [] },
|
|
38
|
-
_select(from, to, priority, notify, isStream) {
|
|
39
|
-
this.calls.select.push({ from, to, priority, isStream });
|
|
40
|
-
items.push({ from, to, priority });
|
|
41
|
-
},
|
|
42
|
-
_deselect(from, to) {
|
|
43
|
-
this.calls.deselect.push({ from, to });
|
|
44
|
-
const at = items.findIndex((item) => item.from === from && item.to === to);
|
|
45
|
-
if (at >= 0) {
|
|
46
|
-
items.splice(at, 1);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
critical(from, to) {
|
|
50
|
-
this.calls.critical.push({ from, to });
|
|
51
|
-
for (let index = from; index <= to; index += 1) {
|
|
52
|
-
this._critical[index] = true;
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
/** Pretend the library dropped a satisfied selection, which it does. */
|
|
56
|
-
forget(from, to) {
|
|
57
|
-
const at = items.findIndex((item) => item.from === from && item.to === to);
|
|
58
|
-
if (at >= 0) {
|
|
59
|
-
items.splice(at, 1);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
test("nothing is asked of the swarm until somebody states a need", () => {
|
|
66
|
-
const torrent = stubTorrent();
|
|
67
|
-
const selection = new SwarmSelection({ torrent, register: new DemandRegister() });
|
|
68
|
-
|
|
69
|
-
assert.deepEqual(selection.reconcile(), { stated: 0, withdrawn: 0 });
|
|
70
|
-
assert.equal(torrent.calls.select.length, 0);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("two viewers of one film are two instructions, and both are urgent", () => {
|
|
74
|
-
const torrent = stubTorrent();
|
|
75
|
-
const register = new DemandRegister();
|
|
76
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
77
|
-
|
|
78
|
-
// One stopped at the start, one stopped further in. Both pictures are still.
|
|
79
|
-
register.state({ claimant: "v1", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.BLOCKED });
|
|
80
|
-
register.state({ claimant: "v2", fileIndex: 0, byteStart: 5 * PIECE, byteEnd: 6 * PIECE - 1, urgency: Urgency.BLOCKED });
|
|
81
|
-
selection.reconcile();
|
|
82
|
-
|
|
83
|
-
assert.deepEqual(torrent.calls.select, [
|
|
84
|
-
{ from: 0, to: 0, priority: 1, isStream: true },
|
|
85
|
-
{ from: 5, to: 5, priority: 1, isStream: true }
|
|
86
|
-
]);
|
|
87
|
-
// Both may take a block from a slow peer, and the mark spans both.
|
|
88
|
-
assert.deepEqual(torrent.calls.critical, [{ from: 0, to: 5 }]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("the same pieces wanted by two claimants are one instruction", () => {
|
|
92
|
-
const torrent = stubTorrent();
|
|
93
|
-
const register = new DemandRegister();
|
|
94
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
95
|
-
|
|
96
|
-
// Picture and sound of one viewer read the same file and overlap.
|
|
97
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.NEAR });
|
|
98
|
-
register.state({ claimant: "audio", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.NEAR });
|
|
99
|
-
const first = selection.reconcile();
|
|
100
|
-
|
|
101
|
-
assert.equal(first.stated, 1, "one range, told once");
|
|
102
|
-
assert.equal(torrent.calls.select.length, 1);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("the speculative levels are withdrawn whole the moment something urgent is missing", () => {
|
|
106
|
-
const torrent = stubTorrent({ have: [0] });
|
|
107
|
-
const register = new DemandRegister();
|
|
108
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
109
|
-
|
|
110
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.NEAR });
|
|
111
|
-
register.state({ claimant: "fill", fileIndex: 0, byteStart: 5 * PIECE, byteEnd: 9 * PIECE - 1, urgency: Urgency.TAIL });
|
|
112
|
-
selection.reconcile();
|
|
113
|
-
|
|
114
|
-
assert.equal(selection.statedRanges().length, 2, "nothing urgent is missing, so the tail is stated");
|
|
115
|
-
assert.ok(torrent.calls.select.some((call) => call.priority === 0), "and it is stated as zero");
|
|
116
|
-
|
|
117
|
-
// The viewer moves on to a piece that has not arrived.
|
|
118
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: PIECE, byteEnd: 2 * PIECE - 1, urgency: Urgency.NEAR });
|
|
119
|
-
selection.reconcile();
|
|
120
|
-
|
|
121
|
-
assert.deepEqual(
|
|
122
|
-
selection.statedRanges().map((range) => range.priority),
|
|
123
|
-
[1],
|
|
124
|
-
"the tail is out of the download set entirely, not lowered — a peer with nothing urgent to give must not be able to fall through to it"
|
|
125
|
-
);
|
|
126
|
-
assert.ok(torrent.calls.deselect.length > 0);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("a selection the library drops once satisfied is stated again", () => {
|
|
130
|
-
const torrent = stubTorrent();
|
|
131
|
-
const register = new DemandRegister();
|
|
132
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
133
|
-
|
|
134
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.NEAR });
|
|
135
|
-
selection.reconcile();
|
|
136
|
-
assert.equal(torrent.calls.select.length, 1);
|
|
137
|
-
|
|
138
|
-
// Nothing changed: no second instruction.
|
|
139
|
-
selection.reconcile();
|
|
140
|
-
assert.equal(torrent.calls.select.length, 1);
|
|
141
|
-
|
|
142
|
-
// WebTorrent removes a selection once every piece in it has arrived, and says
|
|
143
|
-
// nothing about having done so. The window is still wanted.
|
|
144
|
-
torrent.forget(0, 0);
|
|
145
|
-
selection.reconcile();
|
|
146
|
-
assert.equal(torrent.calls.select.length, 2, "stated again, because the library had let it go");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("a claimant that withdraws takes its instruction with it", () => {
|
|
150
|
-
const torrent = stubTorrent();
|
|
151
|
-
const register = new DemandRegister();
|
|
152
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
153
|
-
|
|
154
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.BLOCKED });
|
|
155
|
-
selection.reconcile();
|
|
156
|
-
register.withdraw("video");
|
|
157
|
-
const after = selection.reconcile();
|
|
158
|
-
|
|
159
|
-
assert.equal(after.withdrawn, 1);
|
|
160
|
-
assert.equal(selection.statedRanges().length, 0);
|
|
161
|
-
// And the displacement mark goes with it: WebTorrent never clears it itself,
|
|
162
|
-
// so a reader walking a film would leave every piece of it marked.
|
|
163
|
-
assert.equal(torrent._critical.some((marked) => marked === true), false);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("a window is bounded by its own file, so it cannot claim the next one", () => {
|
|
167
|
-
const torrent = stubTorrent({ files: 3 });
|
|
168
|
-
const register = new DemandRegister();
|
|
169
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
170
|
-
|
|
171
|
-
// Asking past the end of file 1. File 1 occupies pieces 10-19.
|
|
172
|
-
register.state({
|
|
173
|
-
claimant: "video", fileIndex: 1, byteStart: 0, byteEnd: 100 * PIECE, urgency: Urgency.NEAR
|
|
174
|
-
});
|
|
175
|
-
selection.reconcile();
|
|
176
|
-
|
|
177
|
-
assert.deepEqual(torrent.calls.select, [{ from: 10, to: 19, priority: 1, isStream: true }]);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("releasing everything leaves the library holding nothing of ours", () => {
|
|
181
|
-
const torrent = stubTorrent();
|
|
182
|
-
const register = new DemandRegister();
|
|
183
|
-
const selection = new SwarmSelection({ torrent, register });
|
|
184
|
-
|
|
185
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 3 * PIECE, urgency: Urgency.BLOCKED });
|
|
186
|
-
selection.reconcile();
|
|
187
|
-
selection.releaseAll();
|
|
188
|
-
|
|
189
|
-
assert.equal(torrent._selections._items.length, 0);
|
|
190
|
-
assert.equal(selection.statedRanges().length, 0);
|
|
191
|
-
assert.equal(torrent._critical.some((marked) => marked === true), false);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("the store is told what will be read soon, from the same stated needs", () => {
|
|
195
|
-
const torrent = stubTorrent();
|
|
196
|
-
const protectedBy = new Map();
|
|
197
|
-
torrent.store = {
|
|
198
|
-
protectRange: (claimant, from, to) => protectedBy.set(claimant, `${from}-${to}`),
|
|
199
|
-
releaseProtection: (claimant) => protectedBy.delete(claimant)
|
|
200
|
-
};
|
|
201
|
-
const register = new DemandRegister();
|
|
202
|
-
const selection = new SwarmSelection({ torrent, register, findStore: () => torrent.store });
|
|
203
|
-
|
|
204
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: PIECE - 1, urgency: Urgency.NEAR });
|
|
205
|
-
register.state({ claimant: "fill", fileIndex: 0, byteStart: 5 * PIECE, byteEnd: 9 * PIECE - 1, urgency: Urgency.TAIL });
|
|
206
|
-
selection.reconcile();
|
|
207
|
-
|
|
208
|
-
// One statement, two views of it. Until 2026-09-02 a reader said the same
|
|
209
|
-
// thing twice — once to the store for memory, once to the torrent for
|
|
210
|
-
// download — and a third piece of code read the first to rebuild the second.
|
|
211
|
-
assert.equal(protectedBy.get("video"), "0-0");
|
|
212
|
-
// But only the urgent levels: memory holds what will be READ soon, and the
|
|
213
|
-
// tail is fetched speculatively. Protecting it would push out a piece the
|
|
214
|
-
// decoder is about to want.
|
|
215
|
-
assert.equal(protectedBy.has("fill"), false);
|
|
216
|
-
|
|
217
|
-
register.withdraw("video");
|
|
218
|
-
selection.reconcile();
|
|
219
|
-
assert.equal(protectedBy.size, 0, "a reader that withdrew still held memory");
|
|
220
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Asking a second wire for the blocks a reader is still waiting on.
|
|
3
|
-
*
|
|
4
|
-
* Measured on a real swarm 2026-08-19: a blocked read's tail is 2-14 blocks of
|
|
5
|
-
* 512, and it sits on wires at 109-886 KB/s — above the 48 KB/s gate that
|
|
6
|
-
* WebTorrent's own `_hotswap` requires, so the library never duplicates them.
|
|
7
|
-
* The mechanism is the library's: free the reservation with `Piece.cancel` and
|
|
8
|
-
* leave the first request in flight, then ask a second wire through the
|
|
9
|
-
* library's own request path.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { duplicateTailFor } from "../services/torrent/worker/fastest-wires.js";
|
|
15
|
-
|
|
16
|
-
/** A piece with `chunks` blocks, of which `missing` at the end are absent. */
|
|
17
|
-
function piece(chunks, missing) {
|
|
18
|
-
const buffer = new Array(chunks);
|
|
19
|
-
for (let index = 0; index < chunks - missing; index += 1) {
|
|
20
|
-
buffer[index] = new Uint8Array(1);
|
|
21
|
-
}
|
|
22
|
-
const cancelled = [];
|
|
23
|
-
return {
|
|
24
|
-
_buffer: buffer,
|
|
25
|
-
_chunks: chunks,
|
|
26
|
-
cancel: (index) => cancelled.push(index),
|
|
27
|
-
cancelled
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function wire({ speed = 500_000, has = true } = {}) {
|
|
32
|
-
return {
|
|
33
|
-
peerPieces: { get: () => has },
|
|
34
|
-
downloadSpeed: () => speed,
|
|
35
|
-
peerChoking: false,
|
|
36
|
-
destroyed: false,
|
|
37
|
-
requests: []
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test("the missing blocks are freed and asked of a second wire", () => {
|
|
42
|
-
const target = piece(512, 3);
|
|
43
|
-
const placed = [];
|
|
44
|
-
const torrent = {
|
|
45
|
-
pieces: [target],
|
|
46
|
-
wires: [wire({ speed: 900_000 }), wire({ speed: 800_000 }), wire({ speed: 700_000 })],
|
|
47
|
-
_request: (wireAsked, index) => {
|
|
48
|
-
placed.push({ wireAsked, index });
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const result = duplicateTailFor(torrent, 0);
|
|
54
|
-
|
|
55
|
-
assert.equal(result.missing, 3, "the tail is what is still absent from the piece");
|
|
56
|
-
assert.equal(result.duplicated, 3);
|
|
57
|
-
assert.deepEqual(target.cancelled, [509, 510, 511], "each block's reservation is freed first");
|
|
58
|
-
assert.equal(placed.length, 3, "and each is then asked of a wire through the library's own path");
|
|
59
|
-
assert.equal(placed[0].wireAsked, torrent.wires[0], "fastest wire first");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("at most one duplicate per wire, however long the tail", () => {
|
|
63
|
-
const target = piece(512, 16);
|
|
64
|
-
const torrent = {
|
|
65
|
-
pieces: [target],
|
|
66
|
-
wires: [wire(), wire()],
|
|
67
|
-
_request: () => true
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const result = duplicateTailFor(torrent, 0);
|
|
71
|
-
|
|
72
|
-
assert.equal(result.duplicated, 2, "two wires, two duplicates — the rest waits for the next attempt");
|
|
73
|
-
assert.equal(target.cancelled.length, 2, "and no reservation is freed that nobody was asked for");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("a piece still arriving normally is not duplicated at all", () => {
|
|
77
|
-
// Forty blocks outstanding is not a tail, it is a piece in transit. Asking
|
|
78
|
-
// for a second copy of it would spend the shared link on bytes that are
|
|
79
|
-
// already on their way — which is the whole difference between this and the
|
|
80
|
-
// 2-14 blocks a blocked reader was measured waiting on.
|
|
81
|
-
const target = piece(512, 40);
|
|
82
|
-
const torrent = {
|
|
83
|
-
pieces: [target],
|
|
84
|
-
wires: [wire(), wire()],
|
|
85
|
-
_request: () => true
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const result = duplicateTailFor(torrent, 0);
|
|
89
|
-
|
|
90
|
-
assert.equal(result.duplicated, 0);
|
|
91
|
-
assert.equal(target.cancelled.length, 0, "and nothing is freed either");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("a wire whose pipeline is full does not end the attempt", () => {
|
|
95
|
-
// Pipelines are per wire, so one wire being full says nothing about the next.
|
|
96
|
-
// This used to stop the whole pass on the first refusal, on the stated
|
|
97
|
-
// reasoning that the remaining wires were "no emptier" — an assumption about
|
|
98
|
-
// other peers' queues that nothing measures.
|
|
99
|
-
const target = piece(512, 5);
|
|
100
|
-
const torrent = {
|
|
101
|
-
pieces: [target],
|
|
102
|
-
wires: [wire(), wire(), wire()],
|
|
103
|
-
// The library refuses when the wire already has as many requests as its
|
|
104
|
-
// measured speed justifies.
|
|
105
|
-
_request: () => false
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const result = duplicateTailFor(torrent, 0);
|
|
109
|
-
|
|
110
|
-
assert.equal(result.duplicated, 0);
|
|
111
|
-
assert.equal(
|
|
112
|
-
target.cancelled.length,
|
|
113
|
-
3,
|
|
114
|
-
"every wire was offered a block; a freed reservation nobody took is handed to whoever asks next"
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("a refusal by one wire does not cost the block a faster wire would have taken", () => {
|
|
119
|
-
const target = piece(512, 3);
|
|
120
|
-
const placed = [];
|
|
121
|
-
const torrent = {
|
|
122
|
-
pieces: [target],
|
|
123
|
-
wires: [wire({ speed: 900_000 }), wire({ speed: 800_000 })],
|
|
124
|
-
_request: (wireAsked, index) => {
|
|
125
|
-
// The first wire is full; the second is not.
|
|
126
|
-
if (wireAsked === torrent.wires[0]) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
placed.push(index);
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const result = duplicateTailFor(torrent, 0);
|
|
135
|
-
|
|
136
|
-
assert.equal(result.duplicated, 1);
|
|
137
|
-
assert.deepEqual(placed, [0], "the second wire was still asked");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("a piece with nothing missing is left alone", () => {
|
|
141
|
-
const target = piece(4, 0);
|
|
142
|
-
const torrent = { pieces: [target], wires: [wire()], _request: () => true };
|
|
143
|
-
|
|
144
|
-
const result = duplicateTailFor(torrent, 0);
|
|
145
|
-
|
|
146
|
-
assert.equal(result.duplicated, 0);
|
|
147
|
-
assert.equal(result.missing, 0);
|
|
148
|
-
assert.equal(target.cancelled.length, 0);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("with no wire holding the piece there is nothing to duplicate onto", () => {
|
|
152
|
-
const target = piece(8, 2);
|
|
153
|
-
const torrent = { pieces: [target], wires: [wire({ has: false })], _request: () => true };
|
|
154
|
-
|
|
155
|
-
const result = duplicateTailFor(torrent, 0);
|
|
156
|
-
|
|
157
|
-
assert.equal(result.wires, 0);
|
|
158
|
-
assert.equal(result.duplicated, 0);
|
|
159
|
-
assert.equal(target.cancelled.length, 0, "and no reservation is freed for nobody");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("a completed piece is not touched", () => {
|
|
163
|
-
assert.deepEqual(
|
|
164
|
-
duplicateTailFor({ pieces: [null], wires: [wire()], _request: () => true }, 0),
|
|
165
|
-
{ duplicated: 0, missing: 0, wires: 0 }
|
|
166
|
-
);
|
|
167
|
-
});
|