@torrent-tv/proxy 2.89.6 → 2.89.7
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 +338 -338
- package/CHANGELOG.md +5 -0
- 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/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- 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 -178
- 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 +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- 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 +748 -748
- 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 +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- 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/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- 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 -418
- 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 +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- 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/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/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- 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 +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- 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/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- 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 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- 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 +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- 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/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- 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/returns.js +103 -103
- 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/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 +264 -264
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- 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/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/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/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- 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 +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- 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/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 +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- 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 +68 -68
- 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/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- 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/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +240 -240
- 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/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 +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- 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-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- 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 +87 -87
- 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-completeness.test.js +37 -1
- 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 +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- 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 +160 -160
- 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 +64 -64
- package/test/same-height-switch.test.js +663 -663
- 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-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-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- 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-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 -289
- package/test/source-file.test.js +133 -133
- 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 +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- 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/supply-margin.test.js +190 -190
- 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/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-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- 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
package/bin/thread-pool.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How many blocking calls this process can have in flight — set before
|
|
3
|
-
* anything makes one.
|
|
4
|
-
*
|
|
5
|
-
* Node resolves host names with `getaddrinfo`, which runs on the libuv thread
|
|
6
|
-
* pool, and that pool holds FOUR threads by default. A torrent announces to
|
|
7
|
-
* every tracker in its file at once — ten or thirteen of them — so four names
|
|
8
|
-
* are resolved and the rest queue; a tracker that no longer exists holds its
|
|
9
|
-
* thread for the resolver's full ten-second timeout, and every announce behind
|
|
10
|
-
* it blows its own fifteen-second deadline.
|
|
11
|
-
*
|
|
12
|
-
* Measured inside the addon container on 2026-08-18, resolving the ten trackers
|
|
13
|
-
* of one film: as a burst on the default pool, two names answered in 30-40 ms
|
|
14
|
-
* and the other seven took **7.58 s**; with a larger pool every live name
|
|
15
|
-
* answered in **27-42 ms**. The film itself had 517 seeders on a tracker that
|
|
16
|
-
* answers in 50 ms, and spent eleven minutes with zero peers, because every
|
|
17
|
-
* announce — UDP and HTTP alike — timed out waiting for a name.
|
|
18
|
-
*
|
|
19
|
-
* Sized to hold several torrents' announce lists at once, since the same pool
|
|
20
|
-
* also serves this process's file reads. Idle threads cost memory and nothing
|
|
21
|
-
* else, and a deployment that states its own size is left alone.
|
|
22
|
-
*
|
|
23
|
-
* Imported FIRST by the entry point, because a module's imports are evaluated
|
|
24
|
-
* before its body: written as a statement in `cli.js` this would run after
|
|
25
|
-
* every other import had already had its chance to create the pool.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
if (!process.env.UV_THREADPOOL_SIZE) {
|
|
29
|
-
process.env.UV_THREADPOOL_SIZE = "64";
|
|
30
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file How many blocking calls this process can have in flight — set before
|
|
3
|
+
* anything makes one.
|
|
4
|
+
*
|
|
5
|
+
* Node resolves host names with `getaddrinfo`, which runs on the libuv thread
|
|
6
|
+
* pool, and that pool holds FOUR threads by default. A torrent announces to
|
|
7
|
+
* every tracker in its file at once — ten or thirteen of them — so four names
|
|
8
|
+
* are resolved and the rest queue; a tracker that no longer exists holds its
|
|
9
|
+
* thread for the resolver's full ten-second timeout, and every announce behind
|
|
10
|
+
* it blows its own fifteen-second deadline.
|
|
11
|
+
*
|
|
12
|
+
* Measured inside the addon container on 2026-08-18, resolving the ten trackers
|
|
13
|
+
* of one film: as a burst on the default pool, two names answered in 30-40 ms
|
|
14
|
+
* and the other seven took **7.58 s**; with a larger pool every live name
|
|
15
|
+
* answered in **27-42 ms**. The film itself had 517 seeders on a tracker that
|
|
16
|
+
* answers in 50 ms, and spent eleven minutes with zero peers, because every
|
|
17
|
+
* announce — UDP and HTTP alike — timed out waiting for a name.
|
|
18
|
+
*
|
|
19
|
+
* Sized to hold several torrents' announce lists at once, since the same pool
|
|
20
|
+
* also serves this process's file reads. Idle threads cost memory and nothing
|
|
21
|
+
* else, and a deployment that states its own size is left alone.
|
|
22
|
+
*
|
|
23
|
+
* Imported FIRST by the entry point, because a module's imports are evaluated
|
|
24
|
+
* before its body: written as a statement in `cli.js` this would run after
|
|
25
|
+
* every other import had already had its chance to create the pool.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
if (!process.env.UV_THREADPOOL_SIZE) {
|
|
29
|
+
process.env.UV_THREADPOOL_SIZE = "64";
|
|
30
|
+
}
|
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
# Download architecture — what is wanted, and what the swarm is told
|
|
2
|
-
|
|
3
|
-
Two questions, kept apart, and neither of them is "which file".
|
|
4
|
-
|
|
5
|
-
## Two axes, and what is NOT a third
|
|
6
|
-
|
|
7
|
-
**What somebody needs** — a claimant, a file, a byte range, a level of urgency.
|
|
8
|
-
Stated in `services/torrent/demand/`. Knows nothing about WebTorrent, nothing about
|
|
9
|
-
pieces, nothing about the piece store: it is numbers and rules, and it is
|
|
10
|
-
testable without a torrent.
|
|
11
|
-
|
|
12
|
-
**What the swarm is told** — one class, `services/torrent/download/SwarmSelection.js`,
|
|
13
|
-
which reads the register and calls `select`, `deselect` and `critical`. It is
|
|
14
|
-
the only thing in this proxy that calls them.
|
|
15
|
-
|
|
16
|
-
"Which file" is not a third axis. A file nobody has stated a need for is simply
|
|
17
|
-
absent from the register, and a torrent is added with `deselect: true` so
|
|
18
|
-
nothing is fetched until something is stated. Before 2026-09-02 the library's
|
|
19
|
-
own default selected the whole torrent and this proxy undid that afterwards by
|
|
20
|
-
deselecting the files nobody had opened — so on a season pack every episode was
|
|
21
|
-
being fetched for as long as the viewer took to choose one.
|
|
22
|
-
|
|
23
|
-
## Layers
|
|
24
|
-
|
|
25
|
-
```mermaid
|
|
26
|
-
flowchart TB
|
|
27
|
-
subgraph Statements["services/torrent/demand — what is wanted"]
|
|
28
|
-
W[Window<br/>claimant, file, bytes, urgency]
|
|
29
|
-
U[Urgency<br/>BLOCKED NEAR AHEAD TAIL BEHIND]
|
|
30
|
-
R[DemandRegister<br/>live windows by claimant]
|
|
31
|
-
P[pieces.js<br/>the one bytes to pieces conversion]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
subgraph Swarm["services/torrent/download — what the swarm is told"]
|
|
35
|
-
S[SwarmSelection<br/>select / deselect / critical]
|
|
36
|
-
G[registry<br/>one per torrent + the cross-torrent rule]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
subgraph Claimants["who states needs"]
|
|
40
|
-
PM[priority map<br/>where the viewers are, per FILE]
|
|
41
|
-
PR[piece-reader<br/>the piece it is stopped on]
|
|
42
|
-
BF[torrent-pool<br/>background fill, per file]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
PM -->|state / withdraw| R
|
|
46
|
-
PR -->|state / withdraw| R
|
|
47
|
-
BF -->|state / withdraw| R
|
|
48
|
-
W --> R
|
|
49
|
-
U --> R
|
|
50
|
-
R --> S
|
|
51
|
-
P --> S
|
|
52
|
-
G --> S
|
|
53
|
-
S -->|the only caller| WT[(WebTorrent)]
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Who the claimants are, and what each of them knows
|
|
57
|
-
|
|
58
|
-
**The priority map** states what should be downloaded AHEAD of the viewers, once
|
|
59
|
-
per file, built in `services/viewer/` (`PriorityOrchestrator`) from where they are and from nothing
|
|
60
|
-
else. It is the same map the encoding reads — the encoding reads it per OUTPUT
|
|
61
|
-
and in segment numbers, this layer reads it per FILE and in bytes, and both
|
|
62
|
-
scopes are right for what asks them (`encode-architecture.md`).
|
|
63
|
-
|
|
64
|
-
**A read** states only the piece it is STOPPED ON. That is not a forecast but the
|
|
65
|
-
fact that somebody is waiting there, and it is what keeps working for the reads
|
|
66
|
-
the viewers' map does not cover: a container header at open, the subtitle walk, a
|
|
67
|
-
soundtrack being fetched whole. Until 2026-09-02 each read built four bands
|
|
68
|
-
around its own head instead, so fifteen reads were fifteen forecasts on a store
|
|
69
|
-
holding sixteen pieces, half of all evictions took a piece a reader had said it
|
|
70
|
-
wanted, and two thirds of reads came back from disk.
|
|
71
|
-
|
|
72
|
-
**The background fill** states one file at a time — a soundtrack pulled whole
|
|
73
|
-
once the cushion is full, and no further.
|
|
74
|
-
|
|
75
|
-
## Whether the map reaches the swarm, and in what shape
|
|
76
|
-
|
|
77
|
-
Applied in silence until 2026-09-08: that the map had been BUILT was visible in
|
|
78
|
-
the encoding's own line, that the download had received it was visible nowhere,
|
|
79
|
-
and a whole field session carried not one line about it. Said now on change, per
|
|
80
|
-
LEVEL rather than per zone — the register has five levels and the map has as many
|
|
81
|
-
bands as the film needs, and the fit between the two is the one thing here that
|
|
82
|
-
could be wrong:
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
torrent-pool: the swarm is told, for "film.mkv": NEAR 1 zone(s) 42MB,
|
|
86
|
-
AHEAD 4 zone(s) 310MB, TAIL 1 zone(s) 1900MB (7 band(s) of the map, over 1218s of film)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
In megabytes, because that is what a swarm delivers. `NEAR` absent while
|
|
90
|
-
somebody is watching means the map is not arriving; every band landing in one
|
|
91
|
-
level means the fit has collapsed.
|
|
92
|
-
|
|
93
|
-
Beside it, where the waits fell: the reader records each wait against the level
|
|
94
|
-
the map put its piece in, and the `supply` line carries the table. Long waits at
|
|
95
|
-
`blocked` mean the urgent zone is too narrow; long waits further out mean the
|
|
96
|
-
lead is.
|
|
97
|
-
|
|
98
|
-
## What reaches MEMORY is not what reaches the swarm
|
|
99
|
-
|
|
100
|
-
`BLOCKED` and `NEAR` only. Memory holds what will be READ soon; the swarm is
|
|
101
|
-
told what will be DOWNLOADED soon, and the map states the second over the whole
|
|
102
|
-
rest of the film.
|
|
103
|
-
|
|
104
|
-
`AHEAD` used to reach memory too, and it is exactly the speculative lead. The
|
|
105
|
-
map states one claimant per zone, so a film with seven zones arrived at the piece
|
|
106
|
-
store as five separate holders — one `NEAR` and four `AHEAD` — each covering tens
|
|
107
|
-
of megabytes. Field 2026-09-08: `5 reader(s) want 24 piece(s) of 25 the store may
|
|
108
|
-
hold (widest window 17)`, on a session with exactly two reads. The union of what
|
|
109
|
-
was declared equalled the whole capacity, so every admission had to evict a piece
|
|
110
|
-
somebody had declared, and 100 of 1395 evictions did; beside that, 6565 spills
|
|
111
|
-
and 7575 revivals in 44 minutes with a median 0.0 s on disk.
|
|
112
|
-
|
|
113
|
-
Raising the allowance does not touch it: a lead stated over the rest of the film
|
|
114
|
-
grows to fill whatever memory it is given, and the ratio is unchanged.
|
|
115
|
-
|
|
116
|
-
The store's own line now NAMES its holders (`[priority-map:22:0 read:…]`) rather
|
|
117
|
-
than counting them, because the count read as five encoders on a session that
|
|
118
|
-
had two, and choosing between "narrow the windows" and "raise the allowance" was
|
|
119
|
-
guesswork without the names.
|
|
120
|
-
|
|
121
|
-
## How much faster than realtime a step must run
|
|
122
|
-
|
|
123
|
-
`1 / (1 - the share of the reading's time that was lost to waiting)`, over whole
|
|
124
|
-
cycles: from the first interruption's start to the last one's, which holds
|
|
125
|
-
exactly one running stretch per interruption in it.
|
|
126
|
-
|
|
127
|
-
The model is unchanged and was always right — if a fraction `f` of the time
|
|
128
|
-
delivers nothing, producing one second of film takes `1/(1 - f)` seconds. What
|
|
129
|
-
was wrong were the two quantities fed into it: the WORST single interruption
|
|
130
|
-
divided by the MEDIAN gap between interruptions, a maximum over a median, from
|
|
131
|
-
populations that need not be the same events. It asks what would happen if the
|
|
132
|
-
worst interruption recurred at the typical rate — a compound case that never
|
|
133
|
-
occurs — and it divides by a gap that goes to zero whenever interruptions arrive
|
|
134
|
-
in a burst.
|
|
135
|
-
|
|
136
|
-
Field 2026-09-08: 0.79 s over 0.01 s gave **158.60x** on a file already
|
|
137
|
-
downloaded whole, and the quality budget refused every step against it forty
|
|
138
|
-
times in one session. The same measurements as a share of time lost give 1.00x.
|
|
139
|
-
|
|
140
|
-
## The five levels
|
|
141
|
-
|
|
142
|
-
| level | what it is | stated |
|
|
143
|
-
|---|---|---|
|
|
144
|
-
| `BLOCKED` | the bytes a reader is stopped on | always; may take a block from a slow peer |
|
|
145
|
-
| `NEAR` | the rest of that reader's window | always |
|
|
146
|
-
| `AHEAD` | the lead the encoder will reach | always |
|
|
147
|
-
| `TAIL` | to the end of the file | only while nothing urgent is missing, anywhere |
|
|
148
|
-
| `BEHIND` | the gap left by a forward seek | only while nothing urgent is missing, anywhere |
|
|
149
|
-
|
|
150
|
-
## Why urgency is not a number handed to the library
|
|
151
|
-
|
|
152
|
-
Measured against the vendored WebTorrent 2.8.5. Selections are sorted by
|
|
153
|
-
priority **only when one is inserted**:
|
|
154
|
-
|
|
155
|
-
```js
|
|
156
|
-
this._selections.sort((a, b) => b.priority - a.priority)
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
and after a wire's pipeline is filled from a selection, that selection is moved
|
|
160
|
-
to the back of the whole non-zero group:
|
|
161
|
-
|
|
162
|
-
```js
|
|
163
|
-
function shufflePriority (i) {
|
|
164
|
-
let last = i
|
|
165
|
-
for (let j = i; j < self._selections.length && self._selections.get(j).priority; j++) last = j
|
|
166
|
-
self._selections.swap(i, last)
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
So distinct non-zero numbers give an order once and a round robin thereafter.
|
|
171
|
-
Two things hold: non-zero rotates fairly, zero is always last.
|
|
172
|
-
|
|
173
|
-
The ordering is therefore kept in `DemandRegister.levelsToState`, by choosing
|
|
174
|
-
what to state at all, and the library is given only the distinction it honours —
|
|
175
|
-
`1` for anything wanted now, `0` for the speculative tail.
|
|
176
|
-
|
|
177
|
-
The rotation is wanted, not merely tolerated: with two viewers of one film both
|
|
178
|
-
stopped, both needs sit at `BLOCKED` and the swarm alternates between them
|
|
179
|
-
instead of always serving whoever asked first.
|
|
180
|
-
|
|
181
|
-
## Why the speculative levels are withdrawn rather than lowered
|
|
182
|
-
|
|
183
|
-
A peer that cannot help with anything urgent — it lacks those pieces, or every
|
|
184
|
-
block of them is reserved by somebody else — falls through the selection list to
|
|
185
|
-
whatever is below. With a permanently low priority it would then spend the
|
|
186
|
-
shared link on pieces nobody is waiting for, about a second of its own
|
|
187
|
-
throughput at a time (`PIPELINE_MAX_DURATION = 1`).
|
|
188
|
-
|
|
189
|
-
A withdrawn window is not in the download set at all, so there is nothing to
|
|
190
|
-
fall through to.
|
|
191
|
-
|
|
192
|
-
The condition is **global**, in `services/torrent/download/registry.js`, and not per
|
|
193
|
-
torrent: two films on one proxy share the link, so filling the tail of one while
|
|
194
|
-
a viewer of the other has a still picture spends the same bandwidth twice over.
|
|
195
|
-
|
|
196
|
-
## `select` against `critical` — two different things
|
|
197
|
-
|
|
198
|
-
`select(from, to, priority)` decides **what is asked for next**: it inserts into
|
|
199
|
-
the sorted selection list the picker walks.
|
|
200
|
-
|
|
201
|
-
`critical(from, to)` decides **whom it is asked of**. Every block of a piece is
|
|
202
|
-
reserved to exactly one wire; `piece.reserve()` returns `-1` once they all are,
|
|
203
|
-
and a fast idle peer walks past. The flag lets `_hotswap` take a block from the
|
|
204
|
-
slowest holder and give it to the asker:
|
|
205
|
-
|
|
206
|
-
```js
|
|
207
|
-
if (reservation === -1 && hotswap && self._hotswap(wire, index)) {
|
|
208
|
-
reservation = piece.reserve()
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Its thresholds are constants in the library, not settings: the asker must be
|
|
213
|
-
above 16 KB/s, the holder below 48 KB/s and at least twice as slow. So a holder
|
|
214
|
-
at 50 KB/s is never displaced, however long the piece has been waited for.
|
|
215
|
-
Whether that costs us anything is measured rather than assumed — `askFastestWiresFor`
|
|
216
|
-
counts the requests refused while every block was reserved, and the wait line
|
|
217
|
-
prints it. A number there would justify replacing `_hotswap` on the torrent
|
|
218
|
-
object; a zero says the thresholds are not what we are short of.
|
|
219
|
-
|
|
220
|
-
## Several viewers
|
|
221
|
-
|
|
222
|
-
- **Same file.** Each reader is its own claimant. Two stopped viewers put two
|
|
223
|
-
disjoint ranges at `BLOCKED`, and the library's rotation alternates between
|
|
224
|
-
them. Two readers wanting the same pieces are merged into one instruction.
|
|
225
|
-
- **Same torrent, different files.** The background fill is stated **per file**,
|
|
226
|
-
from the furthest window in that file to that file's end. It used to take the
|
|
227
|
-
furthest window across all files and the last piece across all files and claim
|
|
228
|
-
everything between — with two viewers on two episodes of one release, that
|
|
229
|
-
claimed every episode lying between them.
|
|
230
|
-
- **Different torrents.** One register and one selection each, and the
|
|
231
|
-
speculative condition spans all of them.
|
|
232
|
-
|
|
233
|
-
## What was deleted, and why
|
|
234
|
-
|
|
235
|
-
- `claimWindow`, `releaseWindow`, `markCritical`, `clearCritical` in
|
|
236
|
-
`piece-reader.js` — the reader no longer speaks to the library.
|
|
237
|
-
- `#reassertReaderWindows` in `torrent/torrent-pool.js` — it read the piece store's
|
|
238
|
-
MEMORY claims and rebuilt download claims from them, because WebTorrent
|
|
239
|
-
deletes a selection once satisfied. `SwarmSelection.reconcile` does that from
|
|
240
|
-
the register, which is where the statement lives.
|
|
241
|
-
- `#updateBackgroundFill` and `#tailAfterWindows` — replaced by a stated need at
|
|
242
|
-
the `TAIL` level, per file.
|
|
243
|
-
- `#syncSelections` — obsolete once nothing is selected by default.
|
|
244
|
-
- `setActiveFile` — dead: no caller anywhere in the repository.
|
|
245
|
-
- The two claim strategies and the environment variable that chose between them
|
|
246
|
-
(`TORRENT_TV_READ_MODE`). Each read was assigned at random to one of them and
|
|
247
|
-
the waits were sorted by which, and the comparison never decided anything: the
|
|
248
|
-
split halved the sample, so on 2026-08-28 there were nine reads in one arm and
|
|
249
|
-
three in the other against a threshold of ten, and on 2026-08-29 the two arms
|
|
250
|
-
printed together for the first and only time as forty waits against one. Waits
|
|
251
|
-
are now recorded by the LEVEL the reader was stopped in, which says whether a
|
|
252
|
-
band is too narrow rather than whether banding is the wrong idea.
|
|
253
|
-
|
|
254
|
-
## What this does NOT do
|
|
255
|
-
|
|
256
|
-
The piece store keeps its own list of protected ranges for memory
|
|
257
|
-
(`protectRange` / `protectedRanges`). It is fed by the same readers with the
|
|
258
|
-
same windows, but it is a second list, and one of the two could still drift from
|
|
259
|
-
the other. Making the store read this register instead is the remaining half of
|
|
260
|
-
the deduplication; it was left out of the first release because the memory path
|
|
261
|
-
had just been rewritten and had not yet been seen in the field.
|
|
1
|
+
# Download architecture — what is wanted, and what the swarm is told
|
|
2
|
+
|
|
3
|
+
Two questions, kept apart, and neither of them is "which file".
|
|
4
|
+
|
|
5
|
+
## Two axes, and what is NOT a third
|
|
6
|
+
|
|
7
|
+
**What somebody needs** — a claimant, a file, a byte range, a level of urgency.
|
|
8
|
+
Stated in `services/torrent/demand/`. Knows nothing about WebTorrent, nothing about
|
|
9
|
+
pieces, nothing about the piece store: it is numbers and rules, and it is
|
|
10
|
+
testable without a torrent.
|
|
11
|
+
|
|
12
|
+
**What the swarm is told** — one class, `services/torrent/download/SwarmSelection.js`,
|
|
13
|
+
which reads the register and calls `select`, `deselect` and `critical`. It is
|
|
14
|
+
the only thing in this proxy that calls them.
|
|
15
|
+
|
|
16
|
+
"Which file" is not a third axis. A file nobody has stated a need for is simply
|
|
17
|
+
absent from the register, and a torrent is added with `deselect: true` so
|
|
18
|
+
nothing is fetched until something is stated. Before 2026-09-02 the library's
|
|
19
|
+
own default selected the whole torrent and this proxy undid that afterwards by
|
|
20
|
+
deselecting the files nobody had opened — so on a season pack every episode was
|
|
21
|
+
being fetched for as long as the viewer took to choose one.
|
|
22
|
+
|
|
23
|
+
## Layers
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart TB
|
|
27
|
+
subgraph Statements["services/torrent/demand — what is wanted"]
|
|
28
|
+
W[Window<br/>claimant, file, bytes, urgency]
|
|
29
|
+
U[Urgency<br/>BLOCKED NEAR AHEAD TAIL BEHIND]
|
|
30
|
+
R[DemandRegister<br/>live windows by claimant]
|
|
31
|
+
P[pieces.js<br/>the one bytes to pieces conversion]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
subgraph Swarm["services/torrent/download — what the swarm is told"]
|
|
35
|
+
S[SwarmSelection<br/>select / deselect / critical]
|
|
36
|
+
G[registry<br/>one per torrent + the cross-torrent rule]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
subgraph Claimants["who states needs"]
|
|
40
|
+
PM[priority map<br/>where the viewers are, per FILE]
|
|
41
|
+
PR[piece-reader<br/>the piece it is stopped on]
|
|
42
|
+
BF[torrent-pool<br/>background fill, per file]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
PM -->|state / withdraw| R
|
|
46
|
+
PR -->|state / withdraw| R
|
|
47
|
+
BF -->|state / withdraw| R
|
|
48
|
+
W --> R
|
|
49
|
+
U --> R
|
|
50
|
+
R --> S
|
|
51
|
+
P --> S
|
|
52
|
+
G --> S
|
|
53
|
+
S -->|the only caller| WT[(WebTorrent)]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Who the claimants are, and what each of them knows
|
|
57
|
+
|
|
58
|
+
**The priority map** states what should be downloaded AHEAD of the viewers, once
|
|
59
|
+
per file, built in `services/viewer/` (`PriorityOrchestrator`) from where they are and from nothing
|
|
60
|
+
else. It is the same map the encoding reads — the encoding reads it per OUTPUT
|
|
61
|
+
and in segment numbers, this layer reads it per FILE and in bytes, and both
|
|
62
|
+
scopes are right for what asks them (`encode-architecture.md`).
|
|
63
|
+
|
|
64
|
+
**A read** states only the piece it is STOPPED ON. That is not a forecast but the
|
|
65
|
+
fact that somebody is waiting there, and it is what keeps working for the reads
|
|
66
|
+
the viewers' map does not cover: a container header at open, the subtitle walk, a
|
|
67
|
+
soundtrack being fetched whole. Until 2026-09-02 each read built four bands
|
|
68
|
+
around its own head instead, so fifteen reads were fifteen forecasts on a store
|
|
69
|
+
holding sixteen pieces, half of all evictions took a piece a reader had said it
|
|
70
|
+
wanted, and two thirds of reads came back from disk.
|
|
71
|
+
|
|
72
|
+
**The background fill** states one file at a time — a soundtrack pulled whole
|
|
73
|
+
once the cushion is full, and no further.
|
|
74
|
+
|
|
75
|
+
## Whether the map reaches the swarm, and in what shape
|
|
76
|
+
|
|
77
|
+
Applied in silence until 2026-09-08: that the map had been BUILT was visible in
|
|
78
|
+
the encoding's own line, that the download had received it was visible nowhere,
|
|
79
|
+
and a whole field session carried not one line about it. Said now on change, per
|
|
80
|
+
LEVEL rather than per zone — the register has five levels and the map has as many
|
|
81
|
+
bands as the film needs, and the fit between the two is the one thing here that
|
|
82
|
+
could be wrong:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
torrent-pool: the swarm is told, for "film.mkv": NEAR 1 zone(s) 42MB,
|
|
86
|
+
AHEAD 4 zone(s) 310MB, TAIL 1 zone(s) 1900MB (7 band(s) of the map, over 1218s of film)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
In megabytes, because that is what a swarm delivers. `NEAR` absent while
|
|
90
|
+
somebody is watching means the map is not arriving; every band landing in one
|
|
91
|
+
level means the fit has collapsed.
|
|
92
|
+
|
|
93
|
+
Beside it, where the waits fell: the reader records each wait against the level
|
|
94
|
+
the map put its piece in, and the `supply` line carries the table. Long waits at
|
|
95
|
+
`blocked` mean the urgent zone is too narrow; long waits further out mean the
|
|
96
|
+
lead is.
|
|
97
|
+
|
|
98
|
+
## What reaches MEMORY is not what reaches the swarm
|
|
99
|
+
|
|
100
|
+
`BLOCKED` and `NEAR` only. Memory holds what will be READ soon; the swarm is
|
|
101
|
+
told what will be DOWNLOADED soon, and the map states the second over the whole
|
|
102
|
+
rest of the film.
|
|
103
|
+
|
|
104
|
+
`AHEAD` used to reach memory too, and it is exactly the speculative lead. The
|
|
105
|
+
map states one claimant per zone, so a film with seven zones arrived at the piece
|
|
106
|
+
store as five separate holders — one `NEAR` and four `AHEAD` — each covering tens
|
|
107
|
+
of megabytes. Field 2026-09-08: `5 reader(s) want 24 piece(s) of 25 the store may
|
|
108
|
+
hold (widest window 17)`, on a session with exactly two reads. The union of what
|
|
109
|
+
was declared equalled the whole capacity, so every admission had to evict a piece
|
|
110
|
+
somebody had declared, and 100 of 1395 evictions did; beside that, 6565 spills
|
|
111
|
+
and 7575 revivals in 44 minutes with a median 0.0 s on disk.
|
|
112
|
+
|
|
113
|
+
Raising the allowance does not touch it: a lead stated over the rest of the film
|
|
114
|
+
grows to fill whatever memory it is given, and the ratio is unchanged.
|
|
115
|
+
|
|
116
|
+
The store's own line now NAMES its holders (`[priority-map:22:0 read:…]`) rather
|
|
117
|
+
than counting them, because the count read as five encoders on a session that
|
|
118
|
+
had two, and choosing between "narrow the windows" and "raise the allowance" was
|
|
119
|
+
guesswork without the names.
|
|
120
|
+
|
|
121
|
+
## How much faster than realtime a step must run
|
|
122
|
+
|
|
123
|
+
`1 / (1 - the share of the reading's time that was lost to waiting)`, over whole
|
|
124
|
+
cycles: from the first interruption's start to the last one's, which holds
|
|
125
|
+
exactly one running stretch per interruption in it.
|
|
126
|
+
|
|
127
|
+
The model is unchanged and was always right — if a fraction `f` of the time
|
|
128
|
+
delivers nothing, producing one second of film takes `1/(1 - f)` seconds. What
|
|
129
|
+
was wrong were the two quantities fed into it: the WORST single interruption
|
|
130
|
+
divided by the MEDIAN gap between interruptions, a maximum over a median, from
|
|
131
|
+
populations that need not be the same events. It asks what would happen if the
|
|
132
|
+
worst interruption recurred at the typical rate — a compound case that never
|
|
133
|
+
occurs — and it divides by a gap that goes to zero whenever interruptions arrive
|
|
134
|
+
in a burst.
|
|
135
|
+
|
|
136
|
+
Field 2026-09-08: 0.79 s over 0.01 s gave **158.60x** on a file already
|
|
137
|
+
downloaded whole, and the quality budget refused every step against it forty
|
|
138
|
+
times in one session. The same measurements as a share of time lost give 1.00x.
|
|
139
|
+
|
|
140
|
+
## The five levels
|
|
141
|
+
|
|
142
|
+
| level | what it is | stated |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `BLOCKED` | the bytes a reader is stopped on | always; may take a block from a slow peer |
|
|
145
|
+
| `NEAR` | the rest of that reader's window | always |
|
|
146
|
+
| `AHEAD` | the lead the encoder will reach | always |
|
|
147
|
+
| `TAIL` | to the end of the file | only while nothing urgent is missing, anywhere |
|
|
148
|
+
| `BEHIND` | the gap left by a forward seek | only while nothing urgent is missing, anywhere |
|
|
149
|
+
|
|
150
|
+
## Why urgency is not a number handed to the library
|
|
151
|
+
|
|
152
|
+
Measured against the vendored WebTorrent 2.8.5. Selections are sorted by
|
|
153
|
+
priority **only when one is inserted**:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
this._selections.sort((a, b) => b.priority - a.priority)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
and after a wire's pipeline is filled from a selection, that selection is moved
|
|
160
|
+
to the back of the whole non-zero group:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
function shufflePriority (i) {
|
|
164
|
+
let last = i
|
|
165
|
+
for (let j = i; j < self._selections.length && self._selections.get(j).priority; j++) last = j
|
|
166
|
+
self._selections.swap(i, last)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
So distinct non-zero numbers give an order once and a round robin thereafter.
|
|
171
|
+
Two things hold: non-zero rotates fairly, zero is always last.
|
|
172
|
+
|
|
173
|
+
The ordering is therefore kept in `DemandRegister.levelsToState`, by choosing
|
|
174
|
+
what to state at all, and the library is given only the distinction it honours —
|
|
175
|
+
`1` for anything wanted now, `0` for the speculative tail.
|
|
176
|
+
|
|
177
|
+
The rotation is wanted, not merely tolerated: with two viewers of one film both
|
|
178
|
+
stopped, both needs sit at `BLOCKED` and the swarm alternates between them
|
|
179
|
+
instead of always serving whoever asked first.
|
|
180
|
+
|
|
181
|
+
## Why the speculative levels are withdrawn rather than lowered
|
|
182
|
+
|
|
183
|
+
A peer that cannot help with anything urgent — it lacks those pieces, or every
|
|
184
|
+
block of them is reserved by somebody else — falls through the selection list to
|
|
185
|
+
whatever is below. With a permanently low priority it would then spend the
|
|
186
|
+
shared link on pieces nobody is waiting for, about a second of its own
|
|
187
|
+
throughput at a time (`PIPELINE_MAX_DURATION = 1`).
|
|
188
|
+
|
|
189
|
+
A withdrawn window is not in the download set at all, so there is nothing to
|
|
190
|
+
fall through to.
|
|
191
|
+
|
|
192
|
+
The condition is **global**, in `services/torrent/download/registry.js`, and not per
|
|
193
|
+
torrent: two films on one proxy share the link, so filling the tail of one while
|
|
194
|
+
a viewer of the other has a still picture spends the same bandwidth twice over.
|
|
195
|
+
|
|
196
|
+
## `select` against `critical` — two different things
|
|
197
|
+
|
|
198
|
+
`select(from, to, priority)` decides **what is asked for next**: it inserts into
|
|
199
|
+
the sorted selection list the picker walks.
|
|
200
|
+
|
|
201
|
+
`critical(from, to)` decides **whom it is asked of**. Every block of a piece is
|
|
202
|
+
reserved to exactly one wire; `piece.reserve()` returns `-1` once they all are,
|
|
203
|
+
and a fast idle peer walks past. The flag lets `_hotswap` take a block from the
|
|
204
|
+
slowest holder and give it to the asker:
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
if (reservation === -1 && hotswap && self._hotswap(wire, index)) {
|
|
208
|
+
reservation = piece.reserve()
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Its thresholds are constants in the library, not settings: the asker must be
|
|
213
|
+
above 16 KB/s, the holder below 48 KB/s and at least twice as slow. So a holder
|
|
214
|
+
at 50 KB/s is never displaced, however long the piece has been waited for.
|
|
215
|
+
Whether that costs us anything is measured rather than assumed — `askFastestWiresFor`
|
|
216
|
+
counts the requests refused while every block was reserved, and the wait line
|
|
217
|
+
prints it. A number there would justify replacing `_hotswap` on the torrent
|
|
218
|
+
object; a zero says the thresholds are not what we are short of.
|
|
219
|
+
|
|
220
|
+
## Several viewers
|
|
221
|
+
|
|
222
|
+
- **Same file.** Each reader is its own claimant. Two stopped viewers put two
|
|
223
|
+
disjoint ranges at `BLOCKED`, and the library's rotation alternates between
|
|
224
|
+
them. Two readers wanting the same pieces are merged into one instruction.
|
|
225
|
+
- **Same torrent, different files.** The background fill is stated **per file**,
|
|
226
|
+
from the furthest window in that file to that file's end. It used to take the
|
|
227
|
+
furthest window across all files and the last piece across all files and claim
|
|
228
|
+
everything between — with two viewers on two episodes of one release, that
|
|
229
|
+
claimed every episode lying between them.
|
|
230
|
+
- **Different torrents.** One register and one selection each, and the
|
|
231
|
+
speculative condition spans all of them.
|
|
232
|
+
|
|
233
|
+
## What was deleted, and why
|
|
234
|
+
|
|
235
|
+
- `claimWindow`, `releaseWindow`, `markCritical`, `clearCritical` in
|
|
236
|
+
`piece-reader.js` — the reader no longer speaks to the library.
|
|
237
|
+
- `#reassertReaderWindows` in `torrent/torrent-pool.js` — it read the piece store's
|
|
238
|
+
MEMORY claims and rebuilt download claims from them, because WebTorrent
|
|
239
|
+
deletes a selection once satisfied. `SwarmSelection.reconcile` does that from
|
|
240
|
+
the register, which is where the statement lives.
|
|
241
|
+
- `#updateBackgroundFill` and `#tailAfterWindows` — replaced by a stated need at
|
|
242
|
+
the `TAIL` level, per file.
|
|
243
|
+
- `#syncSelections` — obsolete once nothing is selected by default.
|
|
244
|
+
- `setActiveFile` — dead: no caller anywhere in the repository.
|
|
245
|
+
- The two claim strategies and the environment variable that chose between them
|
|
246
|
+
(`TORRENT_TV_READ_MODE`). Each read was assigned at random to one of them and
|
|
247
|
+
the waits were sorted by which, and the comparison never decided anything: the
|
|
248
|
+
split halved the sample, so on 2026-08-28 there were nine reads in one arm and
|
|
249
|
+
three in the other against a threshold of ten, and on 2026-08-29 the two arms
|
|
250
|
+
printed together for the first and only time as forty waits against one. Waits
|
|
251
|
+
are now recorded by the LEVEL the reader was stopped in, which says whether a
|
|
252
|
+
band is too narrow rather than whether banding is the wrong idea.
|
|
253
|
+
|
|
254
|
+
## What this does NOT do
|
|
255
|
+
|
|
256
|
+
The piece store keeps its own list of protected ranges for memory
|
|
257
|
+
(`protectRange` / `protectedRanges`). It is fed by the same readers with the
|
|
258
|
+
same windows, but it is a second list, and one of the two could still drift from
|
|
259
|
+
the other. Making the store read this register instead is the remaining half of
|
|
260
|
+
the deduplication; it was left out of the first release because the memory path
|
|
261
|
+
had just been rewritten and had not yet been seen in the field.
|