@torrent-tv/proxy 2.87.10 → 2.87.12
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 +2191 -2181
- 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 +124 -101
- 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 -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 +136 -129
- package/routes/api/transcode-sessions/post.js +173 -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 +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -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 +758 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -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 +1141 -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 +315 -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 +245 -245
- 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 +931 -931
- 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 +727 -522
- 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 +620 -0
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -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 -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 +365 -365
- 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 +682 -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 +71 -71
- package/test/asset-paths.test.js +28 -28
- 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 +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/playback-readiness.test.js +323 -0
- 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/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 +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
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,386 +1,386 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The only thing in this proxy that tells WebTorrent what to fetch.
|
|
3
|
-
*
|
|
4
|
-
* One per torrent. It reads the demand register — the single statement of what
|
|
5
|
-
* anybody wants — and turns it into the library's `select`, `deselect` and
|
|
6
|
-
* `critical`. Nothing else calls those, so two parts of this program can no
|
|
7
|
-
* longer ask for different things and overwrite each other.
|
|
8
|
-
*
|
|
9
|
-
* That used to happen and it is written into the code this replaces. The reader
|
|
10
|
-
* held a moving window; the pool held a whole-file selection; a third place set
|
|
11
|
-
* a window around the read head. A whole-file read undid a seek that had just
|
|
12
|
-
* happened, and the swarm walked forward from the first hole: measured on a
|
|
13
|
-
* 4.7 GB film, a seek to 89.1 % fetched 2.47 GB over 93 s before the segment
|
|
14
|
-
* could be served.
|
|
15
|
-
*
|
|
16
|
-
* **Why urgency is not a number given to the library.** Measured against the
|
|
17
|
-
* vendored 2.8.5: selections are sorted by priority only when one is inserted,
|
|
18
|
-
* and `shufflePriority` then moves the selection just served to the back of the
|
|
19
|
-
* whole non-zero group. Distinct numbers therefore order the list once and
|
|
20
|
-
* round-robin it afterwards. So the ordering is kept HERE, by choosing what to
|
|
21
|
-
* state at all, and the library is given only the distinction it honours:
|
|
22
|
-
* non-zero for what is wanted now, zero for the speculative tail.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
isConditional,
|
|
27
|
-
piecesOf,
|
|
28
|
-
selectionPriority,
|
|
29
|
-
Urgency,
|
|
30
|
-
urgencyName
|
|
31
|
-
} from "../demand/index.js";
|
|
32
|
-
import { pieceStoreOf } from "../piece-store-of.js";
|
|
33
|
-
|
|
34
|
-
export class SwarmSelection {
|
|
35
|
-
#torrent;
|
|
36
|
-
#register;
|
|
37
|
-
/** What was last stated to the library, so a restatement can be a no-op. */
|
|
38
|
-
#stated = new Map();
|
|
39
|
-
/** Pieces this instance marked for displacement, so it clears only its own. */
|
|
40
|
-
#displacing = null;
|
|
41
|
-
/** Claimants whose windows are currently protected in memory. */
|
|
42
|
-
#protectedInMemory = new Set();
|
|
43
|
-
#findStore;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {object} params
|
|
47
|
-
* @param {import("webtorrent").Torrent} params.torrent
|
|
48
|
-
* @param {import("../demand/index.js").DemandRegister} params.register
|
|
49
|
-
* @param {(torrent: object) => object | null} [params.findStore] - How the
|
|
50
|
-
* piece store is reached. Injectable so a test can drive the memory
|
|
51
|
-
* projection without constructing a real store.
|
|
52
|
-
*/
|
|
53
|
-
constructor({ torrent, register, findStore = pieceStoreOf }) {
|
|
54
|
-
this.#torrent = torrent;
|
|
55
|
-
this.#register = register;
|
|
56
|
-
this.#findStore = findStore;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Bring the library's download set into line with what is stated.
|
|
61
|
-
*
|
|
62
|
-
* Called after any change to the register and on a timer. On a timer because
|
|
63
|
-
* WebTorrent DELETES a selection once every piece in it has arrived, so a
|
|
64
|
-
* window that is satisfied and then reopened — the reader moved on, or a
|
|
65
|
-
* piece was evicted and lost — is gone from the library while it is still
|
|
66
|
-
* stated here.
|
|
67
|
-
*
|
|
68
|
-
* @param {object} [options]
|
|
69
|
-
* @param {boolean} [options.speculativeAllowed] - Whether anything on ANY
|
|
70
|
-
* torrent is still waiting for something urgent. The registry works it out
|
|
71
|
-
* once and hands the same answer to every selection, because the link is
|
|
72
|
-
* shared and the question is not a per-torrent one.
|
|
73
|
-
* @returns {{ stated: number, withdrawn: number }}
|
|
74
|
-
*/
|
|
75
|
-
reconcile({ speculativeAllowed = true } = {}) {
|
|
76
|
-
const levels = this.#register.levelsToState(
|
|
77
|
-
(window) => this.#isSatisfied(window),
|
|
78
|
-
speculativeAllowed
|
|
79
|
-
);
|
|
80
|
-
/** @type {Map<string, { from: number, to: number, priority: number }>} */
|
|
81
|
-
const wanted = new Map();
|
|
82
|
-
for (const urgency of levels) {
|
|
83
|
-
for (const window of this.#register.at(urgency)) {
|
|
84
|
-
const range = this.#piecesFor(window);
|
|
85
|
-
if (!range) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
// Merged by range and priority, not by claimant: two readers wanting
|
|
89
|
-
// the same pieces are one instruction to the swarm.
|
|
90
|
-
const key = `${range.from}-${range.to}-${selectionPriority(urgency)}`;
|
|
91
|
-
wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(urgency) });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let withdrawn = 0;
|
|
96
|
-
for (const [key, range] of [...this.#stated]) {
|
|
97
|
-
if (wanted.has(key)) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
this.#deselect(range);
|
|
101
|
-
this.#stated.delete(key);
|
|
102
|
-
withdrawn += 1;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let stated = 0;
|
|
106
|
-
for (const [key, range] of wanted) {
|
|
107
|
-
// Re-stated when the library has dropped it, even though this instance
|
|
108
|
-
// believes it is stated: that is the whole reason this runs on a timer.
|
|
109
|
-
if (this.#stated.has(key) && this.#libraryHolds(range)) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
this.#select(range);
|
|
113
|
-
this.#stated.set(key, range);
|
|
114
|
-
stated += 1;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
this.#markDisplacement();
|
|
118
|
-
this.#projectIntoMemory();
|
|
119
|
-
return { stated, withdrawn };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Take everything back. The torrent is going, or nobody wants anything. */
|
|
123
|
-
releaseAll() {
|
|
124
|
-
for (const range of this.#stated.values()) {
|
|
125
|
-
this.#deselect(range);
|
|
126
|
-
}
|
|
127
|
-
this.#stated.clear();
|
|
128
|
-
this.#clearDisplacement();
|
|
129
|
-
const store = this.#findStore(this.#torrent);
|
|
130
|
-
for (const claimant of this.#protectedInMemory) {
|
|
131
|
-
store?.releaseProtection?.(claimant);
|
|
132
|
-
}
|
|
133
|
-
this.#protectedInMemory.clear();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* What is stated right now.
|
|
138
|
-
*
|
|
139
|
-
* @returns {Array<{ from: number, to: number, priority: number }>}
|
|
140
|
-
*/
|
|
141
|
-
statedRanges() {
|
|
142
|
-
return [...this.#stated.values()];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Whether anything urgent on THIS torrent has not arrived.
|
|
147
|
-
*
|
|
148
|
-
* Read by the registry, which asks every torrent and gives the same answer
|
|
149
|
-
* back to all of them.
|
|
150
|
-
*
|
|
151
|
-
* @returns {boolean}
|
|
152
|
-
*/
|
|
153
|
-
hasUrgentMissing() {
|
|
154
|
-
for (const urgency of [Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD]) {
|
|
155
|
-
for (const window of this.#register.at(urgency)) {
|
|
156
|
-
if (!this.#isSatisfied(window)) {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Permission to take a block from a slow peer, for the level being waited on
|
|
166
|
-
* and nothing else.
|
|
167
|
-
*
|
|
168
|
-
* Cleared before it is set again, because WebTorrent never clears the flag
|
|
169
|
-
* itself: a reader walking a film would otherwise leave every piece of it
|
|
170
|
-
* marked, and the mark would mean nothing anywhere.
|
|
171
|
-
*
|
|
172
|
-
* @returns {void}
|
|
173
|
-
*/
|
|
174
|
-
#markDisplacement() {
|
|
175
|
-
const blocked = this.#register
|
|
176
|
-
.at(Urgency.BLOCKED)
|
|
177
|
-
.map((window) => this.#piecesFor(window))
|
|
178
|
-
.filter((range) => range !== null);
|
|
179
|
-
if (blocked.length === 0) {
|
|
180
|
-
this.#clearDisplacement();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const nearest = Math.min(...blocked.map((range) => range.from));
|
|
184
|
-
const furthest = Math.max(...blocked.map((range) => range.to));
|
|
185
|
-
if (this.#displacing && this.#displacing.from === nearest && this.#displacing.to === furthest) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
this.#clearDisplacement();
|
|
189
|
-
try {
|
|
190
|
-
this.#torrent.critical?.(nearest, furthest);
|
|
191
|
-
this.#displacing = { from: nearest, to: furthest };
|
|
192
|
-
} catch {
|
|
193
|
-
// silent-ok: displacement is an optimisation, and a torrent being torn
|
|
194
|
-
// down is not worth failing a read over.
|
|
195
|
-
this.#displacing = null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Tell the piece store which bytes will be read soon, from the same stated
|
|
201
|
-
* needs the swarm is told about.
|
|
202
|
-
*
|
|
203
|
-
* The second half of stating a need once. Until 2026-09-02 a reader said the
|
|
204
|
-
* same thing twice — `protectRange` to the store for memory and a selection
|
|
205
|
-
* to the torrent for download — and a third piece of code read the first to
|
|
206
|
-
* rebuild the second. Now there is one statement and two views of it, both
|
|
207
|
-
* computed here.
|
|
208
|
-
*
|
|
209
|
-
* WHAT WILL BE READ SOON, WHICH IS NOT WHAT WILL BE DOWNLOADED SOON. Memory
|
|
210
|
-
* holds the first; the swarm is told the second; and the priority map states
|
|
211
|
-
* the second, over the whole rest of the film.
|
|
212
|
-
*
|
|
213
|
-
* `AHEAD` used to reach memory, and it is exactly the speculative lead: the
|
|
214
|
-
* map states one claimant per zone, so on a film with seven zones one `NEAR`
|
|
215
|
-
* and four `AHEAD` arrived here as five separate holders, each covering tens
|
|
216
|
-
* of megabytes. Field 2026-09-08: the store reported `5 reader(s) want 24
|
|
217
|
-
* piece(s) of 25 the store may hold (widest window 17)` — the union of what
|
|
218
|
-
* was declared equalled the whole capacity, so every admission had to evict a
|
|
219
|
-
* piece somebody had declared, and 100 of 1395 evictions did. Beside that,
|
|
220
|
-
* 6565 spills and 7575 revivals in 44 minutes with a median 0.0 s on disk, and
|
|
221
|
-
* 2138 h264 parse errors on a picture that was being COPIED.
|
|
222
|
-
*
|
|
223
|
-
* Raising the allowance does not touch it: a lead stated over the rest of the
|
|
224
|
-
* film grows to fill whatever memory it is given, and the ratio is unchanged.
|
|
225
|
-
* What belongs in memory is what a READ is stopped on and the little in front
|
|
226
|
-
* of it — the levels a read itself states — and those are `BLOCKED` and
|
|
227
|
-
* `NEAR`.
|
|
228
|
-
*
|
|
229
|
-
* @returns {void}
|
|
230
|
-
*/
|
|
231
|
-
#projectIntoMemory() {
|
|
232
|
-
const store = this.#findStore(this.#torrent);
|
|
233
|
-
if (!store || typeof store.protectRange !== "function") {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const holding = new Set();
|
|
237
|
-
for (const urgency of [Urgency.BLOCKED, Urgency.NEAR]) {
|
|
238
|
-
for (const window of this.#register.at(urgency)) {
|
|
239
|
-
const range = this.#piecesFor(window);
|
|
240
|
-
if (!range) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
// The level goes with the range: it is what eviction compares when
|
|
244
|
-
// everything resident is wanted by somebody, and dropping it here is
|
|
245
|
-
// what left the store choosing by recency alone.
|
|
246
|
-
store.protectRange(window.claimant, range.from, range.to, window.urgency);
|
|
247
|
-
holding.add(window.claimant);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
for (const claimant of this.#protectedInMemory) {
|
|
251
|
-
if (!holding.has(claimant)) {
|
|
252
|
-
store.releaseProtection?.(claimant);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
this.#protectedInMemory = holding;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** @returns {void} */
|
|
259
|
-
#clearDisplacement() {
|
|
260
|
-
if (!this.#displacing || !Array.isArray(this.#torrent._critical)) {
|
|
261
|
-
this.#displacing = null;
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
for (let index = this.#displacing.from; index <= this.#displacing.to; index += 1) {
|
|
265
|
-
this.#torrent._critical[index] = false;
|
|
266
|
-
}
|
|
267
|
-
this.#displacing = null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* The pieces a window covers, or null when the file or the torrent cannot
|
|
272
|
-
* answer yet.
|
|
273
|
-
*
|
|
274
|
-
* @param {import("../demand/index.js").Window} window
|
|
275
|
-
* @returns {{ from: number, to: number } | null}
|
|
276
|
-
*/
|
|
277
|
-
#piecesFor(window) {
|
|
278
|
-
const file = this.#torrent?.files?.[window.fileIndex];
|
|
279
|
-
if (!file) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
return piecesOf({
|
|
283
|
-
fileOffset: Number(file.offset),
|
|
284
|
-
byteStart: window.byteStart,
|
|
285
|
-
byteEnd: Math.min(window.byteEnd, Number(file.length) - 1),
|
|
286
|
-
pieceLength: Number(this.#torrent.pieceLength)
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Whether everything a window asked for has arrived.
|
|
292
|
-
*
|
|
293
|
-
* @param {import("../demand/index.js").Window} window
|
|
294
|
-
* @returns {boolean}
|
|
295
|
-
*/
|
|
296
|
-
#isSatisfied(window) {
|
|
297
|
-
const range = this.#piecesFor(window);
|
|
298
|
-
if (!range) {
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
for (let index = range.from; index <= range.to; index += 1) {
|
|
302
|
-
if (!this.#torrent.bitfield?.get(index)) {
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Whether the library still holds this instruction.
|
|
311
|
-
*
|
|
312
|
-
* Read from its own list, because it removes a selection once satisfied and
|
|
313
|
-
* says nothing about having done so.
|
|
314
|
-
*
|
|
315
|
-
* @param {{ from: number, to: number }} range
|
|
316
|
-
* @returns {boolean}
|
|
317
|
-
*/
|
|
318
|
-
#libraryHolds({ from, to }) {
|
|
319
|
-
const items = Array.isArray(this.#torrent?._selections?._items)
|
|
320
|
-
? this.#torrent._selections._items
|
|
321
|
-
: [];
|
|
322
|
-
return items.some((item) => item?.from === from && item?.to === to);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* @param {{ from: number, to: number, priority: number }} range
|
|
327
|
-
* @returns {void}
|
|
328
|
-
*/
|
|
329
|
-
#select({ from, to, priority }) {
|
|
330
|
-
try {
|
|
331
|
-
// The private form takes the stream flag, which makes a selection several
|
|
332
|
-
// claimants can hold at the same bounds; the public one merges and
|
|
333
|
-
// subtracts intervals and cannot express "one of several wants this". The
|
|
334
|
-
// public call is the fallback if a future version drops the private one.
|
|
335
|
-
if (typeof this.#torrent._select === "function") {
|
|
336
|
-
this.#torrent._select(from, to, priority, null, true);
|
|
337
|
-
} else if (typeof this.#torrent.select === "function") {
|
|
338
|
-
this.#torrent.select(from, to, priority);
|
|
339
|
-
}
|
|
340
|
-
} catch {
|
|
341
|
-
// silent-ok: never fail a read because the download set refused.
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* @param {{ from: number, to: number }} range
|
|
347
|
-
* @returns {void}
|
|
348
|
-
*/
|
|
349
|
-
#deselect({ from, to }) {
|
|
350
|
-
try {
|
|
351
|
-
if (typeof this.#torrent._deselect === "function") {
|
|
352
|
-
this.#torrent._deselect(from, to, true);
|
|
353
|
-
} else if (typeof this.#torrent.deselect === "function") {
|
|
354
|
-
this.#torrent.deselect(from, to);
|
|
355
|
-
}
|
|
356
|
-
} catch {
|
|
357
|
-
// silent-ok.
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* One line saying what the swarm has been told and why.
|
|
363
|
-
*
|
|
364
|
-
* @returns {string}
|
|
365
|
-
*/
|
|
366
|
-
describe(speculativeAllowed = true) {
|
|
367
|
-
const stating = this.#register.levelsToState(
|
|
368
|
-
(window) => this.#isSatisfied(window),
|
|
369
|
-
speculativeAllowed
|
|
370
|
-
);
|
|
371
|
-
const speculative = stating.filter((urgency) => isConditional(urgency)).map(urgencyName);
|
|
372
|
-
const needs = this.#register
|
|
373
|
-
.windows()
|
|
374
|
-
.map((window) => `${urgencyName(window.urgency)}:${window.claimant}`);
|
|
375
|
-
return (
|
|
376
|
-
`download: ${this.#stated.size} instruction(s) to the swarm from ` +
|
|
377
|
-
`${this.#register.size} stated need(s) [${needs.join(" ")}]` +
|
|
378
|
-
(speculative.length > 0
|
|
379
|
-
? `; ${speculative.join(" and ")} also stated — nothing urgent is missing`
|
|
380
|
-
: "; nothing speculative is stated — something urgent is still missing") +
|
|
381
|
-
(this.#displacing
|
|
382
|
-
? `; pieces ${this.#displacing.from}-${this.#displacing.to} may be taken from slow peers`
|
|
383
|
-
: "")
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file The only thing in this proxy that tells WebTorrent what to fetch.
|
|
3
|
+
*
|
|
4
|
+
* One per torrent. It reads the demand register — the single statement of what
|
|
5
|
+
* anybody wants — and turns it into the library's `select`, `deselect` and
|
|
6
|
+
* `critical`. Nothing else calls those, so two parts of this program can no
|
|
7
|
+
* longer ask for different things and overwrite each other.
|
|
8
|
+
*
|
|
9
|
+
* That used to happen and it is written into the code this replaces. The reader
|
|
10
|
+
* held a moving window; the pool held a whole-file selection; a third place set
|
|
11
|
+
* a window around the read head. A whole-file read undid a seek that had just
|
|
12
|
+
* happened, and the swarm walked forward from the first hole: measured on a
|
|
13
|
+
* 4.7 GB film, a seek to 89.1 % fetched 2.47 GB over 93 s before the segment
|
|
14
|
+
* could be served.
|
|
15
|
+
*
|
|
16
|
+
* **Why urgency is not a number given to the library.** Measured against the
|
|
17
|
+
* vendored 2.8.5: selections are sorted by priority only when one is inserted,
|
|
18
|
+
* and `shufflePriority` then moves the selection just served to the back of the
|
|
19
|
+
* whole non-zero group. Distinct numbers therefore order the list once and
|
|
20
|
+
* round-robin it afterwards. So the ordering is kept HERE, by choosing what to
|
|
21
|
+
* state at all, and the library is given only the distinction it honours:
|
|
22
|
+
* non-zero for what is wanted now, zero for the speculative tail.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
isConditional,
|
|
27
|
+
piecesOf,
|
|
28
|
+
selectionPriority,
|
|
29
|
+
Urgency,
|
|
30
|
+
urgencyName
|
|
31
|
+
} from "../demand/index.js";
|
|
32
|
+
import { pieceStoreOf } from "../piece-store-of.js";
|
|
33
|
+
|
|
34
|
+
export class SwarmSelection {
|
|
35
|
+
#torrent;
|
|
36
|
+
#register;
|
|
37
|
+
/** What was last stated to the library, so a restatement can be a no-op. */
|
|
38
|
+
#stated = new Map();
|
|
39
|
+
/** Pieces this instance marked for displacement, so it clears only its own. */
|
|
40
|
+
#displacing = null;
|
|
41
|
+
/** Claimants whose windows are currently protected in memory. */
|
|
42
|
+
#protectedInMemory = new Set();
|
|
43
|
+
#findStore;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} params
|
|
47
|
+
* @param {import("webtorrent").Torrent} params.torrent
|
|
48
|
+
* @param {import("../demand/index.js").DemandRegister} params.register
|
|
49
|
+
* @param {(torrent: object) => object | null} [params.findStore] - How the
|
|
50
|
+
* piece store is reached. Injectable so a test can drive the memory
|
|
51
|
+
* projection without constructing a real store.
|
|
52
|
+
*/
|
|
53
|
+
constructor({ torrent, register, findStore = pieceStoreOf }) {
|
|
54
|
+
this.#torrent = torrent;
|
|
55
|
+
this.#register = register;
|
|
56
|
+
this.#findStore = findStore;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Bring the library's download set into line with what is stated.
|
|
61
|
+
*
|
|
62
|
+
* Called after any change to the register and on a timer. On a timer because
|
|
63
|
+
* WebTorrent DELETES a selection once every piece in it has arrived, so a
|
|
64
|
+
* window that is satisfied and then reopened — the reader moved on, or a
|
|
65
|
+
* piece was evicted and lost — is gone from the library while it is still
|
|
66
|
+
* stated here.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} [options]
|
|
69
|
+
* @param {boolean} [options.speculativeAllowed] - Whether anything on ANY
|
|
70
|
+
* torrent is still waiting for something urgent. The registry works it out
|
|
71
|
+
* once and hands the same answer to every selection, because the link is
|
|
72
|
+
* shared and the question is not a per-torrent one.
|
|
73
|
+
* @returns {{ stated: number, withdrawn: number }}
|
|
74
|
+
*/
|
|
75
|
+
reconcile({ speculativeAllowed = true } = {}) {
|
|
76
|
+
const levels = this.#register.levelsToState(
|
|
77
|
+
(window) => this.#isSatisfied(window),
|
|
78
|
+
speculativeAllowed
|
|
79
|
+
);
|
|
80
|
+
/** @type {Map<string, { from: number, to: number, priority: number }>} */
|
|
81
|
+
const wanted = new Map();
|
|
82
|
+
for (const urgency of levels) {
|
|
83
|
+
for (const window of this.#register.at(urgency)) {
|
|
84
|
+
const range = this.#piecesFor(window);
|
|
85
|
+
if (!range) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Merged by range and priority, not by claimant: two readers wanting
|
|
89
|
+
// the same pieces are one instruction to the swarm.
|
|
90
|
+
const key = `${range.from}-${range.to}-${selectionPriority(urgency)}`;
|
|
91
|
+
wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(urgency) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let withdrawn = 0;
|
|
96
|
+
for (const [key, range] of [...this.#stated]) {
|
|
97
|
+
if (wanted.has(key)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
this.#deselect(range);
|
|
101
|
+
this.#stated.delete(key);
|
|
102
|
+
withdrawn += 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let stated = 0;
|
|
106
|
+
for (const [key, range] of wanted) {
|
|
107
|
+
// Re-stated when the library has dropped it, even though this instance
|
|
108
|
+
// believes it is stated: that is the whole reason this runs on a timer.
|
|
109
|
+
if (this.#stated.has(key) && this.#libraryHolds(range)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
this.#select(range);
|
|
113
|
+
this.#stated.set(key, range);
|
|
114
|
+
stated += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.#markDisplacement();
|
|
118
|
+
this.#projectIntoMemory();
|
|
119
|
+
return { stated, withdrawn };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Take everything back. The torrent is going, or nobody wants anything. */
|
|
123
|
+
releaseAll() {
|
|
124
|
+
for (const range of this.#stated.values()) {
|
|
125
|
+
this.#deselect(range);
|
|
126
|
+
}
|
|
127
|
+
this.#stated.clear();
|
|
128
|
+
this.#clearDisplacement();
|
|
129
|
+
const store = this.#findStore(this.#torrent);
|
|
130
|
+
for (const claimant of this.#protectedInMemory) {
|
|
131
|
+
store?.releaseProtection?.(claimant);
|
|
132
|
+
}
|
|
133
|
+
this.#protectedInMemory.clear();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What is stated right now.
|
|
138
|
+
*
|
|
139
|
+
* @returns {Array<{ from: number, to: number, priority: number }>}
|
|
140
|
+
*/
|
|
141
|
+
statedRanges() {
|
|
142
|
+
return [...this.#stated.values()];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether anything urgent on THIS torrent has not arrived.
|
|
147
|
+
*
|
|
148
|
+
* Read by the registry, which asks every torrent and gives the same answer
|
|
149
|
+
* back to all of them.
|
|
150
|
+
*
|
|
151
|
+
* @returns {boolean}
|
|
152
|
+
*/
|
|
153
|
+
hasUrgentMissing() {
|
|
154
|
+
for (const urgency of [Urgency.BLOCKED, Urgency.NEAR, Urgency.AHEAD]) {
|
|
155
|
+
for (const window of this.#register.at(urgency)) {
|
|
156
|
+
if (!this.#isSatisfied(window)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Permission to take a block from a slow peer, for the level being waited on
|
|
166
|
+
* and nothing else.
|
|
167
|
+
*
|
|
168
|
+
* Cleared before it is set again, because WebTorrent never clears the flag
|
|
169
|
+
* itself: a reader walking a film would otherwise leave every piece of it
|
|
170
|
+
* marked, and the mark would mean nothing anywhere.
|
|
171
|
+
*
|
|
172
|
+
* @returns {void}
|
|
173
|
+
*/
|
|
174
|
+
#markDisplacement() {
|
|
175
|
+
const blocked = this.#register
|
|
176
|
+
.at(Urgency.BLOCKED)
|
|
177
|
+
.map((window) => this.#piecesFor(window))
|
|
178
|
+
.filter((range) => range !== null);
|
|
179
|
+
if (blocked.length === 0) {
|
|
180
|
+
this.#clearDisplacement();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const nearest = Math.min(...blocked.map((range) => range.from));
|
|
184
|
+
const furthest = Math.max(...blocked.map((range) => range.to));
|
|
185
|
+
if (this.#displacing && this.#displacing.from === nearest && this.#displacing.to === furthest) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.#clearDisplacement();
|
|
189
|
+
try {
|
|
190
|
+
this.#torrent.critical?.(nearest, furthest);
|
|
191
|
+
this.#displacing = { from: nearest, to: furthest };
|
|
192
|
+
} catch {
|
|
193
|
+
// silent-ok: displacement is an optimisation, and a torrent being torn
|
|
194
|
+
// down is not worth failing a read over.
|
|
195
|
+
this.#displacing = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Tell the piece store which bytes will be read soon, from the same stated
|
|
201
|
+
* needs the swarm is told about.
|
|
202
|
+
*
|
|
203
|
+
* The second half of stating a need once. Until 2026-09-02 a reader said the
|
|
204
|
+
* same thing twice — `protectRange` to the store for memory and a selection
|
|
205
|
+
* to the torrent for download — and a third piece of code read the first to
|
|
206
|
+
* rebuild the second. Now there is one statement and two views of it, both
|
|
207
|
+
* computed here.
|
|
208
|
+
*
|
|
209
|
+
* WHAT WILL BE READ SOON, WHICH IS NOT WHAT WILL BE DOWNLOADED SOON. Memory
|
|
210
|
+
* holds the first; the swarm is told the second; and the priority map states
|
|
211
|
+
* the second, over the whole rest of the film.
|
|
212
|
+
*
|
|
213
|
+
* `AHEAD` used to reach memory, and it is exactly the speculative lead: the
|
|
214
|
+
* map states one claimant per zone, so on a film with seven zones one `NEAR`
|
|
215
|
+
* and four `AHEAD` arrived here as five separate holders, each covering tens
|
|
216
|
+
* of megabytes. Field 2026-09-08: the store reported `5 reader(s) want 24
|
|
217
|
+
* piece(s) of 25 the store may hold (widest window 17)` — the union of what
|
|
218
|
+
* was declared equalled the whole capacity, so every admission had to evict a
|
|
219
|
+
* piece somebody had declared, and 100 of 1395 evictions did. Beside that,
|
|
220
|
+
* 6565 spills and 7575 revivals in 44 minutes with a median 0.0 s on disk, and
|
|
221
|
+
* 2138 h264 parse errors on a picture that was being COPIED.
|
|
222
|
+
*
|
|
223
|
+
* Raising the allowance does not touch it: a lead stated over the rest of the
|
|
224
|
+
* film grows to fill whatever memory it is given, and the ratio is unchanged.
|
|
225
|
+
* What belongs in memory is what a READ is stopped on and the little in front
|
|
226
|
+
* of it — the levels a read itself states — and those are `BLOCKED` and
|
|
227
|
+
* `NEAR`.
|
|
228
|
+
*
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
#projectIntoMemory() {
|
|
232
|
+
const store = this.#findStore(this.#torrent);
|
|
233
|
+
if (!store || typeof store.protectRange !== "function") {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const holding = new Set();
|
|
237
|
+
for (const urgency of [Urgency.BLOCKED, Urgency.NEAR]) {
|
|
238
|
+
for (const window of this.#register.at(urgency)) {
|
|
239
|
+
const range = this.#piecesFor(window);
|
|
240
|
+
if (!range) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// The level goes with the range: it is what eviction compares when
|
|
244
|
+
// everything resident is wanted by somebody, and dropping it here is
|
|
245
|
+
// what left the store choosing by recency alone.
|
|
246
|
+
store.protectRange(window.claimant, range.from, range.to, window.urgency);
|
|
247
|
+
holding.add(window.claimant);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const claimant of this.#protectedInMemory) {
|
|
251
|
+
if (!holding.has(claimant)) {
|
|
252
|
+
store.releaseProtection?.(claimant);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.#protectedInMemory = holding;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @returns {void} */
|
|
259
|
+
#clearDisplacement() {
|
|
260
|
+
if (!this.#displacing || !Array.isArray(this.#torrent._critical)) {
|
|
261
|
+
this.#displacing = null;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
for (let index = this.#displacing.from; index <= this.#displacing.to; index += 1) {
|
|
265
|
+
this.#torrent._critical[index] = false;
|
|
266
|
+
}
|
|
267
|
+
this.#displacing = null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The pieces a window covers, or null when the file or the torrent cannot
|
|
272
|
+
* answer yet.
|
|
273
|
+
*
|
|
274
|
+
* @param {import("../demand/index.js").Window} window
|
|
275
|
+
* @returns {{ from: number, to: number } | null}
|
|
276
|
+
*/
|
|
277
|
+
#piecesFor(window) {
|
|
278
|
+
const file = this.#torrent?.files?.[window.fileIndex];
|
|
279
|
+
if (!file) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return piecesOf({
|
|
283
|
+
fileOffset: Number(file.offset),
|
|
284
|
+
byteStart: window.byteStart,
|
|
285
|
+
byteEnd: Math.min(window.byteEnd, Number(file.length) - 1),
|
|
286
|
+
pieceLength: Number(this.#torrent.pieceLength)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Whether everything a window asked for has arrived.
|
|
292
|
+
*
|
|
293
|
+
* @param {import("../demand/index.js").Window} window
|
|
294
|
+
* @returns {boolean}
|
|
295
|
+
*/
|
|
296
|
+
#isSatisfied(window) {
|
|
297
|
+
const range = this.#piecesFor(window);
|
|
298
|
+
if (!range) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
for (let index = range.from; index <= range.to; index += 1) {
|
|
302
|
+
if (!this.#torrent.bitfield?.get(index)) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Whether the library still holds this instruction.
|
|
311
|
+
*
|
|
312
|
+
* Read from its own list, because it removes a selection once satisfied and
|
|
313
|
+
* says nothing about having done so.
|
|
314
|
+
*
|
|
315
|
+
* @param {{ from: number, to: number }} range
|
|
316
|
+
* @returns {boolean}
|
|
317
|
+
*/
|
|
318
|
+
#libraryHolds({ from, to }) {
|
|
319
|
+
const items = Array.isArray(this.#torrent?._selections?._items)
|
|
320
|
+
? this.#torrent._selections._items
|
|
321
|
+
: [];
|
|
322
|
+
return items.some((item) => item?.from === from && item?.to === to);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {{ from: number, to: number, priority: number }} range
|
|
327
|
+
* @returns {void}
|
|
328
|
+
*/
|
|
329
|
+
#select({ from, to, priority }) {
|
|
330
|
+
try {
|
|
331
|
+
// The private form takes the stream flag, which makes a selection several
|
|
332
|
+
// claimants can hold at the same bounds; the public one merges and
|
|
333
|
+
// subtracts intervals and cannot express "one of several wants this". The
|
|
334
|
+
// public call is the fallback if a future version drops the private one.
|
|
335
|
+
if (typeof this.#torrent._select === "function") {
|
|
336
|
+
this.#torrent._select(from, to, priority, null, true);
|
|
337
|
+
} else if (typeof this.#torrent.select === "function") {
|
|
338
|
+
this.#torrent.select(from, to, priority);
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
// silent-ok: never fail a read because the download set refused.
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {{ from: number, to: number }} range
|
|
347
|
+
* @returns {void}
|
|
348
|
+
*/
|
|
349
|
+
#deselect({ from, to }) {
|
|
350
|
+
try {
|
|
351
|
+
if (typeof this.#torrent._deselect === "function") {
|
|
352
|
+
this.#torrent._deselect(from, to, true);
|
|
353
|
+
} else if (typeof this.#torrent.deselect === "function") {
|
|
354
|
+
this.#torrent.deselect(from, to);
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
// silent-ok.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* One line saying what the swarm has been told and why.
|
|
363
|
+
*
|
|
364
|
+
* @returns {string}
|
|
365
|
+
*/
|
|
366
|
+
describe(speculativeAllowed = true) {
|
|
367
|
+
const stating = this.#register.levelsToState(
|
|
368
|
+
(window) => this.#isSatisfied(window),
|
|
369
|
+
speculativeAllowed
|
|
370
|
+
);
|
|
371
|
+
const speculative = stating.filter((urgency) => isConditional(urgency)).map(urgencyName);
|
|
372
|
+
const needs = this.#register
|
|
373
|
+
.windows()
|
|
374
|
+
.map((window) => `${urgencyName(window.urgency)}:${window.claimant}`);
|
|
375
|
+
return (
|
|
376
|
+
`download: ${this.#stated.size} instruction(s) to the swarm from ` +
|
|
377
|
+
`${this.#register.size} stated need(s) [${needs.join(" ")}]` +
|
|
378
|
+
(speculative.length > 0
|
|
379
|
+
? `; ${speculative.join(" and ")} also stated — nothing urgent is missing`
|
|
380
|
+
: "; nothing speculative is stated — something urgent is still missing") +
|
|
381
|
+
(this.#displacing
|
|
382
|
+
? `; pieces ${this.#displacing.from}-${this.#displacing.to} may be taken from slow peers`
|
|
383
|
+
: "")
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|