@torrent-tv/proxy 2.89.7 → 2.89.8
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 +358 -338
- package/CHANGELOG.md +2340 -2335
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -412
- package/docs/browser-proxy-contract.md +155 -155
- package/docs/container-architecture.md +273 -273
- 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 +113 -113
- 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 +51 -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/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +106 -106
- 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/build-audio-samples.mjs +31 -31
- 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 +36 -12
- 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 +784 -784
- 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/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- 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 -116
- 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/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 +24 -24
- 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 +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 +757 -757
- 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/init-compat.js +324 -324
- 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 +202 -202
- package/services/media/KeyframeTables.js +356 -356
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +592 -592
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +299 -299
- package/services/media/container/AviContainer.js +268 -268
- package/services/media/container/Container.js +632 -632
- package/services/media/container/ContainerFactory.js +38 -38
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1031 -1031
- package/services/media/container/Mp4Container.js +1354 -1354
- package/services/media/container/SubtitleFileContainer.js +339 -339
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/container/ebml-stream.js +174 -174
- package/services/media/container/matroska-clusters.js +1165 -1165
- package/services/media/container/unavailable.js +77 -77
- 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/ContainerTrack.js +84 -84
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +295 -295
- 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/piece-store/shared-piece-store.js +1937 -1937
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1294 -1294
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -382
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/torrent-pool.js +2688 -2688
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +836 -836
- 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 +231 -231
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1137 -1137
- package/services/transport/data-channel-handler.js +1373 -1373
- 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 +421 -421
- 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-calibration.test.js +23 -23
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -762
- 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/coalesce.test.js +39 -39
- 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 +233 -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 +475 -475
- 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 +20 -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/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -1
- 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 +36 -38
- package/test/held-read-ownership.test.js +77 -77
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +248 -240
- package/test/helpers/held-file.js +77 -77
- package/test/helpers/manager.js +62 -62
- package/test/helpers/matroska-file.js +296 -296
- 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 +123 -123
- 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-cluster-discovery.test.js +323 -323
- 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-output-one-session.test.js +273 -273
- 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 +92 -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-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/quality-variants.test.js +1274 -1270
- package/test/rate-control-in-the-output.test.js +181 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/reads-not-yet-arrived.test.js +218 -218
- 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-closed-on-both-branches.test.js +114 -114
- 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 +222 -222
- package/test/segment-serve-wiring.test.js +481 -481
- 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-existing-output.test.js +111 -111
- 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-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- 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 +385 -385
- 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-metadata.test.js +15 -15
- package/test/subtitle-plan-waits.test.js +125 -125
- package/test/subtitle-track-numbering.test.js +391 -391
- 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/two-viewers-one-picture.test.js +508 -508
- 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 +92 -92
- package/test/viewer-starts-on-map.test.js +27 -27
- package/test/viewer-states.test.js +192 -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/whole-file-held-read.test.js +30 -30
- 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/coalesce.js +41 -41
- 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,340 +1,340 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which output answers THIS viewer, and for how long that stands.
|
|
3
|
-
*
|
|
4
|
-
* Until now the answer was one record per (file, asked height), shared by
|
|
5
|
-
* everybody watching the file: `OutputCatalog.servedBy`. That is the very thing
|
|
6
|
-
* roadmap item 97 forbids — a viewer on a thin link would have moved the output
|
|
7
|
-
* of a viewer on a thick one, because both read the same entry. So the answer
|
|
8
|
-
* belongs to the viewer, and this is where a viewer keeps it.
|
|
9
|
-
*
|
|
10
|
-
* FIVE FACTS, and each is here because something reads it:
|
|
11
|
-
*
|
|
12
|
-
* 1. **the chosen output** per asked height — what the suitability rule
|
|
13
|
-
* decided, and the only thing that may change it;
|
|
14
|
-
* 2. the output being PREPARED — `Viewer.warmingVariantId`, which already
|
|
15
|
-
* exists and stays where it is;
|
|
16
|
-
* 3. **this viewer's own generation**, raised when THIS viewer seeks. Not the
|
|
17
|
-
* output's `waitEpoch`: that one is shared, rises for reasons that are not a
|
|
18
|
-
* seek, and is moved by other people's seeks on a shared output. A request
|
|
19
|
-
* CARRIES the generation it was made in, because a request that arrives late
|
|
20
|
-
* would otherwise be read against the current one and answered as though it
|
|
21
|
-
* had just been made;
|
|
22
|
-
* 4. **what was given**: which output answered segment N in generation G. This
|
|
23
|
-
* is what makes a repeat of a request get the same output rather than being
|
|
24
|
-
* decided again;
|
|
25
|
-
* 5. **what was accepted**: the assignments whose response has already begun.
|
|
26
|
-
*
|
|
27
|
-
* TWO DEADLINES, NOT ONE. An earlier draft dropped an assignment the moment its
|
|
28
|
-
* generation was left behind, which contradicts finishing a response that is
|
|
29
|
-
* still being sent: the bytes would still be going out while the reason for
|
|
30
|
-
* holding their output had gone.
|
|
31
|
-
*
|
|
32
|
-
* 1. a generation that has been left behind stops accepting NEW requests after
|
|
33
|
-
* `acceptWindowMs`. What that window should be is NOT derived here — the
|
|
34
|
-
* player's retry policy is overridden by the product's own page and the
|
|
35
|
-
* native path has none at all — so it is given explicitly and checked on
|
|
36
|
-
* what it is given;
|
|
37
|
-
* 2. an ACCEPTED request keeps its assignment until its response finishes or is
|
|
38
|
-
* released, whatever generation it belongs to and however long that takes.
|
|
39
|
-
*
|
|
40
|
-
* An output is kept while any assignment of it stands: `heldKeys()` is asked
|
|
41
|
-
* before an output is let go, rather than the assignments being discovered
|
|
42
|
-
* afterwards.
|
|
43
|
-
*
|
|
44
|
-
* LIMIT, stated rather than left to be found: an assignment is keyed by the
|
|
45
|
-
* HEIGHT asked for, so it speaks for the picture and its steps. A soundtrack is
|
|
46
|
-
* not a height and records nothing in `given`; item 97 is about the picture.
|
|
47
|
-
* A soundtrack's response is still held by `accept` while it is being sent,
|
|
48
|
-
* and a request for a soundtrack made in a viewing that was left is still
|
|
49
|
-
* refused — neither of those is about heights.
|
|
50
|
-
*
|
|
51
|
-
* WHERE EACH FACT IS READ AND WRITTEN (roadmap item 97, step 9):
|
|
52
|
-
*
|
|
53
|
-
* 1. the generation — told by the seek route (`ViewerRequests.requestSeek`),
|
|
54
|
-
* checked by every file route BEFORE anything else the route does
|
|
55
|
-
* (`routes/transcode/session-file/get.js`, `refusedAsStale`);
|
|
56
|
-
* 2. `given` — written and read by the step route
|
|
57
|
-
* (`Renditions.resolveVariantFile`) under the height asked for and the
|
|
58
|
-
* segment, −1 for the init; written by the picture's own route under the
|
|
59
|
-
* height that output is named after (`ViewerRequests.noteAnsweredDirectly`);
|
|
60
|
-
* 3. `accepted` — held from the start of a response to its `finish`, `close`
|
|
61
|
-
* or `error` (`serveSessionFile`);
|
|
62
|
-
* 4. `heldKeys` — asked before an output is disposed (`Viewers.stillNeeded`,
|
|
63
|
-
* `Viewers.assignmentsHold`);
|
|
64
|
-
* 5. `chosen` — written only by the suitability rule, for an output that suits
|
|
65
|
-
* this viewer (`Renditions`, through `viewer/choices.js`), and read first
|
|
66
|
-
* for every NEW address after `given` (step 11). The shared
|
|
67
|
-
* `OutputCatalog.servedBy` record it replaced is gone: one viewer's answer is
|
|
68
|
-
* no longer every viewer's. An answer of "nothing suits" is never recorded
|
|
69
|
-
* here — it is answered to the request as `output-unavailable`.
|
|
70
|
-
*
|
|
71
|
-
* ORDER OF ANSWERING, and why: `given` before `chosen` before the rule. A
|
|
72
|
-
* repeat of an address must get what answered it, even after `chosen` has
|
|
73
|
-
* moved to another output of the same height — the player holds that output's
|
|
74
|
-
* header for it. When the output behind `given` has gone, the address is not
|
|
75
|
-
* decided again as if new: it is served from the stored piece, or by an output
|
|
76
|
-
* whose header is proven compatible, or answered `assignment-lost`.
|
|
77
|
-
*/
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The address of one thing asked for: a height of the picture and a segment.
|
|
81
|
-
*
|
|
82
|
-
* @param {number} askedHeight
|
|
83
|
-
* @param {number} segmentIndex
|
|
84
|
-
* @returns {string}
|
|
85
|
-
*/
|
|
86
|
-
function addressOf(askedHeight, segmentIndex) {
|
|
87
|
-
return `${askedHeight}:${segmentIndex}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class Assignments {
|
|
91
|
-
/** Chosen output per asked height. @type {Map<number, string>} */
|
|
92
|
-
#chosen = new Map();
|
|
93
|
-
/** Generation → (address → output key). @type {Map<number, Map<string, string>>} */
|
|
94
|
-
#given = new Map();
|
|
95
|
-
/** Generation → when it stopped being the current one. @type {Map<number, number>} */
|
|
96
|
-
#leftAt = new Map();
|
|
97
|
-
/** Accepted responses, by their own token. @type {Map<string, { outputKey: string }>} */
|
|
98
|
-
#accepted = new Map();
|
|
99
|
-
#generation = 0;
|
|
100
|
-
#nextToken = 0;
|
|
101
|
-
#acceptWindowMs;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* @param {{ acceptWindowMs: number }} limits - Explicitly given; see the
|
|
105
|
-
* first deadline above for why it is not derived.
|
|
106
|
-
*/
|
|
107
|
-
constructor({ acceptWindowMs }) {
|
|
108
|
-
if (!Number.isFinite(acceptWindowMs) || acceptWindowMs < 0) {
|
|
109
|
-
throw new TypeError("Assignments: acceptWindowMs must be a number of milliseconds");
|
|
110
|
-
}
|
|
111
|
-
this.#acceptWindowMs = acceptWindowMs;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** The generation a request made now belongs to. @returns {number} */
|
|
115
|
-
get generation() {
|
|
116
|
-
return this.#generation;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* The page states which viewing its requests now belong to.
|
|
121
|
-
*
|
|
122
|
-
* TOLD, NOT COUNTED. The page is where a seek begins and where every request
|
|
123
|
-
* is built, so it is the only place that can stamp a request with the viewing
|
|
124
|
-
* it belongs to; a number counted here would be a second owner of one fact
|
|
125
|
-
* and the two would disagree exactly when a request and a seek cross.
|
|
126
|
-
*
|
|
127
|
-
* Monotonic — a lower number is ignored, so a reordered or repeated message
|
|
128
|
-
* cannot take the viewer back. The generation being left is not dropped: it
|
|
129
|
-
* is marked with the moment it stopped being current, which is what its
|
|
130
|
-
* acceptance window is measured from.
|
|
131
|
-
*
|
|
132
|
-
* @param {number} generation
|
|
133
|
-
* @param {number} [now]
|
|
134
|
-
* @returns {number} The generation now current.
|
|
135
|
-
*/
|
|
136
|
-
statedGeneration(generation, now = Date.now()) {
|
|
137
|
-
if (!Number.isInteger(generation) || generation <= this.#generation) {
|
|
138
|
-
return this.#generation;
|
|
139
|
-
}
|
|
140
|
-
this.#leftAt.set(this.#generation, now);
|
|
141
|
-
this.#generation = generation;
|
|
142
|
-
return this.#generation;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Whether a request made in `generation` may still be answered.
|
|
147
|
-
*
|
|
148
|
-
* A generation AHEAD of the one stated is accepted, and that is not
|
|
149
|
-
* laxness: the page raises its own number before it sends anything, so a
|
|
150
|
-
* segment request can overtake the seek that announces the new viewing. The
|
|
151
|
-
* page is the owner, so being behind it means this side has not heard yet.
|
|
152
|
-
*
|
|
153
|
-
* A request that states nothing at all is accepted — a transport with no
|
|
154
|
-
* loader of ours builds no such parameter, and it has one viewing.
|
|
155
|
-
*
|
|
156
|
-
* @param {number} generation
|
|
157
|
-
* @param {number} [now]
|
|
158
|
-
* @returns {boolean}
|
|
159
|
-
*/
|
|
160
|
-
accepts(generation, now = Date.now()) {
|
|
161
|
-
if (!Number.isInteger(generation) || generation >= this.#generation) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
const left = this.#leftAt.get(generation);
|
|
165
|
-
return left !== undefined && now - left <= this.#acceptWindowMs;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** The output chosen for this height, or an empty string. @returns {string} */
|
|
169
|
-
chosenFor(askedHeight) {
|
|
170
|
-
return this.#chosen.get(askedHeight) ?? "";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* The suitability rule has decided. This is the ONLY thing that moves a
|
|
175
|
-
* choice: a request never decides one for itself.
|
|
176
|
-
*
|
|
177
|
-
* @param {number} askedHeight
|
|
178
|
-
* @param {string} outputKey
|
|
179
|
-
* @returns {void}
|
|
180
|
-
*/
|
|
181
|
-
choose(askedHeight, outputKey) {
|
|
182
|
-
if (!(askedHeight > 0) || !outputKey) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
this.#chosen.set(askedHeight, outputKey);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* The asked heights whose choice is this output.
|
|
190
|
-
*
|
|
191
|
-
* A step is NAMED after the height it produces and CHOSEN under the height
|
|
192
|
-
* the player asked for, and the two can differ. A move to another output of
|
|
193
|
-
* the same height has to replace the choice under the height the player
|
|
194
|
-
* asks for, so it is read back from the choice rather than from the name.
|
|
195
|
-
*
|
|
196
|
-
* @param {string} outputKey
|
|
197
|
-
* @returns {number[]}
|
|
198
|
-
*/
|
|
199
|
-
heightsChosenAs(outputKey) {
|
|
200
|
-
const heights = [];
|
|
201
|
-
for (const [askedHeight, key] of this.#chosen) {
|
|
202
|
-
if (key === outputKey) {
|
|
203
|
-
heights.push(askedHeight);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return heights;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* The highest segment given at this height in this generation, or −1 when
|
|
211
|
-
* nothing but perhaps the init was.
|
|
212
|
-
*
|
|
213
|
-
* What the player asks for next is the segment after it: hls.js fetches a
|
|
214
|
-
* level's segments in order within one viewing. Nothing given yet is the
|
|
215
|
-
* state straight after a seek, and then the next is where the viewer stands,
|
|
216
|
-
* which only the caller can turn into a number.
|
|
217
|
-
*
|
|
218
|
-
* @param {number} generation
|
|
219
|
-
* @param {number} askedHeight
|
|
220
|
-
* @returns {number}
|
|
221
|
-
*/
|
|
222
|
-
highestGiven(generation, askedHeight) {
|
|
223
|
-
const byAddress = this.#given.get(generation);
|
|
224
|
-
if (!byAddress) {
|
|
225
|
-
return -1;
|
|
226
|
-
}
|
|
227
|
-
const prefix = `${askedHeight}:`;
|
|
228
|
-
let highest = -1;
|
|
229
|
-
for (const address of byAddress.keys()) {
|
|
230
|
-
if (!address.startsWith(prefix)) {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const index = Number(address.slice(prefix.length));
|
|
234
|
-
if (Number.isInteger(index) && index > highest) {
|
|
235
|
-
highest = index;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return highest;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* What answered this segment in this generation, or an empty string.
|
|
243
|
-
*
|
|
244
|
-
* @param {number} generation
|
|
245
|
-
* @param {number} askedHeight
|
|
246
|
-
* @param {number} segmentIndex
|
|
247
|
-
* @returns {string}
|
|
248
|
-
*/
|
|
249
|
-
givenFor(generation, askedHeight, segmentIndex) {
|
|
250
|
-
return this.#given.get(generation)?.get(addressOf(askedHeight, segmentIndex)) ?? "";
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Record what answered, so a repeat of the same request is answered the same
|
|
255
|
-
* way rather than decided again.
|
|
256
|
-
*
|
|
257
|
-
* @param {number} generation
|
|
258
|
-
* @param {number} askedHeight
|
|
259
|
-
* @param {number} segmentIndex
|
|
260
|
-
* @param {string} outputKey
|
|
261
|
-
* @returns {void}
|
|
262
|
-
*/
|
|
263
|
-
give(generation, askedHeight, segmentIndex, outputKey) {
|
|
264
|
-
if (!outputKey || !(askedHeight > 0) || !Number.isInteger(segmentIndex)) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const byAddress = this.#given.get(generation) ?? new Map();
|
|
268
|
-
byAddress.set(addressOf(askedHeight, segmentIndex), outputKey);
|
|
269
|
-
this.#given.set(generation, byAddress);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* A response has begun. The token returned releases it, and until it is
|
|
274
|
-
* released this output is held whatever else happens to its generation.
|
|
275
|
-
*
|
|
276
|
-
* @param {string} outputKey
|
|
277
|
-
* @returns {string} token
|
|
278
|
-
*/
|
|
279
|
-
accept(outputKey) {
|
|
280
|
-
this.#nextToken += 1;
|
|
281
|
-
const token = `a${this.#nextToken}`;
|
|
282
|
-
this.#accepted.set(token, { outputKey });
|
|
283
|
-
return token;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* The response is finished or cancelled.
|
|
288
|
-
*
|
|
289
|
-
* @param {string} token
|
|
290
|
-
* @returns {void}
|
|
291
|
-
*/
|
|
292
|
-
release(token) {
|
|
293
|
-
this.#accepted.delete(token);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Every output this viewer's assignments hold, asked BEFORE one is let go.
|
|
298
|
-
*
|
|
299
|
-
* Three sources, and each is a reason on its own: a choice that stands, a
|
|
300
|
-
* response still being sent, and what was given inside a generation still
|
|
301
|
-
* within its acceptance window. Generations past that window are forgotten
|
|
302
|
-
* here — this is the one place that prunes, so the maps cannot grow with
|
|
303
|
-
* every seek of a long viewing.
|
|
304
|
-
*
|
|
305
|
-
* @param {number} [now]
|
|
306
|
-
* @returns {Set<string>}
|
|
307
|
-
*/
|
|
308
|
-
heldKeys(now = Date.now()) {
|
|
309
|
-
const held = new Set(this.#chosen.values());
|
|
310
|
-
for (const { outputKey } of this.#accepted.values()) {
|
|
311
|
-
held.add(outputKey);
|
|
312
|
-
}
|
|
313
|
-
for (const generation of [...this.#given.keys()]) {
|
|
314
|
-
if (this.accepts(generation, now)) {
|
|
315
|
-
for (const outputKey of this.#given.get(generation).values()) {
|
|
316
|
-
held.add(outputKey);
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
this.#given.delete(generation);
|
|
320
|
-
this.#leftAt.delete(generation);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return held;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* This viewer has gone: nothing they asked for holds anything any more.
|
|
328
|
-
*
|
|
329
|
-
* An accepted response is dropped with the rest, because the connection it
|
|
330
|
-
* was being sent over is what went.
|
|
331
|
-
*
|
|
332
|
-
* @returns {void}
|
|
333
|
-
*/
|
|
334
|
-
clear() {
|
|
335
|
-
this.#chosen.clear();
|
|
336
|
-
this.#given.clear();
|
|
337
|
-
this.#leftAt.clear();
|
|
338
|
-
this.#accepted.clear();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Which output answers THIS viewer, and for how long that stands.
|
|
3
|
+
*
|
|
4
|
+
* Until now the answer was one record per (file, asked height), shared by
|
|
5
|
+
* everybody watching the file: `OutputCatalog.servedBy`. That is the very thing
|
|
6
|
+
* roadmap item 97 forbids — a viewer on a thin link would have moved the output
|
|
7
|
+
* of a viewer on a thick one, because both read the same entry. So the answer
|
|
8
|
+
* belongs to the viewer, and this is where a viewer keeps it.
|
|
9
|
+
*
|
|
10
|
+
* FIVE FACTS, and each is here because something reads it:
|
|
11
|
+
*
|
|
12
|
+
* 1. **the chosen output** per asked height — what the suitability rule
|
|
13
|
+
* decided, and the only thing that may change it;
|
|
14
|
+
* 2. the output being PREPARED — `Viewer.warmingVariantId`, which already
|
|
15
|
+
* exists and stays where it is;
|
|
16
|
+
* 3. **this viewer's own generation**, raised when THIS viewer seeks. Not the
|
|
17
|
+
* output's `waitEpoch`: that one is shared, rises for reasons that are not a
|
|
18
|
+
* seek, and is moved by other people's seeks on a shared output. A request
|
|
19
|
+
* CARRIES the generation it was made in, because a request that arrives late
|
|
20
|
+
* would otherwise be read against the current one and answered as though it
|
|
21
|
+
* had just been made;
|
|
22
|
+
* 4. **what was given**: which output answered segment N in generation G. This
|
|
23
|
+
* is what makes a repeat of a request get the same output rather than being
|
|
24
|
+
* decided again;
|
|
25
|
+
* 5. **what was accepted**: the assignments whose response has already begun.
|
|
26
|
+
*
|
|
27
|
+
* TWO DEADLINES, NOT ONE. An earlier draft dropped an assignment the moment its
|
|
28
|
+
* generation was left behind, which contradicts finishing a response that is
|
|
29
|
+
* still being sent: the bytes would still be going out while the reason for
|
|
30
|
+
* holding their output had gone.
|
|
31
|
+
*
|
|
32
|
+
* 1. a generation that has been left behind stops accepting NEW requests after
|
|
33
|
+
* `acceptWindowMs`. What that window should be is NOT derived here — the
|
|
34
|
+
* player's retry policy is overridden by the product's own page and the
|
|
35
|
+
* native path has none at all — so it is given explicitly and checked on
|
|
36
|
+
* what it is given;
|
|
37
|
+
* 2. an ACCEPTED request keeps its assignment until its response finishes or is
|
|
38
|
+
* released, whatever generation it belongs to and however long that takes.
|
|
39
|
+
*
|
|
40
|
+
* An output is kept while any assignment of it stands: `heldKeys()` is asked
|
|
41
|
+
* before an output is let go, rather than the assignments being discovered
|
|
42
|
+
* afterwards.
|
|
43
|
+
*
|
|
44
|
+
* LIMIT, stated rather than left to be found: an assignment is keyed by the
|
|
45
|
+
* HEIGHT asked for, so it speaks for the picture and its steps. A soundtrack is
|
|
46
|
+
* not a height and records nothing in `given`; item 97 is about the picture.
|
|
47
|
+
* A soundtrack's response is still held by `accept` while it is being sent,
|
|
48
|
+
* and a request for a soundtrack made in a viewing that was left is still
|
|
49
|
+
* refused — neither of those is about heights.
|
|
50
|
+
*
|
|
51
|
+
* WHERE EACH FACT IS READ AND WRITTEN (roadmap item 97, step 9):
|
|
52
|
+
*
|
|
53
|
+
* 1. the generation — told by the seek route (`ViewerRequests.requestSeek`),
|
|
54
|
+
* checked by every file route BEFORE anything else the route does
|
|
55
|
+
* (`routes/transcode/session-file/get.js`, `refusedAsStale`);
|
|
56
|
+
* 2. `given` — written and read by the step route
|
|
57
|
+
* (`Renditions.resolveVariantFile`) under the height asked for and the
|
|
58
|
+
* segment, −1 for the init; written by the picture's own route under the
|
|
59
|
+
* height that output is named after (`ViewerRequests.noteAnsweredDirectly`);
|
|
60
|
+
* 3. `accepted` — held from the start of a response to its `finish`, `close`
|
|
61
|
+
* or `error` (`serveSessionFile`);
|
|
62
|
+
* 4. `heldKeys` — asked before an output is disposed (`Viewers.stillNeeded`,
|
|
63
|
+
* `Viewers.assignmentsHold`);
|
|
64
|
+
* 5. `chosen` — written only by the suitability rule, for an output that suits
|
|
65
|
+
* this viewer (`Renditions`, through `viewer/choices.js`), and read first
|
|
66
|
+
* for every NEW address after `given` (step 11). The shared
|
|
67
|
+
* `OutputCatalog.servedBy` record it replaced is gone: one viewer's answer is
|
|
68
|
+
* no longer every viewer's. An answer of "nothing suits" is never recorded
|
|
69
|
+
* here — it is answered to the request as `output-unavailable`.
|
|
70
|
+
*
|
|
71
|
+
* ORDER OF ANSWERING, and why: `given` before `chosen` before the rule. A
|
|
72
|
+
* repeat of an address must get what answered it, even after `chosen` has
|
|
73
|
+
* moved to another output of the same height — the player holds that output's
|
|
74
|
+
* header for it. When the output behind `given` has gone, the address is not
|
|
75
|
+
* decided again as if new: it is served from the stored piece, or by an output
|
|
76
|
+
* whose header is proven compatible, or answered `assignment-lost`.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The address of one thing asked for: a height of the picture and a segment.
|
|
81
|
+
*
|
|
82
|
+
* @param {number} askedHeight
|
|
83
|
+
* @param {number} segmentIndex
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function addressOf(askedHeight, segmentIndex) {
|
|
87
|
+
return `${askedHeight}:${segmentIndex}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class Assignments {
|
|
91
|
+
/** Chosen output per asked height. @type {Map<number, string>} */
|
|
92
|
+
#chosen = new Map();
|
|
93
|
+
/** Generation → (address → output key). @type {Map<number, Map<string, string>>} */
|
|
94
|
+
#given = new Map();
|
|
95
|
+
/** Generation → when it stopped being the current one. @type {Map<number, number>} */
|
|
96
|
+
#leftAt = new Map();
|
|
97
|
+
/** Accepted responses, by their own token. @type {Map<string, { outputKey: string }>} */
|
|
98
|
+
#accepted = new Map();
|
|
99
|
+
#generation = 0;
|
|
100
|
+
#nextToken = 0;
|
|
101
|
+
#acceptWindowMs;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {{ acceptWindowMs: number }} limits - Explicitly given; see the
|
|
105
|
+
* first deadline above for why it is not derived.
|
|
106
|
+
*/
|
|
107
|
+
constructor({ acceptWindowMs }) {
|
|
108
|
+
if (!Number.isFinite(acceptWindowMs) || acceptWindowMs < 0) {
|
|
109
|
+
throw new TypeError("Assignments: acceptWindowMs must be a number of milliseconds");
|
|
110
|
+
}
|
|
111
|
+
this.#acceptWindowMs = acceptWindowMs;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The generation a request made now belongs to. @returns {number} */
|
|
115
|
+
get generation() {
|
|
116
|
+
return this.#generation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The page states which viewing its requests now belong to.
|
|
121
|
+
*
|
|
122
|
+
* TOLD, NOT COUNTED. The page is where a seek begins and where every request
|
|
123
|
+
* is built, so it is the only place that can stamp a request with the viewing
|
|
124
|
+
* it belongs to; a number counted here would be a second owner of one fact
|
|
125
|
+
* and the two would disagree exactly when a request and a seek cross.
|
|
126
|
+
*
|
|
127
|
+
* Monotonic — a lower number is ignored, so a reordered or repeated message
|
|
128
|
+
* cannot take the viewer back. The generation being left is not dropped: it
|
|
129
|
+
* is marked with the moment it stopped being current, which is what its
|
|
130
|
+
* acceptance window is measured from.
|
|
131
|
+
*
|
|
132
|
+
* @param {number} generation
|
|
133
|
+
* @param {number} [now]
|
|
134
|
+
* @returns {number} The generation now current.
|
|
135
|
+
*/
|
|
136
|
+
statedGeneration(generation, now = Date.now()) {
|
|
137
|
+
if (!Number.isInteger(generation) || generation <= this.#generation) {
|
|
138
|
+
return this.#generation;
|
|
139
|
+
}
|
|
140
|
+
this.#leftAt.set(this.#generation, now);
|
|
141
|
+
this.#generation = generation;
|
|
142
|
+
return this.#generation;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether a request made in `generation` may still be answered.
|
|
147
|
+
*
|
|
148
|
+
* A generation AHEAD of the one stated is accepted, and that is not
|
|
149
|
+
* laxness: the page raises its own number before it sends anything, so a
|
|
150
|
+
* segment request can overtake the seek that announces the new viewing. The
|
|
151
|
+
* page is the owner, so being behind it means this side has not heard yet.
|
|
152
|
+
*
|
|
153
|
+
* A request that states nothing at all is accepted — a transport with no
|
|
154
|
+
* loader of ours builds no such parameter, and it has one viewing.
|
|
155
|
+
*
|
|
156
|
+
* @param {number} generation
|
|
157
|
+
* @param {number} [now]
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
accepts(generation, now = Date.now()) {
|
|
161
|
+
if (!Number.isInteger(generation) || generation >= this.#generation) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
const left = this.#leftAt.get(generation);
|
|
165
|
+
return left !== undefined && now - left <= this.#acceptWindowMs;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The output chosen for this height, or an empty string. @returns {string} */
|
|
169
|
+
chosenFor(askedHeight) {
|
|
170
|
+
return this.#chosen.get(askedHeight) ?? "";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The suitability rule has decided. This is the ONLY thing that moves a
|
|
175
|
+
* choice: a request never decides one for itself.
|
|
176
|
+
*
|
|
177
|
+
* @param {number} askedHeight
|
|
178
|
+
* @param {string} outputKey
|
|
179
|
+
* @returns {void}
|
|
180
|
+
*/
|
|
181
|
+
choose(askedHeight, outputKey) {
|
|
182
|
+
if (!(askedHeight > 0) || !outputKey) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.#chosen.set(askedHeight, outputKey);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The asked heights whose choice is this output.
|
|
190
|
+
*
|
|
191
|
+
* A step is NAMED after the height it produces and CHOSEN under the height
|
|
192
|
+
* the player asked for, and the two can differ. A move to another output of
|
|
193
|
+
* the same height has to replace the choice under the height the player
|
|
194
|
+
* asks for, so it is read back from the choice rather than from the name.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} outputKey
|
|
197
|
+
* @returns {number[]}
|
|
198
|
+
*/
|
|
199
|
+
heightsChosenAs(outputKey) {
|
|
200
|
+
const heights = [];
|
|
201
|
+
for (const [askedHeight, key] of this.#chosen) {
|
|
202
|
+
if (key === outputKey) {
|
|
203
|
+
heights.push(askedHeight);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return heights;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The highest segment given at this height in this generation, or −1 when
|
|
211
|
+
* nothing but perhaps the init was.
|
|
212
|
+
*
|
|
213
|
+
* What the player asks for next is the segment after it: hls.js fetches a
|
|
214
|
+
* level's segments in order within one viewing. Nothing given yet is the
|
|
215
|
+
* state straight after a seek, and then the next is where the viewer stands,
|
|
216
|
+
* which only the caller can turn into a number.
|
|
217
|
+
*
|
|
218
|
+
* @param {number} generation
|
|
219
|
+
* @param {number} askedHeight
|
|
220
|
+
* @returns {number}
|
|
221
|
+
*/
|
|
222
|
+
highestGiven(generation, askedHeight) {
|
|
223
|
+
const byAddress = this.#given.get(generation);
|
|
224
|
+
if (!byAddress) {
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
const prefix = `${askedHeight}:`;
|
|
228
|
+
let highest = -1;
|
|
229
|
+
for (const address of byAddress.keys()) {
|
|
230
|
+
if (!address.startsWith(prefix)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const index = Number(address.slice(prefix.length));
|
|
234
|
+
if (Number.isInteger(index) && index > highest) {
|
|
235
|
+
highest = index;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return highest;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* What answered this segment in this generation, or an empty string.
|
|
243
|
+
*
|
|
244
|
+
* @param {number} generation
|
|
245
|
+
* @param {number} askedHeight
|
|
246
|
+
* @param {number} segmentIndex
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
givenFor(generation, askedHeight, segmentIndex) {
|
|
250
|
+
return this.#given.get(generation)?.get(addressOf(askedHeight, segmentIndex)) ?? "";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Record what answered, so a repeat of the same request is answered the same
|
|
255
|
+
* way rather than decided again.
|
|
256
|
+
*
|
|
257
|
+
* @param {number} generation
|
|
258
|
+
* @param {number} askedHeight
|
|
259
|
+
* @param {number} segmentIndex
|
|
260
|
+
* @param {string} outputKey
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
give(generation, askedHeight, segmentIndex, outputKey) {
|
|
264
|
+
if (!outputKey || !(askedHeight > 0) || !Number.isInteger(segmentIndex)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const byAddress = this.#given.get(generation) ?? new Map();
|
|
268
|
+
byAddress.set(addressOf(askedHeight, segmentIndex), outputKey);
|
|
269
|
+
this.#given.set(generation, byAddress);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A response has begun. The token returned releases it, and until it is
|
|
274
|
+
* released this output is held whatever else happens to its generation.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} outputKey
|
|
277
|
+
* @returns {string} token
|
|
278
|
+
*/
|
|
279
|
+
accept(outputKey) {
|
|
280
|
+
this.#nextToken += 1;
|
|
281
|
+
const token = `a${this.#nextToken}`;
|
|
282
|
+
this.#accepted.set(token, { outputKey });
|
|
283
|
+
return token;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The response is finished or cancelled.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} token
|
|
290
|
+
* @returns {void}
|
|
291
|
+
*/
|
|
292
|
+
release(token) {
|
|
293
|
+
this.#accepted.delete(token);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Every output this viewer's assignments hold, asked BEFORE one is let go.
|
|
298
|
+
*
|
|
299
|
+
* Three sources, and each is a reason on its own: a choice that stands, a
|
|
300
|
+
* response still being sent, and what was given inside a generation still
|
|
301
|
+
* within its acceptance window. Generations past that window are forgotten
|
|
302
|
+
* here — this is the one place that prunes, so the maps cannot grow with
|
|
303
|
+
* every seek of a long viewing.
|
|
304
|
+
*
|
|
305
|
+
* @param {number} [now]
|
|
306
|
+
* @returns {Set<string>}
|
|
307
|
+
*/
|
|
308
|
+
heldKeys(now = Date.now()) {
|
|
309
|
+
const held = new Set(this.#chosen.values());
|
|
310
|
+
for (const { outputKey } of this.#accepted.values()) {
|
|
311
|
+
held.add(outputKey);
|
|
312
|
+
}
|
|
313
|
+
for (const generation of [...this.#given.keys()]) {
|
|
314
|
+
if (this.accepts(generation, now)) {
|
|
315
|
+
for (const outputKey of this.#given.get(generation).values()) {
|
|
316
|
+
held.add(outputKey);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
this.#given.delete(generation);
|
|
320
|
+
this.#leftAt.delete(generation);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return held;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* This viewer has gone: nothing they asked for holds anything any more.
|
|
328
|
+
*
|
|
329
|
+
* An accepted response is dropped with the rest, because the connection it
|
|
330
|
+
* was being sent over is what went.
|
|
331
|
+
*
|
|
332
|
+
* @returns {void}
|
|
333
|
+
*/
|
|
334
|
+
clear() {
|
|
335
|
+
this.#chosen.clear();
|
|
336
|
+
this.#given.clear();
|
|
337
|
+
this.#leftAt.clear();
|
|
338
|
+
this.#accepted.clear();
|
|
339
|
+
}
|
|
340
|
+
}
|