@torrent-tv/proxy 2.87.3 → 2.87.4
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2157 -2153
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +94 -94
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -181
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +721 -720
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +212 -212
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +856 -848
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +36 -36
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2635
- package/services/torrent/torrent-source-key.js +38 -38
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -760
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +365 -350
- package/services/torrent/worker/protocol.js +184 -182
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1116
- package/services/transport/data-channel-handler.js +1356 -1356
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +73 -73
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +176 -176
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What is wanted, by whom, and how urgently.
|
|
3
|
-
*
|
|
4
|
-
* These are the rules the download layer and the memory store both read, so a
|
|
5
|
-
* defect here is a defect in two places at once. Everything is numbers: no
|
|
6
|
-
* torrent, no library, no piece store.
|
|
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 { unionOf, Window } from "../services/torrent/demand/Window.js";
|
|
14
|
-
import {
|
|
15
|
-
isConditional,
|
|
16
|
-
mayDisplaceSlowPeer,
|
|
17
|
-
selectionPriority,
|
|
18
|
-
Urgency
|
|
19
|
-
} from "../services/torrent/demand/Urgency.js";
|
|
20
|
-
import { nearestFirst, piecesNeededFor, piecesOf, piecesWithin } from "../services/torrent/demand/pieces.js";
|
|
21
|
-
|
|
22
|
-
const MEGABYTE = 1024 * 1024;
|
|
23
|
-
|
|
24
|
-
test("a window states bytes and refuses what it cannot mean", () => {
|
|
25
|
-
const window = new Window({
|
|
26
|
-
claimant: "video", fileIndex: 0, byteStart: 100, byteEnd: 199, urgency: Urgency.NEAR
|
|
27
|
-
});
|
|
28
|
-
assert.equal(window.byteLength, 100, "inclusive at both ends");
|
|
29
|
-
|
|
30
|
-
assert.throws(() => new Window({
|
|
31
|
-
claimant: "", fileIndex: 0, byteStart: 0, byteEnd: 1, urgency: 0
|
|
32
|
-
}), /claimant/, "a window nobody can release is a leak with a name missing");
|
|
33
|
-
assert.throws(() => new Window({
|
|
34
|
-
claimant: "video", fileIndex: 0, byteStart: 200, byteEnd: 100, urgency: 0
|
|
35
|
-
}), /Byte end/);
|
|
36
|
-
assert.throws(() => new Window({
|
|
37
|
-
claimant: "video", fileIndex: -1, byteStart: 0, byteEnd: 1, urgency: 0
|
|
38
|
-
}), /File index/);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("windows of different files are never merged", () => {
|
|
42
|
-
const register = new DemandRegister();
|
|
43
|
-
register.state({ claimant: "a", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
44
|
-
register.state({ claimant: "b", fileIndex: 1, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
45
|
-
|
|
46
|
-
// Byte 50 of file 0 and byte 50 of file 1 are different bytes. Two viewers on
|
|
47
|
-
// two episodes of one release is the ordinary case, not an edge one.
|
|
48
|
-
assert.deepEqual(register.union(), [
|
|
49
|
-
{ byteStart: 0, byteEnd: 99 },
|
|
50
|
-
{ byteStart: 0, byteEnd: 99 }
|
|
51
|
-
]);
|
|
52
|
-
assert.deepEqual(register.files(), [0, 1]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("the union of overlapping windows is their union, not their sum", () => {
|
|
56
|
-
// Picture and sound are two readers of one file and their windows overlap by
|
|
57
|
-
// construction. Adding them would report a demand that is not there.
|
|
58
|
-
assert.deepEqual(
|
|
59
|
-
unionOf([
|
|
60
|
-
{ byteStart: 0, byteEnd: 99 },
|
|
61
|
-
{ byteStart: 50, byteEnd: 149 }
|
|
62
|
-
]),
|
|
63
|
-
[{ byteStart: 0, byteEnd: 149 }]
|
|
64
|
-
);
|
|
65
|
-
// Touching ranges are one run: 0-99 and 100-199 have nothing between them.
|
|
66
|
-
assert.deepEqual(
|
|
67
|
-
unionOf([
|
|
68
|
-
{ byteStart: 100, byteEnd: 199 },
|
|
69
|
-
{ byteStart: 0, byteEnd: 99 }
|
|
70
|
-
]),
|
|
71
|
-
[{ byteStart: 0, byteEnd: 199 }]
|
|
72
|
-
);
|
|
73
|
-
assert.deepEqual(
|
|
74
|
-
unionOf([
|
|
75
|
-
{ byteStart: 0, byteEnd: 99 },
|
|
76
|
-
{ byteStart: 200, byteEnd: 299 }
|
|
77
|
-
]),
|
|
78
|
-
[{ byteStart: 0, byteEnd: 99 }, { byteStart: 200, byteEnd: 299 }],
|
|
79
|
-
"a gap between them is a gap"
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("a claimant states one window, and restating replaces it", () => {
|
|
84
|
-
const register = new DemandRegister();
|
|
85
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
86
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 100, byteEnd: 199, urgency: Urgency.NEAR });
|
|
87
|
-
|
|
88
|
-
// A reader walking a film restates its window many times a second. If those
|
|
89
|
-
// accumulated, the download set would be the whole file within a minute.
|
|
90
|
-
assert.equal(register.size, 1);
|
|
91
|
-
assert.deepEqual(register.union(0), [{ byteStart: 100, byteEnd: 199 }]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("a withdrawal names its own claimant and nothing else", () => {
|
|
95
|
-
const register = new DemandRegister();
|
|
96
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
97
|
-
register.state({ claimant: "audio", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
98
|
-
|
|
99
|
-
assert.equal(register.withdraw("audio"), true);
|
|
100
|
-
assert.equal(register.withdraw("audio"), false, "a second release withdraws nothing");
|
|
101
|
-
assert.equal(register.withdraw("nobody"), false, "a stray release matches nothing");
|
|
102
|
-
assert.equal(register.size, 1, "and video still wants what it wanted");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("the speculative levels are stated only while nothing urgent is missing", () => {
|
|
106
|
-
const register = new DemandRegister();
|
|
107
|
-
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.BLOCKED });
|
|
108
|
-
register.state({ claimant: "fill", fileIndex: 0, byteStart: 1000, byteEnd: 9999, urgency: Urgency.TAIL });
|
|
109
|
-
register.state({ claimant: "back", fileIndex: 0, byteStart: 0, byteEnd: 999, urgency: Urgency.BEHIND });
|
|
110
|
-
|
|
111
|
-
// Something urgent is missing: the speculative levels are not stated at all.
|
|
112
|
-
// Withdrawn, not lowered — a withdrawn window is not in the download set, so
|
|
113
|
-
// a peer with nothing urgent to give cannot fall through to it and spend the
|
|
114
|
-
// shared link on a piece nobody is waiting for.
|
|
115
|
-
assert.deepEqual(
|
|
116
|
-
register.levelsToState((window) => window.urgency !== Urgency.BLOCKED),
|
|
117
|
-
[Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD]
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// Everything urgent has arrived: they are stated, in order.
|
|
121
|
-
assert.deepEqual(
|
|
122
|
-
register.levelsToState(() => true),
|
|
123
|
-
[Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD, Urgency.TAIL, Urgency.BEHIND]
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// The tail is complete but the gap behind is not: the gap stays stated and
|
|
127
|
-
// nothing below it exists to withhold.
|
|
128
|
-
assert.deepEqual(
|
|
129
|
-
register.levelsToState((window) => window.urgency !== Urgency.BEHIND),
|
|
130
|
-
[Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD, Urgency.TAIL, Urgency.BEHIND]
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("only the level being waited on may take a block from a slow peer", () => {
|
|
135
|
-
assert.equal(mayDisplaceSlowPeer(Urgency.BLOCKED), true);
|
|
136
|
-
for (const urgency of [Urgency.NEAR, Urgency.AHEAD, Urgency.TAIL, Urgency.BEHIND]) {
|
|
137
|
-
assert.equal(mayDisplaceSlowPeer(urgency), false);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("the library is given the only distinction it keeps", () => {
|
|
142
|
-
// Measured against the vendored 2.8.5: distinct non-zero priorities order the
|
|
143
|
-
// selection list once and are round-robined afterwards by `shufflePriority`.
|
|
144
|
-
// Zero is the only value that stays put, always last.
|
|
145
|
-
assert.equal(selectionPriority(Urgency.BLOCKED), 1);
|
|
146
|
-
assert.equal(selectionPriority(Urgency.NEAR), 1);
|
|
147
|
-
assert.equal(selectionPriority(Urgency.AHEAD), 1);
|
|
148
|
-
assert.equal(selectionPriority(Urgency.TAIL), 0);
|
|
149
|
-
assert.equal(selectionPriority(Urgency.BEHIND), 0);
|
|
150
|
-
|
|
151
|
-
assert.equal(isConditional(Urgency.AHEAD), false);
|
|
152
|
-
assert.equal(isConditional(Urgency.TAIL), true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("bytes become pieces in one place, and a partial piece is a whole piece", () => {
|
|
156
|
-
const pieceLength = 4 * MEGABYTE;
|
|
157
|
-
// A file starting one piece into the torrent, wanting its first byte.
|
|
158
|
-
assert.deepEqual(
|
|
159
|
-
piecesOf({ fileOffset: pieceLength, byteStart: 0, byteEnd: 0, pieceLength }),
|
|
160
|
-
{ from: 1, to: 1 }
|
|
161
|
-
);
|
|
162
|
-
// A range ending one byte into the next piece still needs that whole piece:
|
|
163
|
-
// the protocol delivers and verifies nothing smaller.
|
|
164
|
-
assert.deepEqual(
|
|
165
|
-
piecesOf({ fileOffset: 0, byteStart: 0, byteEnd: pieceLength, pieceLength }),
|
|
166
|
-
{ from: 0, to: 1 }
|
|
167
|
-
);
|
|
168
|
-
assert.equal(piecesOf({ fileOffset: 0, byteStart: 10, byteEnd: 5, pieceLength }), null);
|
|
169
|
-
assert.equal(piecesOf({ fileOffset: 0, byteStart: 0, byteEnd: 1, pieceLength: 0 }), null);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("a budget buys whole places, and a window needs one more than it measures", () => {
|
|
173
|
-
const pieceLength = 16 * MEGABYTE;
|
|
174
|
-
// The failure this rounding hid: 64 MB of allowance is four places, and two
|
|
175
|
-
// readers asking for 96 MB each want six. Counted in bytes the shortage is
|
|
176
|
-
// plain; counted in floored pieces it is not.
|
|
177
|
-
assert.equal(piecesWithin(64 * MEGABYTE, pieceLength), 4);
|
|
178
|
-
assert.equal(piecesNeededFor(96 * MEGABYTE, pieceLength), 7, "six, plus the one it can straddle");
|
|
179
|
-
assert.equal(piecesWithin(0, pieceLength), 0);
|
|
180
|
-
assert.equal(piecesNeededFor(0, pieceLength), 0);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("the gap behind the playhead is stated nearest first", () => {
|
|
184
|
-
// WebTorrent walks a selection from its start upwards, so one claim over
|
|
185
|
-
// everything behind the viewer would be fetched from the beginning of the
|
|
186
|
-
// file — the end furthest from where a backward seek lands.
|
|
187
|
-
const ranges = nearestFirst({ byteStart: 0, byteEnd: 999, parts: 4 });
|
|
188
|
-
assert.equal(ranges.length, 4);
|
|
189
|
-
assert.equal(ranges[0].byteEnd, 999, "the part nearest the playhead comes first");
|
|
190
|
-
assert.equal(ranges[ranges.length - 1].byteStart, 0, "and the furthest comes last");
|
|
191
|
-
for (const range of ranges) {
|
|
192
|
-
assert.ok(range.byteStart <= range.byteEnd);
|
|
193
|
-
}
|
|
194
|
-
assert.deepEqual(nearestFirst({ byteStart: 10, byteEnd: 5, parts: 2 }), []);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("a byte is as urgent as the most urgent thing that wants it", () => {
|
|
198
|
-
const register = new DemandRegister();
|
|
199
|
-
register.state({ claimant: "map:tail", fileIndex: 0, byteStart: 0, byteEnd: 999, urgency: Urgency.TAIL });
|
|
200
|
-
register.state({ claimant: "map:near", fileIndex: 0, byteStart: 400, byteEnd: 599, urgency: Urgency.NEAR });
|
|
201
|
-
|
|
202
|
-
assert.equal(register.urgencyAt(0, 100), Urgency.TAIL);
|
|
203
|
-
assert.equal(register.urgencyAt(0, 500), Urgency.NEAR, "the overlap took the less urgent of the two");
|
|
204
|
-
assert.equal(register.urgencyAt(0, 5000), null, "a byte nobody wants reported a level anyway");
|
|
205
|
-
assert.equal(register.urgencyAt(1, 500), null, "byte 500 of another file is not this byte");
|
|
206
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What is wanted, by whom, and how urgently.
|
|
3
|
+
*
|
|
4
|
+
* These are the rules the download layer and the memory store both read, so a
|
|
5
|
+
* defect here is a defect in two places at once. Everything is numbers: no
|
|
6
|
+
* torrent, no library, no piece store.
|
|
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 { unionOf, Window } from "../services/torrent/demand/Window.js";
|
|
14
|
+
import {
|
|
15
|
+
isConditional,
|
|
16
|
+
mayDisplaceSlowPeer,
|
|
17
|
+
selectionPriority,
|
|
18
|
+
Urgency
|
|
19
|
+
} from "../services/torrent/demand/Urgency.js";
|
|
20
|
+
import { nearestFirst, piecesNeededFor, piecesOf, piecesWithin } from "../services/torrent/demand/pieces.js";
|
|
21
|
+
|
|
22
|
+
const MEGABYTE = 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
test("a window states bytes and refuses what it cannot mean", () => {
|
|
25
|
+
const window = new Window({
|
|
26
|
+
claimant: "video", fileIndex: 0, byteStart: 100, byteEnd: 199, urgency: Urgency.NEAR
|
|
27
|
+
});
|
|
28
|
+
assert.equal(window.byteLength, 100, "inclusive at both ends");
|
|
29
|
+
|
|
30
|
+
assert.throws(() => new Window({
|
|
31
|
+
claimant: "", fileIndex: 0, byteStart: 0, byteEnd: 1, urgency: 0
|
|
32
|
+
}), /claimant/, "a window nobody can release is a leak with a name missing");
|
|
33
|
+
assert.throws(() => new Window({
|
|
34
|
+
claimant: "video", fileIndex: 0, byteStart: 200, byteEnd: 100, urgency: 0
|
|
35
|
+
}), /Byte end/);
|
|
36
|
+
assert.throws(() => new Window({
|
|
37
|
+
claimant: "video", fileIndex: -1, byteStart: 0, byteEnd: 1, urgency: 0
|
|
38
|
+
}), /File index/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("windows of different files are never merged", () => {
|
|
42
|
+
const register = new DemandRegister();
|
|
43
|
+
register.state({ claimant: "a", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
44
|
+
register.state({ claimant: "b", fileIndex: 1, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
45
|
+
|
|
46
|
+
// Byte 50 of file 0 and byte 50 of file 1 are different bytes. Two viewers on
|
|
47
|
+
// two episodes of one release is the ordinary case, not an edge one.
|
|
48
|
+
assert.deepEqual(register.union(), [
|
|
49
|
+
{ byteStart: 0, byteEnd: 99 },
|
|
50
|
+
{ byteStart: 0, byteEnd: 99 }
|
|
51
|
+
]);
|
|
52
|
+
assert.deepEqual(register.files(), [0, 1]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the union of overlapping windows is their union, not their sum", () => {
|
|
56
|
+
// Picture and sound are two readers of one file and their windows overlap by
|
|
57
|
+
// construction. Adding them would report a demand that is not there.
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
unionOf([
|
|
60
|
+
{ byteStart: 0, byteEnd: 99 },
|
|
61
|
+
{ byteStart: 50, byteEnd: 149 }
|
|
62
|
+
]),
|
|
63
|
+
[{ byteStart: 0, byteEnd: 149 }]
|
|
64
|
+
);
|
|
65
|
+
// Touching ranges are one run: 0-99 and 100-199 have nothing between them.
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
unionOf([
|
|
68
|
+
{ byteStart: 100, byteEnd: 199 },
|
|
69
|
+
{ byteStart: 0, byteEnd: 99 }
|
|
70
|
+
]),
|
|
71
|
+
[{ byteStart: 0, byteEnd: 199 }]
|
|
72
|
+
);
|
|
73
|
+
assert.deepEqual(
|
|
74
|
+
unionOf([
|
|
75
|
+
{ byteStart: 0, byteEnd: 99 },
|
|
76
|
+
{ byteStart: 200, byteEnd: 299 }
|
|
77
|
+
]),
|
|
78
|
+
[{ byteStart: 0, byteEnd: 99 }, { byteStart: 200, byteEnd: 299 }],
|
|
79
|
+
"a gap between them is a gap"
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a claimant states one window, and restating replaces it", () => {
|
|
84
|
+
const register = new DemandRegister();
|
|
85
|
+
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
86
|
+
register.state({ claimant: "video", fileIndex: 0, byteStart: 100, byteEnd: 199, urgency: Urgency.NEAR });
|
|
87
|
+
|
|
88
|
+
// A reader walking a film restates its window many times a second. If those
|
|
89
|
+
// accumulated, the download set would be the whole file within a minute.
|
|
90
|
+
assert.equal(register.size, 1);
|
|
91
|
+
assert.deepEqual(register.union(0), [{ byteStart: 100, byteEnd: 199 }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a withdrawal names its own claimant and nothing else", () => {
|
|
95
|
+
const register = new DemandRegister();
|
|
96
|
+
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
97
|
+
register.state({ claimant: "audio", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.NEAR });
|
|
98
|
+
|
|
99
|
+
assert.equal(register.withdraw("audio"), true);
|
|
100
|
+
assert.equal(register.withdraw("audio"), false, "a second release withdraws nothing");
|
|
101
|
+
assert.equal(register.withdraw("nobody"), false, "a stray release matches nothing");
|
|
102
|
+
assert.equal(register.size, 1, "and video still wants what it wanted");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the speculative levels are stated only while nothing urgent is missing", () => {
|
|
106
|
+
const register = new DemandRegister();
|
|
107
|
+
register.state({ claimant: "video", fileIndex: 0, byteStart: 0, byteEnd: 99, urgency: Urgency.BLOCKED });
|
|
108
|
+
register.state({ claimant: "fill", fileIndex: 0, byteStart: 1000, byteEnd: 9999, urgency: Urgency.TAIL });
|
|
109
|
+
register.state({ claimant: "back", fileIndex: 0, byteStart: 0, byteEnd: 999, urgency: Urgency.BEHIND });
|
|
110
|
+
|
|
111
|
+
// Something urgent is missing: the speculative levels are not stated at all.
|
|
112
|
+
// Withdrawn, not lowered — a withdrawn window is not in the download set, so
|
|
113
|
+
// a peer with nothing urgent to give cannot fall through to it and spend the
|
|
114
|
+
// shared link on a piece nobody is waiting for.
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
register.levelsToState((window) => window.urgency !== Urgency.BLOCKED),
|
|
117
|
+
[Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Everything urgent has arrived: they are stated, in order.
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
register.levelsToState(() => true),
|
|
123
|
+
[Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD, Urgency.TAIL, Urgency.BEHIND]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// The tail is complete but the gap behind is not: the gap stays stated and
|
|
127
|
+
// nothing below it exists to withhold.
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
register.levelsToState((window) => window.urgency !== Urgency.BEHIND),
|
|
130
|
+
[Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD, Urgency.TAIL, Urgency.BEHIND]
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("only the level being waited on may take a block from a slow peer", () => {
|
|
135
|
+
assert.equal(mayDisplaceSlowPeer(Urgency.BLOCKED), true);
|
|
136
|
+
for (const urgency of [Urgency.NEAR, Urgency.AHEAD, Urgency.TAIL, Urgency.BEHIND]) {
|
|
137
|
+
assert.equal(mayDisplaceSlowPeer(urgency), false);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the library is given the only distinction it keeps", () => {
|
|
142
|
+
// Measured against the vendored 2.8.5: distinct non-zero priorities order the
|
|
143
|
+
// selection list once and are round-robined afterwards by `shufflePriority`.
|
|
144
|
+
// Zero is the only value that stays put, always last.
|
|
145
|
+
assert.equal(selectionPriority(Urgency.BLOCKED), 1);
|
|
146
|
+
assert.equal(selectionPriority(Urgency.NEAR), 1);
|
|
147
|
+
assert.equal(selectionPriority(Urgency.AHEAD), 1);
|
|
148
|
+
assert.equal(selectionPriority(Urgency.TAIL), 0);
|
|
149
|
+
assert.equal(selectionPriority(Urgency.BEHIND), 0);
|
|
150
|
+
|
|
151
|
+
assert.equal(isConditional(Urgency.AHEAD), false);
|
|
152
|
+
assert.equal(isConditional(Urgency.TAIL), true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("bytes become pieces in one place, and a partial piece is a whole piece", () => {
|
|
156
|
+
const pieceLength = 4 * MEGABYTE;
|
|
157
|
+
// A file starting one piece into the torrent, wanting its first byte.
|
|
158
|
+
assert.deepEqual(
|
|
159
|
+
piecesOf({ fileOffset: pieceLength, byteStart: 0, byteEnd: 0, pieceLength }),
|
|
160
|
+
{ from: 1, to: 1 }
|
|
161
|
+
);
|
|
162
|
+
// A range ending one byte into the next piece still needs that whole piece:
|
|
163
|
+
// the protocol delivers and verifies nothing smaller.
|
|
164
|
+
assert.deepEqual(
|
|
165
|
+
piecesOf({ fileOffset: 0, byteStart: 0, byteEnd: pieceLength, pieceLength }),
|
|
166
|
+
{ from: 0, to: 1 }
|
|
167
|
+
);
|
|
168
|
+
assert.equal(piecesOf({ fileOffset: 0, byteStart: 10, byteEnd: 5, pieceLength }), null);
|
|
169
|
+
assert.equal(piecesOf({ fileOffset: 0, byteStart: 0, byteEnd: 1, pieceLength: 0 }), null);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a budget buys whole places, and a window needs one more than it measures", () => {
|
|
173
|
+
const pieceLength = 16 * MEGABYTE;
|
|
174
|
+
// The failure this rounding hid: 64 MB of allowance is four places, and two
|
|
175
|
+
// readers asking for 96 MB each want six. Counted in bytes the shortage is
|
|
176
|
+
// plain; counted in floored pieces it is not.
|
|
177
|
+
assert.equal(piecesWithin(64 * MEGABYTE, pieceLength), 4);
|
|
178
|
+
assert.equal(piecesNeededFor(96 * MEGABYTE, pieceLength), 7, "six, plus the one it can straddle");
|
|
179
|
+
assert.equal(piecesWithin(0, pieceLength), 0);
|
|
180
|
+
assert.equal(piecesNeededFor(0, pieceLength), 0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("the gap behind the playhead is stated nearest first", () => {
|
|
184
|
+
// WebTorrent walks a selection from its start upwards, so one claim over
|
|
185
|
+
// everything behind the viewer would be fetched from the beginning of the
|
|
186
|
+
// file — the end furthest from where a backward seek lands.
|
|
187
|
+
const ranges = nearestFirst({ byteStart: 0, byteEnd: 999, parts: 4 });
|
|
188
|
+
assert.equal(ranges.length, 4);
|
|
189
|
+
assert.equal(ranges[0].byteEnd, 999, "the part nearest the playhead comes first");
|
|
190
|
+
assert.equal(ranges[ranges.length - 1].byteStart, 0, "and the furthest comes last");
|
|
191
|
+
for (const range of ranges) {
|
|
192
|
+
assert.ok(range.byteStart <= range.byteEnd);
|
|
193
|
+
}
|
|
194
|
+
assert.deepEqual(nearestFirst({ byteStart: 10, byteEnd: 5, parts: 2 }), []);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a byte is as urgent as the most urgent thing that wants it", () => {
|
|
198
|
+
const register = new DemandRegister();
|
|
199
|
+
register.state({ claimant: "map:tail", fileIndex: 0, byteStart: 0, byteEnd: 999, urgency: Urgency.TAIL });
|
|
200
|
+
register.state({ claimant: "map:near", fileIndex: 0, byteStart: 400, byteEnd: 599, urgency: Urgency.NEAR });
|
|
201
|
+
|
|
202
|
+
assert.equal(register.urgencyAt(0, 100), Urgency.TAIL);
|
|
203
|
+
assert.equal(register.urgencyAt(0, 500), Urgency.NEAR, "the overlap took the less urgent of the two");
|
|
204
|
+
assert.equal(register.urgencyAt(0, 5000), null, "a byte nobody wants reported a level anyway");
|
|
205
|
+
assert.equal(register.urgencyAt(1, 500), null, "byte 500 of another file is not this byte");
|
|
206
|
+
});
|
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The DHT's entry points are given as addresses, not names.
|
|
3
|
-
*
|
|
4
|
-
* Measured 2026-08-21 on the addon host. Two of the three bootstrap nodes the
|
|
5
|
-
* library ships answer nothing: `router.bittorrent.com` and `router.utorrent.com`
|
|
6
|
-
* did not reply to a hand-written `ping` at all, while a control datagram to a
|
|
7
|
-
* DNS server came back in 20 ms. The third, `dht.transmissionbt.com`, is alive —
|
|
8
|
-
* it answered `find_node` with eight nodes — but on a host with global IPv6 its
|
|
9
|
-
* name resolves to an IPv6 address first, and the DHT's socket is IPv4, so by
|
|
10
|
-
* name it was never reached. By name: 0 nodes after 21 s, every run. By address:
|
|
11
|
-
* 22 nodes in 5 s.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
|
|
17
|
-
import { dhtNodeCount, parseBootstrapEntry, resolveDhtBootstrap } from "../services/torrent/torrent-pool.js";
|
|
18
|
-
|
|
19
|
-
test("a bootstrap entry is split into host and port", () => {
|
|
20
|
-
assert.deepEqual(parseBootstrapEntry("dht.libtorrent.org:25401"), {
|
|
21
|
-
host: "dht.libtorrent.org",
|
|
22
|
-
port: 25401
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("an entry with no port gets the DHT's own", () => {
|
|
27
|
-
assert.deepEqual(parseBootstrapEntry("router.bittorrent.com"), {
|
|
28
|
-
host: "router.bittorrent.com",
|
|
29
|
-
port: 6881
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("an unusable entry is dropped rather than breaking the client", () => {
|
|
34
|
-
// One bad line in the list must cost that node and nothing else — the DHT is
|
|
35
|
-
// best-effort by nature and a typo here would otherwise take the whole client
|
|
36
|
-
// down at construction.
|
|
37
|
-
assert.equal(parseBootstrapEntry(""), null);
|
|
38
|
-
assert.equal(parseBootstrapEntry(" "), null);
|
|
39
|
-
assert.equal(parseBootstrapEntry("host:0"), null);
|
|
40
|
-
assert.equal(parseBootstrapEntry("host:70000"), null);
|
|
41
|
-
assert.equal(parseBootstrapEntry("host:not-a-port"), null);
|
|
42
|
-
assert.equal(parseBootstrapEntry(":6881"), null);
|
|
43
|
-
assert.equal(parseBootstrapEntry(null), null);
|
|
44
|
-
assert.equal(parseBootstrapEntry(42), null);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("the routing table's size is readable, and its absence is not an error", () => {
|
|
48
|
-
assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => [1, 2, 3] } } }), 3);
|
|
49
|
-
assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => [] } } }), 0);
|
|
50
|
-
// A client built without a DHT, or one whose internals moved: the difference
|
|
51
|
-
// between "no DHT" and "an empty DHT" is the whole point of the report, so
|
|
52
|
-
// the first must not be printed as the second.
|
|
53
|
-
assert.equal(dhtNodeCount({}), null);
|
|
54
|
-
assert.equal(dhtNodeCount({ dht: {} }), null);
|
|
55
|
-
assert.equal(dhtNodeCount(null), null);
|
|
56
|
-
assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => { throw new Error("gone"); } } } }), null);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("a name that will not resolve inside the cap is dropped, not waited on", async () => {
|
|
60
|
-
// The whole call is awaited before the torrent client exists, so a resolver
|
|
61
|
-
// that black-holes must cost the cap and not c-ares' own four tries.
|
|
62
|
-
const started = Date.now();
|
|
63
|
-
// `.invalid` is reserved by RFC 2606 and never resolves; the cap is what
|
|
64
|
-
// bounds the wait when a resolver answers slowly rather than quickly.
|
|
65
|
-
const resolved = await resolveDhtBootstrap(["nothing.invalid:6881"], 200);
|
|
66
|
-
assert.deepEqual(resolved, []);
|
|
67
|
-
assert.ok(Date.now() - started < 2000, "the cap, not the resolver, decided when to give up");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("an empty list is an answer, not a failure", async () => {
|
|
71
|
-
assert.deepEqual(await resolveDhtBootstrap([], 200), []);
|
|
72
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The DHT's entry points are given as addresses, not names.
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-08-21 on the addon host. Two of the three bootstrap nodes the
|
|
5
|
+
* library ships answer nothing: `router.bittorrent.com` and `router.utorrent.com`
|
|
6
|
+
* did not reply to a hand-written `ping` at all, while a control datagram to a
|
|
7
|
+
* DNS server came back in 20 ms. The third, `dht.transmissionbt.com`, is alive —
|
|
8
|
+
* it answered `find_node` with eight nodes — but on a host with global IPv6 its
|
|
9
|
+
* name resolves to an IPv6 address first, and the DHT's socket is IPv4, so by
|
|
10
|
+
* name it was never reached. By name: 0 nodes after 21 s, every run. By address:
|
|
11
|
+
* 22 nodes in 5 s.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
|
|
17
|
+
import { dhtNodeCount, parseBootstrapEntry, resolveDhtBootstrap } from "../services/torrent/torrent-pool.js";
|
|
18
|
+
|
|
19
|
+
test("a bootstrap entry is split into host and port", () => {
|
|
20
|
+
assert.deepEqual(parseBootstrapEntry("dht.libtorrent.org:25401"), {
|
|
21
|
+
host: "dht.libtorrent.org",
|
|
22
|
+
port: 25401
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("an entry with no port gets the DHT's own", () => {
|
|
27
|
+
assert.deepEqual(parseBootstrapEntry("router.bittorrent.com"), {
|
|
28
|
+
host: "router.bittorrent.com",
|
|
29
|
+
port: 6881
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("an unusable entry is dropped rather than breaking the client", () => {
|
|
34
|
+
// One bad line in the list must cost that node and nothing else — the DHT is
|
|
35
|
+
// best-effort by nature and a typo here would otherwise take the whole client
|
|
36
|
+
// down at construction.
|
|
37
|
+
assert.equal(parseBootstrapEntry(""), null);
|
|
38
|
+
assert.equal(parseBootstrapEntry(" "), null);
|
|
39
|
+
assert.equal(parseBootstrapEntry("host:0"), null);
|
|
40
|
+
assert.equal(parseBootstrapEntry("host:70000"), null);
|
|
41
|
+
assert.equal(parseBootstrapEntry("host:not-a-port"), null);
|
|
42
|
+
assert.equal(parseBootstrapEntry(":6881"), null);
|
|
43
|
+
assert.equal(parseBootstrapEntry(null), null);
|
|
44
|
+
assert.equal(parseBootstrapEntry(42), null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("the routing table's size is readable, and its absence is not an error", () => {
|
|
48
|
+
assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => [1, 2, 3] } } }), 3);
|
|
49
|
+
assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => [] } } }), 0);
|
|
50
|
+
// A client built without a DHT, or one whose internals moved: the difference
|
|
51
|
+
// between "no DHT" and "an empty DHT" is the whole point of the report, so
|
|
52
|
+
// the first must not be printed as the second.
|
|
53
|
+
assert.equal(dhtNodeCount({}), null);
|
|
54
|
+
assert.equal(dhtNodeCount({ dht: {} }), null);
|
|
55
|
+
assert.equal(dhtNodeCount(null), null);
|
|
56
|
+
assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => { throw new Error("gone"); } } } }), null);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a name that will not resolve inside the cap is dropped, not waited on", async () => {
|
|
60
|
+
// The whole call is awaited before the torrent client exists, so a resolver
|
|
61
|
+
// that black-holes must cost the cap and not c-ares' own four tries.
|
|
62
|
+
const started = Date.now();
|
|
63
|
+
// `.invalid` is reserved by RFC 2606 and never resolves; the cap is what
|
|
64
|
+
// bounds the wait when a resolver answers slowly rather than quickly.
|
|
65
|
+
const resolved = await resolveDhtBootstrap(["nothing.invalid:6881"], 200);
|
|
66
|
+
assert.deepEqual(resolved, []);
|
|
67
|
+
assert.ok(Date.now() - started < 2000, "the cap, not the resolver, decided when to give up");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("an empty list is an answer, not a failure", async () => {
|
|
71
|
+
assert.deepEqual(await resolveDhtBootstrap([], 200), []);
|
|
72
|
+
});
|