@torrent-tv/proxy 2.88.2 → 2.88.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 +2259 -2246
- 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 +23 -4
- 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 +1 -1
- 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/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- 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 -217
- 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 +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- 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 +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -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 +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- 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 +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- 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 +418 -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 +1150 -1141
- 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 +349 -254
- package/services/encode/quality/output-format.js +319 -315
- 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 +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +21 -6
- 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 +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -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 +1344 -1244
- 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 +931 -931
- package/services/media/tracks/AudioTrack.js +143 -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/audio-bound.js +233 -0
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +13 -8
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- 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 +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- 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/episode-naming.js +182 -182
- 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 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- 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 +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- 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 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- 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 +691 -691
- 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 +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -0
- package/test/audio-inventory.test.js +212 -212
- 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/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- 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 +162 -162
- 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/episode-naming.test.js +131 -131
- 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/fixtures/audio-frame-boundary.json +1 -0
- package/test/fmp4-presentation-clock.test.js +147 -147
- 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 +112 -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 +270 -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 +401 -401
- 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 +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- 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/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- 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 +1257 -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 +105 -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/soundtrack-rate-bound.test.js +289 -0
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- 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 +209 -209
- 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 +504 -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
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Turning a byte range into positions in shared memory.
|
|
3
|
-
*
|
|
4
|
-
* This arithmetic fails silently when it is wrong: the response comes back the
|
|
5
|
-
* right length and full of the wrong bytes. Piece numbers are torrent-wide
|
|
6
|
-
* while a read is expressed in file coordinates, and the first and last pieces
|
|
7
|
-
* of a range are almost always partial — so every case here is a boundary.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { EventEmitter } from "node:events";
|
|
13
|
-
import { readFragments } from "../services/torrent/worker/piece-reader.js";
|
|
14
|
-
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import fs from "node:fs/promises";
|
|
18
|
-
|
|
19
|
-
const PIECE = 1024;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A torrent whose pieces are all present, backed by a real store so that
|
|
23
|
-
* `locate`/`reside`/`pin` behave as they do in production.
|
|
24
|
-
*
|
|
25
|
-
* @param {{ fileOffset: number, fileLength: number, totalLength: number }} shape
|
|
26
|
-
*/
|
|
27
|
-
async function fakeTorrent({ fileOffset, fileLength, totalLength }) {
|
|
28
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "piece-reader-test-"));
|
|
29
|
-
const store = new SharedPieceStore(PIECE, {
|
|
30
|
-
length: totalLength,
|
|
31
|
-
memoryBytes: 64 * PIECE,
|
|
32
|
-
path: directory,
|
|
33
|
-
name: "test"
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const pieceCount = Math.ceil(totalLength / PIECE);
|
|
37
|
-
for (let index = 0; index < pieceCount; index += 1) {
|
|
38
|
-
const length = index === pieceCount - 1 ? totalLength - index * PIECE : PIECE;
|
|
39
|
-
const piece = Buffer.alloc(length);
|
|
40
|
-
// Each byte encodes its own absolute position, so a misplaced offset is
|
|
41
|
-
// visible in the value itself rather than only in the length.
|
|
42
|
-
for (let at = 0; at < length; at += 1) {
|
|
43
|
-
piece[at] = (index * PIECE + at) % 251;
|
|
44
|
-
}
|
|
45
|
-
await new Promise((resolve, reject) => {
|
|
46
|
-
store.put(index, piece, (error) => (error ? reject(error) : resolve()));
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const torrent = Object.assign(new EventEmitter(), {
|
|
51
|
-
pieceLength: PIECE,
|
|
52
|
-
store,
|
|
53
|
-
bitfield: { get: () => true },
|
|
54
|
-
files: [{ offset: fileOffset, length: fileLength, name: "file.bin" }],
|
|
55
|
-
select() {},
|
|
56
|
-
critical() {}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return { torrent, store, directory };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Collect a range through the reader, as the worker does. */
|
|
63
|
-
async function readRange(torrent, start, end) {
|
|
64
|
-
const collected = [];
|
|
65
|
-
const positions = [];
|
|
66
|
-
for await (const fragment of readFragments({
|
|
67
|
-
torrent,
|
|
68
|
-
fileIndex: 0,
|
|
69
|
-
start,
|
|
70
|
-
end,
|
|
71
|
-
cancellation: { isCancelled: () => false }
|
|
72
|
-
})) {
|
|
73
|
-
const source = fragment.buffer ? Buffer.from(fragment.buffer, fragment.offset, fragment.length) : Buffer.alloc(0);
|
|
74
|
-
collected.push(Buffer.from(source));
|
|
75
|
-
positions.push({ piece: fragment.pieceIndex, length: fragment.length });
|
|
76
|
-
fragment.release();
|
|
77
|
-
}
|
|
78
|
-
return { bytes: Buffer.concat(collected), positions };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** What the bytes at an absolute torrent offset should be. */
|
|
82
|
-
function expectedBytes(absoluteStart, length) {
|
|
83
|
-
const expected = Buffer.alloc(length);
|
|
84
|
-
for (let at = 0; at < length; at += 1) {
|
|
85
|
-
expected[at] = (absoluteStart + at) % 251;
|
|
86
|
-
}
|
|
87
|
-
return expected;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
test("a range inside one piece is read from that piece only", async () => {
|
|
91
|
-
const { torrent, store, directory } = await fakeTorrent({
|
|
92
|
-
fileOffset: 0,
|
|
93
|
-
fileLength: 4 * PIECE,
|
|
94
|
-
totalLength: 4 * PIECE
|
|
95
|
-
});
|
|
96
|
-
try {
|
|
97
|
-
const { bytes, positions } = await readRange(torrent, 100, 199);
|
|
98
|
-
assert.equal(positions.length, 1);
|
|
99
|
-
assert.deepEqual(bytes, expectedBytes(100, 100));
|
|
100
|
-
} finally {
|
|
101
|
-
store.destroy(() => undefined);
|
|
102
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a range spanning pieces reassembles in order", async () => {
|
|
107
|
-
const { torrent, store, directory } = await fakeTorrent({
|
|
108
|
-
fileOffset: 0,
|
|
109
|
-
fileLength: 4 * PIECE,
|
|
110
|
-
totalLength: 4 * PIECE
|
|
111
|
-
});
|
|
112
|
-
try {
|
|
113
|
-
const start = PIECE - 10;
|
|
114
|
-
const end = 2 * PIECE + 9;
|
|
115
|
-
const { bytes, positions } = await readRange(torrent, start, end);
|
|
116
|
-
assert.deepEqual(positions.map((entry) => entry.piece), [0, 1, 2]);
|
|
117
|
-
assert.deepEqual(bytes, expectedBytes(start, end - start + 1));
|
|
118
|
-
} finally {
|
|
119
|
-
store.destroy(() => undefined);
|
|
120
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("a file that does not start at a piece boundary is still read correctly", async () => {
|
|
125
|
-
// The usual case in a multi-file torrent, and the one where using file
|
|
126
|
-
// offsets as if they were torrent offsets returns the wrong bytes at the
|
|
127
|
-
// right length.
|
|
128
|
-
const fileOffset = PIECE + 300;
|
|
129
|
-
const { torrent, store, directory } = await fakeTorrent({
|
|
130
|
-
fileOffset,
|
|
131
|
-
fileLength: 2 * PIECE,
|
|
132
|
-
totalLength: 5 * PIECE
|
|
133
|
-
});
|
|
134
|
-
try {
|
|
135
|
-
const { bytes } = await readRange(torrent, 0, 1499);
|
|
136
|
-
assert.deepEqual(bytes, expectedBytes(fileOffset, 1500));
|
|
137
|
-
} finally {
|
|
138
|
-
store.destroy(() => undefined);
|
|
139
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("every fragment releases its pin, so nothing stays held", async () => {
|
|
144
|
-
const { torrent, store, directory } = await fakeTorrent({
|
|
145
|
-
fileOffset: 0,
|
|
146
|
-
fileLength: 4 * PIECE,
|
|
147
|
-
totalLength: 4 * PIECE
|
|
148
|
-
});
|
|
149
|
-
try {
|
|
150
|
-
await readRange(torrent, 0, 4 * PIECE - 1);
|
|
151
|
-
// With every piece unpinned the store can still make room; if a pin leaked
|
|
152
|
-
// it would eventually refuse.
|
|
153
|
-
const before = store.stats().blockedByPins;
|
|
154
|
-
for (let index = 0; index < 200; index += 1) {
|
|
155
|
-
await store.reside(index % 4);
|
|
156
|
-
}
|
|
157
|
-
assert.equal(store.stats().blockedByPins, before, "a pin was left behind");
|
|
158
|
-
} finally {
|
|
159
|
-
store.destroy(() => undefined);
|
|
160
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
161
|
-
}
|
|
162
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Turning a byte range into positions in shared memory.
|
|
3
|
+
*
|
|
4
|
+
* This arithmetic fails silently when it is wrong: the response comes back the
|
|
5
|
+
* right length and full of the wrong bytes. Piece numbers are torrent-wide
|
|
6
|
+
* while a read is expressed in file coordinates, and the first and last pieces
|
|
7
|
+
* of a range are almost always partial — so every case here is a boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
import { readFragments } from "../services/torrent/worker/piece-reader.js";
|
|
14
|
+
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import fs from "node:fs/promises";
|
|
18
|
+
|
|
19
|
+
const PIECE = 1024;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A torrent whose pieces are all present, backed by a real store so that
|
|
23
|
+
* `locate`/`reside`/`pin` behave as they do in production.
|
|
24
|
+
*
|
|
25
|
+
* @param {{ fileOffset: number, fileLength: number, totalLength: number }} shape
|
|
26
|
+
*/
|
|
27
|
+
async function fakeTorrent({ fileOffset, fileLength, totalLength }) {
|
|
28
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "piece-reader-test-"));
|
|
29
|
+
const store = new SharedPieceStore(PIECE, {
|
|
30
|
+
length: totalLength,
|
|
31
|
+
memoryBytes: 64 * PIECE,
|
|
32
|
+
path: directory,
|
|
33
|
+
name: "test"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const pieceCount = Math.ceil(totalLength / PIECE);
|
|
37
|
+
for (let index = 0; index < pieceCount; index += 1) {
|
|
38
|
+
const length = index === pieceCount - 1 ? totalLength - index * PIECE : PIECE;
|
|
39
|
+
const piece = Buffer.alloc(length);
|
|
40
|
+
// Each byte encodes its own absolute position, so a misplaced offset is
|
|
41
|
+
// visible in the value itself rather than only in the length.
|
|
42
|
+
for (let at = 0; at < length; at += 1) {
|
|
43
|
+
piece[at] = (index * PIECE + at) % 251;
|
|
44
|
+
}
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
store.put(index, piece, (error) => (error ? reject(error) : resolve()));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const torrent = Object.assign(new EventEmitter(), {
|
|
51
|
+
pieceLength: PIECE,
|
|
52
|
+
store,
|
|
53
|
+
bitfield: { get: () => true },
|
|
54
|
+
files: [{ offset: fileOffset, length: fileLength, name: "file.bin" }],
|
|
55
|
+
select() {},
|
|
56
|
+
critical() {}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return { torrent, store, directory };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Collect a range through the reader, as the worker does. */
|
|
63
|
+
async function readRange(torrent, start, end) {
|
|
64
|
+
const collected = [];
|
|
65
|
+
const positions = [];
|
|
66
|
+
for await (const fragment of readFragments({
|
|
67
|
+
torrent,
|
|
68
|
+
fileIndex: 0,
|
|
69
|
+
start,
|
|
70
|
+
end,
|
|
71
|
+
cancellation: { isCancelled: () => false }
|
|
72
|
+
})) {
|
|
73
|
+
const source = fragment.buffer ? Buffer.from(fragment.buffer, fragment.offset, fragment.length) : Buffer.alloc(0);
|
|
74
|
+
collected.push(Buffer.from(source));
|
|
75
|
+
positions.push({ piece: fragment.pieceIndex, length: fragment.length });
|
|
76
|
+
fragment.release();
|
|
77
|
+
}
|
|
78
|
+
return { bytes: Buffer.concat(collected), positions };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What the bytes at an absolute torrent offset should be. */
|
|
82
|
+
function expectedBytes(absoluteStart, length) {
|
|
83
|
+
const expected = Buffer.alloc(length);
|
|
84
|
+
for (let at = 0; at < length; at += 1) {
|
|
85
|
+
expected[at] = (absoluteStart + at) % 251;
|
|
86
|
+
}
|
|
87
|
+
return expected;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test("a range inside one piece is read from that piece only", async () => {
|
|
91
|
+
const { torrent, store, directory } = await fakeTorrent({
|
|
92
|
+
fileOffset: 0,
|
|
93
|
+
fileLength: 4 * PIECE,
|
|
94
|
+
totalLength: 4 * PIECE
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
const { bytes, positions } = await readRange(torrent, 100, 199);
|
|
98
|
+
assert.equal(positions.length, 1);
|
|
99
|
+
assert.deepEqual(bytes, expectedBytes(100, 100));
|
|
100
|
+
} finally {
|
|
101
|
+
store.destroy(() => undefined);
|
|
102
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a range spanning pieces reassembles in order", async () => {
|
|
107
|
+
const { torrent, store, directory } = await fakeTorrent({
|
|
108
|
+
fileOffset: 0,
|
|
109
|
+
fileLength: 4 * PIECE,
|
|
110
|
+
totalLength: 4 * PIECE
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
const start = PIECE - 10;
|
|
114
|
+
const end = 2 * PIECE + 9;
|
|
115
|
+
const { bytes, positions } = await readRange(torrent, start, end);
|
|
116
|
+
assert.deepEqual(positions.map((entry) => entry.piece), [0, 1, 2]);
|
|
117
|
+
assert.deepEqual(bytes, expectedBytes(start, end - start + 1));
|
|
118
|
+
} finally {
|
|
119
|
+
store.destroy(() => undefined);
|
|
120
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a file that does not start at a piece boundary is still read correctly", async () => {
|
|
125
|
+
// The usual case in a multi-file torrent, and the one where using file
|
|
126
|
+
// offsets as if they were torrent offsets returns the wrong bytes at the
|
|
127
|
+
// right length.
|
|
128
|
+
const fileOffset = PIECE + 300;
|
|
129
|
+
const { torrent, store, directory } = await fakeTorrent({
|
|
130
|
+
fileOffset,
|
|
131
|
+
fileLength: 2 * PIECE,
|
|
132
|
+
totalLength: 5 * PIECE
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const { bytes } = await readRange(torrent, 0, 1499);
|
|
136
|
+
assert.deepEqual(bytes, expectedBytes(fileOffset, 1500));
|
|
137
|
+
} finally {
|
|
138
|
+
store.destroy(() => undefined);
|
|
139
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("every fragment releases its pin, so nothing stays held", async () => {
|
|
144
|
+
const { torrent, store, directory } = await fakeTorrent({
|
|
145
|
+
fileOffset: 0,
|
|
146
|
+
fileLength: 4 * PIECE,
|
|
147
|
+
totalLength: 4 * PIECE
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
await readRange(torrent, 0, 4 * PIECE - 1);
|
|
151
|
+
// With every piece unpinned the store can still make room; if a pin leaked
|
|
152
|
+
// it would eventually refuse.
|
|
153
|
+
const before = store.stats().blockedByPins;
|
|
154
|
+
for (let index = 0; index < 200; index += 1) {
|
|
155
|
+
await store.reside(index % 4);
|
|
156
|
+
}
|
|
157
|
+
assert.equal(store.stats().blockedByPins, before, "a pin was left behind");
|
|
158
|
+
} finally {
|
|
159
|
+
store.destroy(() => undefined);
|
|
160
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What WebTorrent's own selection bookkeeping does when a seek demotes
|
|
3
|
-
* the pieces behind the playhead.
|
|
4
|
-
*
|
|
5
|
-
* This is the library's behaviour, not ours, and `prioritizeByteRange` depends
|
|
6
|
-
* on it: a selection carries an `offset` — how many pieces from its start are
|
|
7
|
-
* already downloaded — and the picker scans from `from + offset`
|
|
8
|
-
* (`torrent.js`, `for (piece = next.from + next.offset; piece <= next.to; …)`).
|
|
9
|
-
* `deselect` subtracts an interval and copies that offset into what survives,
|
|
10
|
-
* so the remaining selection can end up scanning past its own end and yield
|
|
11
|
-
* nothing at all.
|
|
12
|
-
*
|
|
13
|
-
* Measured consequence before the fix: a seek to 89.1% of a 4.7 GB film left
|
|
14
|
-
* the seek target wanted by nobody, a later range-less read re-selected the
|
|
15
|
-
* whole file, and the swarm walked it from the first missing piece — 2.47 GB
|
|
16
|
-
* over 93 s before the segment could be served.
|
|
17
|
-
*
|
|
18
|
-
* If a WebTorrent upgrade changes any of this, these tests fail rather than the
|
|
19
|
-
* behaviour silently regressing.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import test from "node:test";
|
|
23
|
-
import assert from "node:assert/strict";
|
|
24
|
-
import { Selections } from "webtorrent/lib/selections.js";
|
|
25
|
-
|
|
26
|
-
// The numbers are the measured session: 588 pieces of 8 MiB, sequential
|
|
27
|
-
// download had reached 38.4% (piece 226), the viewer seeked to 89.1% (piece
|
|
28
|
-
// 523).
|
|
29
|
-
const LAST_PIECE = 587;
|
|
30
|
-
const DOWNLOADED_TO = 226;
|
|
31
|
-
const PLAYHEAD = 523;
|
|
32
|
-
|
|
33
|
-
/** Where the picker would start scanning this selection. */
|
|
34
|
-
const scanStart = (selection) => selection.from + selection.offset;
|
|
35
|
-
|
|
36
|
-
test("deselecting the gap behind the playhead leaves a selection that yields nothing", () => {
|
|
37
|
-
const selections = new Selections();
|
|
38
|
-
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
39
|
-
// What `_gcSelections` does as pieces arrive.
|
|
40
|
-
selections.get(0).offset = DOWNLOADED_TO;
|
|
41
|
-
|
|
42
|
-
selections.remove({ from: 0, to: PLAYHEAD - 1, isStreamSelection: false });
|
|
43
|
-
|
|
44
|
-
assert.equal(selections.length, 1);
|
|
45
|
-
const survivor = selections.get(0);
|
|
46
|
-
assert.equal(survivor.from, PLAYHEAD, "the surviving selection starts at the playhead");
|
|
47
|
-
assert.equal(survivor.offset, DOWNLOADED_TO, "and it kept the offset of the range it came from");
|
|
48
|
-
assert.ok(
|
|
49
|
-
scanStart(survivor) > survivor.to,
|
|
50
|
-
`scan would start at piece ${scanStart(survivor)} of ${survivor.to} — nothing is downloadable`
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("re-selecting the same range restores a scan that starts at the playhead", () => {
|
|
55
|
-
const selections = new Selections();
|
|
56
|
-
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
57
|
-
selections.get(0).offset = DOWNLOADED_TO;
|
|
58
|
-
|
|
59
|
-
// Exactly what prioritizeByteRange does on a forward seek.
|
|
60
|
-
selections.remove({ from: 0, to: PLAYHEAD - 1, isStreamSelection: false });
|
|
61
|
-
selections.insert({ from: PLAYHEAD, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
62
|
-
|
|
63
|
-
assert.equal(selections.length, 1, "the dead selection was replaced, not added to");
|
|
64
|
-
const selection = selections.get(0);
|
|
65
|
-
assert.equal(scanStart(selection), PLAYHEAD, "the picker now starts at the seek target");
|
|
66
|
-
assert.equal(selection.to, LAST_PIECE);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("selecting the whole file again undoes the demotion", () => {
|
|
70
|
-
const selections = new Selections();
|
|
71
|
-
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
72
|
-
selections.get(0).offset = DOWNLOADED_TO;
|
|
73
|
-
selections.remove({ from: 0, to: PLAYHEAD - 1, isStreamSelection: false });
|
|
74
|
-
selections.insert({ from: PLAYHEAD, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
75
|
-
|
|
76
|
-
// A range-less read reporting position 0 used to land here.
|
|
77
|
-
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
78
|
-
|
|
79
|
-
assert.equal(selections.length, 1);
|
|
80
|
-
assert.equal(
|
|
81
|
-
scanStart(selections.get(0)),
|
|
82
|
-
0,
|
|
83
|
-
"the whole file is selected again, so the picker falls back to the first missing piece"
|
|
84
|
-
);
|
|
85
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What WebTorrent's own selection bookkeeping does when a seek demotes
|
|
3
|
+
* the pieces behind the playhead.
|
|
4
|
+
*
|
|
5
|
+
* This is the library's behaviour, not ours, and `prioritizeByteRange` depends
|
|
6
|
+
* on it: a selection carries an `offset` — how many pieces from its start are
|
|
7
|
+
* already downloaded — and the picker scans from `from + offset`
|
|
8
|
+
* (`torrent.js`, `for (piece = next.from + next.offset; piece <= next.to; …)`).
|
|
9
|
+
* `deselect` subtracts an interval and copies that offset into what survives,
|
|
10
|
+
* so the remaining selection can end up scanning past its own end and yield
|
|
11
|
+
* nothing at all.
|
|
12
|
+
*
|
|
13
|
+
* Measured consequence before the fix: a seek to 89.1% of a 4.7 GB film left
|
|
14
|
+
* the seek target wanted by nobody, a later range-less read re-selected the
|
|
15
|
+
* whole file, and the swarm walked it from the first missing piece — 2.47 GB
|
|
16
|
+
* over 93 s before the segment could be served.
|
|
17
|
+
*
|
|
18
|
+
* If a WebTorrent upgrade changes any of this, these tests fail rather than the
|
|
19
|
+
* behaviour silently regressing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { Selections } from "webtorrent/lib/selections.js";
|
|
25
|
+
|
|
26
|
+
// The numbers are the measured session: 588 pieces of 8 MiB, sequential
|
|
27
|
+
// download had reached 38.4% (piece 226), the viewer seeked to 89.1% (piece
|
|
28
|
+
// 523).
|
|
29
|
+
const LAST_PIECE = 587;
|
|
30
|
+
const DOWNLOADED_TO = 226;
|
|
31
|
+
const PLAYHEAD = 523;
|
|
32
|
+
|
|
33
|
+
/** Where the picker would start scanning this selection. */
|
|
34
|
+
const scanStart = (selection) => selection.from + selection.offset;
|
|
35
|
+
|
|
36
|
+
test("deselecting the gap behind the playhead leaves a selection that yields nothing", () => {
|
|
37
|
+
const selections = new Selections();
|
|
38
|
+
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
39
|
+
// What `_gcSelections` does as pieces arrive.
|
|
40
|
+
selections.get(0).offset = DOWNLOADED_TO;
|
|
41
|
+
|
|
42
|
+
selections.remove({ from: 0, to: PLAYHEAD - 1, isStreamSelection: false });
|
|
43
|
+
|
|
44
|
+
assert.equal(selections.length, 1);
|
|
45
|
+
const survivor = selections.get(0);
|
|
46
|
+
assert.equal(survivor.from, PLAYHEAD, "the surviving selection starts at the playhead");
|
|
47
|
+
assert.equal(survivor.offset, DOWNLOADED_TO, "and it kept the offset of the range it came from");
|
|
48
|
+
assert.ok(
|
|
49
|
+
scanStart(survivor) > survivor.to,
|
|
50
|
+
`scan would start at piece ${scanStart(survivor)} of ${survivor.to} — nothing is downloadable`
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("re-selecting the same range restores a scan that starts at the playhead", () => {
|
|
55
|
+
const selections = new Selections();
|
|
56
|
+
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
57
|
+
selections.get(0).offset = DOWNLOADED_TO;
|
|
58
|
+
|
|
59
|
+
// Exactly what prioritizeByteRange does on a forward seek.
|
|
60
|
+
selections.remove({ from: 0, to: PLAYHEAD - 1, isStreamSelection: false });
|
|
61
|
+
selections.insert({ from: PLAYHEAD, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
62
|
+
|
|
63
|
+
assert.equal(selections.length, 1, "the dead selection was replaced, not added to");
|
|
64
|
+
const selection = selections.get(0);
|
|
65
|
+
assert.equal(scanStart(selection), PLAYHEAD, "the picker now starts at the seek target");
|
|
66
|
+
assert.equal(selection.to, LAST_PIECE);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("selecting the whole file again undoes the demotion", () => {
|
|
70
|
+
const selections = new Selections();
|
|
71
|
+
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
72
|
+
selections.get(0).offset = DOWNLOADED_TO;
|
|
73
|
+
selections.remove({ from: 0, to: PLAYHEAD - 1, isStreamSelection: false });
|
|
74
|
+
selections.insert({ from: PLAYHEAD, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
75
|
+
|
|
76
|
+
// A range-less read reporting position 0 used to land here.
|
|
77
|
+
selections.insert({ from: 0, to: LAST_PIECE, offset: 0, priority: 1 });
|
|
78
|
+
|
|
79
|
+
assert.equal(selections.length, 1);
|
|
80
|
+
assert.equal(
|
|
81
|
+
scanStart(selections.get(0)),
|
|
82
|
+
0,
|
|
83
|
+
"the whole file is selected again, so the picker falls back to the first missing piece"
|
|
84
|
+
);
|
|
85
|
+
});
|