@torrent-tv/proxy 2.89.17 → 2.89.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/package.json +10 -2
- package/server.js +0 -11
- 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,131 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
|
|
4
|
-
import { wedgeIsCertain } from "../services/transport/data-channel-handler.js";
|
|
5
|
-
import { PROBE_INTERVAL_MS } from "../services/transport/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 still a wedge when the counter has been flat long enough — the channel queue was 0 on the real wedge while usrsctp held 399 MB", () => {
|
|
69
|
-
const verdict = wedgeIsCertain({
|
|
70
|
-
queuedBytes: 0,
|
|
71
|
-
bytesPerSecond: 16 * MEGABYTE,
|
|
72
|
-
flatForMs: 600_000
|
|
73
|
-
});
|
|
74
|
-
assert.equal(verdict.certain, true);
|
|
75
|
-
assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
|
|
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
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A file kept whole is read from the disk by the subtitle walk.
|
|
3
|
-
*
|
|
4
|
-
* Once a file is kept whole its pieces leave the torrent's store while the
|
|
5
|
-
* bitfield still says they are held, and a read through the torrent waited on
|
|
6
|
-
* a store that no longer had them (field 2026-10-01). The reading itself is
|
|
7
|
-
* checked here over a temporary file; the client that uses it starts the
|
|
8
|
-
* torrent thread and is not built.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
14
|
-
import os from "node:os";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
|
|
17
|
-
import { readWholeFile } from "../services/torrent/worker/client.js";
|
|
18
|
-
|
|
19
|
-
test("a range of a whole file is read from the disk, and a range past it is not here", async (t) => {
|
|
20
|
-
const dir = await mkdtemp(path.join(os.tmpdir(), "ttv-whole-"));
|
|
21
|
-
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
22
|
-
const file = path.join(dir, "film.mkv");
|
|
23
|
-
await writeFile(file, Buffer.from("0123456789"));
|
|
24
|
-
const whole = { path: file, length: 10, name: "film.mkv" };
|
|
25
|
-
|
|
26
|
-
assert.equal((await readWholeFile(whole, 2, 5)).toString(), "2345");
|
|
27
|
-
assert.equal((await readWholeFile(whole, 8, 20)).toString(), "89", "clamped to the file");
|
|
28
|
-
assert.equal(await readWholeFile(whole, 12, 20), null);
|
|
29
|
-
await assert.rejects(readWholeFile({ ...whole, path: path.join(dir, "gone.mkv") }, 0, 1), { code: "ENOENT" });
|
|
30
|
-
});
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One owner of the fact "this proxy has piece N".
|
|
3
|
-
*
|
|
4
|
-
* The store holds the bytes, so it owns the fact. The library keeps a second
|
|
5
|
-
* copy of it in its completion bitfield, and until 2026-09-12 nothing
|
|
6
|
-
* reconciled them: the disk tier dropped a piece once every reader was past it
|
|
7
|
-
* — correctly, that is what bounds the spill — and the bitfield went on saying
|
|
8
|
-
* the piece was verified. A read then concluded the piece was had, asked for it,
|
|
9
|
-
* was told it was absent, and failed; nothing fetched it again either, because
|
|
10
|
-
* the library does not download what it believes it owns. Field: a film played
|
|
11
|
-
* 80 seconds and then answered `Piece 0 is verified but absent from the store`
|
|
12
|
-
* for 92 minutes.
|
|
13
|
-
*
|
|
14
|
-
* Pinned here: the announcement and its one rule — said when the piece has gone
|
|
15
|
-
* from EVERYWHERE, never when one tier alone lost it — and the withdrawal.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import os from "node:os";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import fs from "node:fs/promises";
|
|
23
|
-
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
24
|
-
import { withdrawClaim } from "../services/torrent/download/withdraw-claim.js";
|
|
25
|
-
|
|
26
|
-
const PIECE = 1024;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A store of four pieces with room for one, so every admission spills the last,
|
|
30
|
-
* plus the announcements it made.
|
|
31
|
-
*/
|
|
32
|
-
async function storeWithGone(extras = {}) {
|
|
33
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "withdraw-test-"));
|
|
34
|
-
/** @type {number[]} */
|
|
35
|
-
const gone = [];
|
|
36
|
-
const store = new SharedPieceStore(PIECE, {
|
|
37
|
-
length: 4 * PIECE,
|
|
38
|
-
memoryBytes: PIECE,
|
|
39
|
-
path: directory,
|
|
40
|
-
name: "test",
|
|
41
|
-
files: [{ offset: 0, length: 4 * PIECE, name: "file.bin" }],
|
|
42
|
-
onPieceGone: ({ index }) => gone.push(index),
|
|
43
|
-
...extras
|
|
44
|
-
});
|
|
45
|
-
const put = (index) => new Promise((resolve, reject) => {
|
|
46
|
-
store.put(index, Buffer.alloc(PIECE, index + 1), (error) => (error ? reject(error) : resolve()));
|
|
47
|
-
});
|
|
48
|
-
const clean = async () => {
|
|
49
|
-
store.destroy(() => undefined);
|
|
50
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
51
|
-
};
|
|
52
|
-
return { store, gone, put, clean };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Wait for the CONDITION, with a deadline only as a backstop. A fixed pause
|
|
57
|
-
* here would measure the machine rather than the store.
|
|
58
|
-
*
|
|
59
|
-
* @param {() => boolean} ready
|
|
60
|
-
* @param {string} what
|
|
61
|
-
*/
|
|
62
|
-
async function until(ready, what) {
|
|
63
|
-
const deadline = Date.now() + 5_000;
|
|
64
|
-
while (!ready()) {
|
|
65
|
-
if (Date.now() > deadline) {
|
|
66
|
-
throw new Error(`timed out waiting for ${what}`);
|
|
67
|
-
}
|
|
68
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
test("a piece left behind every reader is dropped AND the claim withdrawn", async () => {
|
|
73
|
-
const { store, gone, put, clean } = await storeWithGone();
|
|
74
|
-
try {
|
|
75
|
-
await put(0);
|
|
76
|
-
await put(1);
|
|
77
|
-
await put(2);
|
|
78
|
-
// WHERE THE READERS STAND, which is the whole trigger: the encoder ran
|
|
79
|
-
// ahead, so pieces 0-1 are behind every one of them. This is the production
|
|
80
|
-
// path — `reviseSpillCeiling` asks `forgetBehind(readHeads)` — and it is
|
|
81
|
-
// what dropped 565 pieces in the field.
|
|
82
|
-
store.protectRange("reader", 2, 3, 0);
|
|
83
|
-
const revision = store.reviseSpillCeiling(null);
|
|
84
|
-
|
|
85
|
-
assert.ok(revision.behind >= 1, `something should have been dropped, got ${revision.behind}`);
|
|
86
|
-
assert.ok(gone.includes(0), `piece 0 has gone and should say so, got ${JSON.stringify(gone)}`);
|
|
87
|
-
assert.ok(
|
|
88
|
-
gone.every((index) => index < 2),
|
|
89
|
-
`nothing a reader still wants may be announced, got ${JSON.stringify(gone)}`
|
|
90
|
-
);
|
|
91
|
-
} finally {
|
|
92
|
-
await clean();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("a piece dropped as a duplicate of a file held whole is NOT announced", async () => {
|
|
97
|
-
const { store, gone, put, clean } = await storeWithGone({
|
|
98
|
-
// Every piece can be had from the assembled file, which is exactly why the
|
|
99
|
-
// spilled copy is being dropped. Nothing has been lost, so nothing is said.
|
|
100
|
-
isPieceElsewhere: () => true
|
|
101
|
-
});
|
|
102
|
-
try {
|
|
103
|
-
await put(0);
|
|
104
|
-
await put(1);
|
|
105
|
-
await put(2);
|
|
106
|
-
// The spill is what puts a piece on disk, and it finishes on its own time;
|
|
107
|
-
// dropping duplicates deliberately leaves a piece whose spill is still in
|
|
108
|
-
// flight alone, so the precondition is waited for rather than assumed.
|
|
109
|
-
await until(() => store.stats().spilled >= 1, "a piece to reach the disk");
|
|
110
|
-
const dropped = store.dropDuplicatesHeldElsewhere();
|
|
111
|
-
assert.ok(dropped >= 1, "the spilled duplicate should have been dropped");
|
|
112
|
-
assert.deepEqual(gone, [], "a piece still readable from a whole file has not gone");
|
|
113
|
-
} finally {
|
|
114
|
-
await clean();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("a closing store announces nothing, because its torrent is going too", async () => {
|
|
119
|
-
const { store, gone, put, clean } = await storeWithGone();
|
|
120
|
-
try {
|
|
121
|
-
await put(0);
|
|
122
|
-
await put(1);
|
|
123
|
-
store.protectRange("reader", 2, 3, 0);
|
|
124
|
-
store.close(() => undefined);
|
|
125
|
-
store.reviseSpillCeiling(null);
|
|
126
|
-
assert.deepEqual(gone, [], "a claim withdrawn against a dying torrent reaches nothing useful");
|
|
127
|
-
} finally {
|
|
128
|
-
await clean();
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("the withdrawal is counted in the store's own figures", async () => {
|
|
133
|
-
const { store, put, clean } = await storeWithGone();
|
|
134
|
-
try {
|
|
135
|
-
await put(0);
|
|
136
|
-
await put(1);
|
|
137
|
-
await put(2);
|
|
138
|
-
store.protectRange("reader", 2, 3, 0);
|
|
139
|
-
store.reviseSpillCeiling(null);
|
|
140
|
-
assert.ok(
|
|
141
|
-
store.stats().withdrawn >= 1,
|
|
142
|
-
"the figure that makes the eviction's bargain checkable must move"
|
|
143
|
-
);
|
|
144
|
-
} finally {
|
|
145
|
-
await clean();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("a piece the library thinks it has is withdrawn", () => {
|
|
150
|
-
const asked = [];
|
|
151
|
-
const torrent = {
|
|
152
|
-
name: "film.mkv",
|
|
153
|
-
destroyed: false,
|
|
154
|
-
bitfield: { get: () => true },
|
|
155
|
-
_markUnverified: (index) => asked.push(index)
|
|
156
|
-
};
|
|
157
|
-
assert.equal(withdrawClaim({ index: 3, files: [{ _torrent: torrent }] }), "withdrawn");
|
|
158
|
-
assert.deepEqual(asked, [3]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("a piece the library already knows is missing is left alone", () => {
|
|
162
|
-
let touched = 0;
|
|
163
|
-
const torrent = {
|
|
164
|
-
destroyed: false,
|
|
165
|
-
bitfield: { get: () => false },
|
|
166
|
-
_markUnverified: () => { touched += 1; }
|
|
167
|
-
};
|
|
168
|
-
assert.equal(
|
|
169
|
-
withdrawClaim({ index: 7, torrent }),
|
|
170
|
-
"nothing-to-withdraw",
|
|
171
|
-
"re-creating a piece the library is already fetching would discard its blocks in flight"
|
|
172
|
-
);
|
|
173
|
-
assert.equal(touched, 0);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("a destroyed torrent is left alone", () => {
|
|
177
|
-
let touched = 0;
|
|
178
|
-
const torrent = {
|
|
179
|
-
destroyed: true,
|
|
180
|
-
bitfield: { get: () => true },
|
|
181
|
-
_markUnverified: () => { touched += 1; }
|
|
182
|
-
};
|
|
183
|
-
assert.equal(withdrawClaim({ index: 1, torrent }), "no-torrent");
|
|
184
|
-
assert.equal(touched, 0);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("a library that refuses says so instead of failing the eviction", () => {
|
|
188
|
-
const said = [];
|
|
189
|
-
const torrent = {
|
|
190
|
-
name: "film.mkv",
|
|
191
|
-
destroyed: false,
|
|
192
|
-
bitfield: { get: () => true },
|
|
193
|
-
_markUnverified: () => { throw new Error("no such method any more"); }
|
|
194
|
-
};
|
|
195
|
-
assert.equal(withdrawClaim({ index: 2, torrent, warn: (line) => said.push(line) }), "refused");
|
|
196
|
-
assert.equal(said.length, 1);
|
|
197
|
-
assert.match(said[0], /piece 2 of film\.mkv/);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("a store with nobody listening evicts exactly as it did before", async () => {
|
|
201
|
-
const { store, put, clean } = await storeWithGone({ onPieceGone: undefined });
|
|
202
|
-
try {
|
|
203
|
-
await put(0);
|
|
204
|
-
await put(1);
|
|
205
|
-
await put(2);
|
|
206
|
-
store.protectRange("reader", 2, 3, 0);
|
|
207
|
-
const revision = store.reviseSpillCeiling(null);
|
|
208
|
-
assert.ok(revision.behind >= 1, "the eviction does not depend on anybody listening");
|
|
209
|
-
} finally {
|
|
210
|
-
await clean();
|
|
211
|
-
}
|
|
212
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The worker transport's memory contract.
|
|
3
|
-
*
|
|
4
|
-
* These cover the defect that broke playback in 2.9.71-2.9.73: the worker
|
|
5
|
-
* handed the main thread ownership of memory it did not own. WebTorrent's piece
|
|
6
|
-
* cache keeps the buffer it returns and slices it again on the next read, so
|
|
7
|
-
* transferring it detached the cache's own memory and every later read failed
|
|
8
|
-
* with "Cannot perform %TypedArray%.prototype.slice on a detached ArrayBuffer".
|
|
9
|
-
*
|
|
10
|
-
* The field could not tell us this, because the error never reached anyone: the
|
|
11
|
-
* worker sent READ_END from its `finally` before the error was posted, and the
|
|
12
|
-
* main thread had no handler for a read error at all. So a failed read looked
|
|
13
|
-
* exactly like an empty file.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import test from "node:test";
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
|
-
import { MessageChannel } from "node:worker_threads";
|
|
19
|
-
import { createSendStream, createReceiveStream, createCaller } from "../services/torrent/worker/channel.js";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A buffer standing in for one owned by WebTorrent's piece cache: allocated
|
|
23
|
-
* outside Node's shared pool, covering its whole region, and still referenced
|
|
24
|
-
* by its owner after we hand it on.
|
|
25
|
-
*
|
|
26
|
-
* @param {number} size
|
|
27
|
-
* @param {number} fill
|
|
28
|
-
* @returns {Buffer}
|
|
29
|
-
*/
|
|
30
|
-
function foreignPiece(size, fill) {
|
|
31
|
-
const piece = Buffer.allocUnsafeSlow(size);
|
|
32
|
-
piece.fill(fill);
|
|
33
|
-
return piece;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
test("sending a chunk leaves the source buffer usable by its owner", async () => {
|
|
37
|
-
const { port1, port2 } = new MessageChannel();
|
|
38
|
-
try {
|
|
39
|
-
const sender = createSendStream({ port: port1, requestId: 1 });
|
|
40
|
-
const piece = foreignPiece(1024 * 1024, 7);
|
|
41
|
-
|
|
42
|
-
await sender.send(piece);
|
|
43
|
-
|
|
44
|
-
// The owner reads its own buffer again, exactly as the piece cache does on
|
|
45
|
-
// the next request for the same piece.
|
|
46
|
-
assert.equal(piece.length, 1024 * 1024, "buffer was detached by the send");
|
|
47
|
-
assert.equal(piece[0], 7);
|
|
48
|
-
assert.equal(piece.subarray(0, 16).length, 16, "slicing the source failed");
|
|
49
|
-
} finally {
|
|
50
|
-
port1.close();
|
|
51
|
-
port2.close();
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("a second read of the same piece still returns its bytes", async () => {
|
|
56
|
-
const { port1, port2 } = new MessageChannel();
|
|
57
|
-
try {
|
|
58
|
-
const piece = foreignPiece(512 * 1024, 3);
|
|
59
|
-
|
|
60
|
-
for (const requestId of [1, 2]) {
|
|
61
|
-
const sender = createSendStream({ port: port1, requestId });
|
|
62
|
-
await sender.send(piece);
|
|
63
|
-
sender.end();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
assert.equal(piece[0], 3, "the piece did not survive being sent twice");
|
|
67
|
-
} finally {
|
|
68
|
-
port1.close();
|
|
69
|
-
port2.close();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("chunks arrive with their contents intact", async () => {
|
|
74
|
-
const { port1, port2 } = new MessageChannel();
|
|
75
|
-
try {
|
|
76
|
-
const received = [];
|
|
77
|
-
// Waited on rather than slept through. This test used to sleep 50 ms and
|
|
78
|
-
// then assert, so it failed whenever the run was busy — recorded as flaky in
|
|
79
|
-
// roadmap items 53 and 54, and it failed again on 2026-09-02 for a reason
|
|
80
|
-
// that shows exactly how little a chosen interval is worth: the worker had
|
|
81
|
-
// gained one more module to load, and 50 ms stopped being enough.
|
|
82
|
-
let arrived;
|
|
83
|
-
const firstChunk = new Promise((resolve) => { arrived = resolve; });
|
|
84
|
-
port2.on("message", (message) => {
|
|
85
|
-
if (message?.type === "chunk") {
|
|
86
|
-
received.push(Buffer.from(message.bytes));
|
|
87
|
-
arrived();
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const sender = createSendStream({ port: port1, requestId: 1 });
|
|
92
|
-
const piece = foreignPiece(256 * 1024, 42);
|
|
93
|
-
await sender.send(piece);
|
|
94
|
-
sender.end();
|
|
95
|
-
|
|
96
|
-
await firstChunk;
|
|
97
|
-
assert.equal(received.length, 1);
|
|
98
|
-
assert.equal(received[0].length, 256 * 1024);
|
|
99
|
-
assert.equal(received[0][0], 42);
|
|
100
|
-
assert.equal(received[0][received[0].length - 1], 42);
|
|
101
|
-
} finally {
|
|
102
|
-
port1.close();
|
|
103
|
-
port2.close();
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("reads and commands never share a request id", () => {
|
|
108
|
-
const { port1, port2 } = new MessageChannel();
|
|
109
|
-
try {
|
|
110
|
-
const commandIds = [];
|
|
111
|
-
port2.on("message", (message) => commandIds.push(message.id));
|
|
112
|
-
|
|
113
|
-
const caller = createCaller(port1);
|
|
114
|
-
const readIds = [];
|
|
115
|
-
|
|
116
|
-
// Interleaved exactly as the real client does it: commands through `call`,
|
|
117
|
-
// reads taking an id directly, both over the same channel.
|
|
118
|
-
for (let round = 0; round < 50; round += 1) {
|
|
119
|
-
void caller.call("noop", {});
|
|
120
|
-
readIds.push(caller.nextId());
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// A repeat between the two sequences is the collision that let a read's
|
|
124
|
-
// reply resolve a command — with its result, silently.
|
|
125
|
-
const everyId = [...commandIds, ...readIds];
|
|
126
|
-
assert.equal(
|
|
127
|
-
new Set(everyId).size,
|
|
128
|
-
everyId.length,
|
|
129
|
-
"an id was handed out to both a command and a read"
|
|
130
|
-
);
|
|
131
|
-
} finally {
|
|
132
|
-
port1.close();
|
|
133
|
-
port2.close();
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("a failed read surfaces on the reader instead of ending quietly", async () => {
|
|
138
|
-
const { port1, port2 } = new MessageChannel();
|
|
139
|
-
try {
|
|
140
|
-
const receive = createReceiveStream({
|
|
141
|
-
port: port1,
|
|
142
|
-
requestId: 1,
|
|
143
|
-
onCancel: () => undefined
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
receive.fail(new Error("read failed in the worker"));
|
|
147
|
-
|
|
148
|
-
const reader = receive.stream.getReader();
|
|
149
|
-
await assert.rejects(
|
|
150
|
-
() => reader.read(),
|
|
151
|
-
/read failed in the worker/,
|
|
152
|
-
"the reader saw a clean end instead of the failure"
|
|
153
|
-
);
|
|
154
|
-
} finally {
|
|
155
|
-
port1.close();
|
|
156
|
-
port2.close();
|
|
157
|
-
}
|
|
158
|
-
});
|