@torrent-tv/proxy 2.87.20 → 2.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +8 -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/biome.json +411 -411
- package/docs/browser-proxy-contract.md +133 -133
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +106 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +173 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +758 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1141 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +315 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +515 -515
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +245 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +696 -696
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1908
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -0
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -0
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +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 -365
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +12 -2
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
package/test/resume-warm.test.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Where a viewer's resume position falls in the file, in bytes.
|
|
3
|
-
*
|
|
4
|
-
* The warm-up fetches a file's two edges, because that is what the codec probe
|
|
5
|
-
* reads. The region the VIEWER will resume at was asked for by nobody until the
|
|
6
|
-
* encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
|
|
7
|
-
* cold torrent, and the piece it then needed took another 46.3 s to arrive.
|
|
8
|
-
* Turning the position into an offset is the only arithmetic in that path, so it
|
|
9
|
-
* is the only part with anything to get wrong.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
|
|
15
|
-
import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
|
|
16
|
-
|
|
17
|
-
test("a position halfway through a film is halfway through its file", () => {
|
|
18
|
-
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("the field case lands where the encoder went looking", () => {
|
|
22
|
-
// The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
|
|
23
|
-
// read that blocked was at 63 MB.
|
|
24
|
-
const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
|
|
25
|
-
const megabytes = at / (1024 * 1024);
|
|
26
|
-
assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("a position past the end reads the end, rather than past it", () => {
|
|
30
|
-
const length = 1000;
|
|
31
|
-
const at = resumeByteOffset(length, 100, 10_000);
|
|
32
|
-
assert.equal(at, length - 1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("nothing is known, nothing is guessed", () => {
|
|
36
|
-
assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
|
|
37
|
-
assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
|
|
38
|
-
assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
|
|
39
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a viewer's resume position falls in the file, in bytes.
|
|
3
|
+
*
|
|
4
|
+
* The warm-up fetches a file's two edges, because that is what the codec probe
|
|
5
|
+
* reads. The region the VIEWER will resume at was asked for by nobody until the
|
|
6
|
+
* encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
|
|
7
|
+
* cold torrent, and the piece it then needed took another 46.3 s to arrive.
|
|
8
|
+
* Turning the position into an offset is the only arithmetic in that path, so it
|
|
9
|
+
* is the only part with anything to get wrong.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
|
|
15
|
+
import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
|
|
16
|
+
|
|
17
|
+
test("a position halfway through a film is halfway through its file", () => {
|
|
18
|
+
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("the field case lands where the encoder went looking", () => {
|
|
22
|
+
// The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
|
|
23
|
+
// read that blocked was at 63 MB.
|
|
24
|
+
const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
|
|
25
|
+
const megabytes = at / (1024 * 1024);
|
|
26
|
+
assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a position past the end reads the end, rather than past it", () => {
|
|
30
|
+
const length = 1000;
|
|
31
|
+
const at = resumeByteOffset(length, 100, 10_000);
|
|
32
|
+
assert.equal(at, length - 1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("nothing is known, nothing is guessed", () => {
|
|
36
|
+
assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
|
|
37
|
+
assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
|
|
38
|
+
assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
|
|
39
|
+
});
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The drawing cannot disagree with the table.
|
|
3
|
-
*
|
|
4
|
-
* `docs/encode-run-state.md` is generated from `services/encode/encode-run-state.js`.
|
|
5
|
-
* A picture kept beside the code drifts — this repository has the receipts —
|
|
6
|
-
* so the check is mechanical: regenerate it and compare. A behavioural change
|
|
7
|
-
* to the table that forgets `npm run graph` fails here, in the same commit.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import { GRAPH_DOC_PATH, renderRunGraphMarkdown } from "../scripts/render-run-graph.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Line endings are not part of the comparison. Git is configured with
|
|
18
|
-
* `autocrlf=true` on the machine this is developed on, so the same file is LF
|
|
19
|
-
* in the index and CRLF in the working tree — and a test failing on that would
|
|
20
|
-
* point at the generator, whose output is always LF, and be unfixable by the
|
|
21
|
-
* remedy it suggests.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} text
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
function withoutLineEndings(text) {
|
|
27
|
-
return text.replace(/\r\n/g, "\n");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
test("the committed picture is the one the table produces", () => {
|
|
31
|
-
const committed = readFileSync(GRAPH_DOC_PATH, "utf8");
|
|
32
|
-
assert.equal(
|
|
33
|
-
withoutLineEndings(committed),
|
|
34
|
-
withoutLineEndings(renderRunGraphMarkdown()),
|
|
35
|
-
"docs/encode-run-state.md is out of date — run `npm run graph` and commit the result"
|
|
36
|
-
);
|
|
37
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The drawing cannot disagree with the table.
|
|
3
|
+
*
|
|
4
|
+
* `docs/encode-run-state.md` is generated from `services/encode/encode-run-state.js`.
|
|
5
|
+
* A picture kept beside the code drifts — this repository has the receipts —
|
|
6
|
+
* so the check is mechanical: regenerate it and compare. A behavioural change
|
|
7
|
+
* to the table that forgets `npm run graph` fails here, in the same commit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { GRAPH_DOC_PATH, renderRunGraphMarkdown } from "../scripts/render-run-graph.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Line endings are not part of the comparison. Git is configured with
|
|
18
|
+
* `autocrlf=true` on the machine this is developed on, so the same file is LF
|
|
19
|
+
* in the index and CRLF in the working tree — and a test failing on that would
|
|
20
|
+
* point at the generator, whose output is always LF, and be unfixable by the
|
|
21
|
+
* remedy it suggests.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} text
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function withoutLineEndings(text) {
|
|
27
|
+
return text.replace(/\r\n/g, "\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("the committed picture is the one the table produces", () => {
|
|
31
|
+
const committed = readFileSync(GRAPH_DOC_PATH, "utf8");
|
|
32
|
+
assert.equal(
|
|
33
|
+
withoutLineEndings(committed),
|
|
34
|
+
withoutLineEndings(renderRunGraphMarkdown()),
|
|
35
|
+
"docs/encode-run-state.md is out of date — run `npm run graph` and commit the result"
|
|
36
|
+
);
|
|
37
|
+
});
|
package/test/run-inputs.test.js
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What an encoder is told to read.
|
|
3
|
-
*
|
|
4
|
-
* The encoder's own read of its source is the one read that tracks where the
|
|
5
|
-
* viewer is (`reader=playback`), and the only one sized in seconds of playback
|
|
6
|
-
* (`windowBytes`). Both travel on the input address. When the address stopped
|
|
7
|
-
* being kept on the session and started being built per run, both were left
|
|
8
|
-
* off it: every encoder read looked to the stream route like a probe, and the
|
|
9
|
-
* read window fell back to the reader's default.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { encoderInputs } from "../services/encode/run-inputs.js";
|
|
15
|
-
|
|
16
|
-
const BASE = "http://127.0.0.1:9090";
|
|
17
|
-
|
|
18
|
-
function file(fileIndex) {
|
|
19
|
-
return {
|
|
20
|
-
fileIndex,
|
|
21
|
-
streamUrl(baseUrl, { sessionId = "" } = {}) {
|
|
22
|
-
const url = new URL("/stream", `${baseUrl}/`);
|
|
23
|
-
url.searchParams.set("fileIndex", String(fileIndex));
|
|
24
|
-
if (sessionId) {
|
|
25
|
-
url.searchParams.set("session", sessionId);
|
|
26
|
-
}
|
|
27
|
-
return url;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
test("the picture's read says it is the playback read, with its window", () => {
|
|
33
|
-
const picture = file(0);
|
|
34
|
-
const { inputUrl } = encoderInputs({
|
|
35
|
-
picture,
|
|
36
|
-
soundtrack: picture,
|
|
37
|
-
carries: "video-only",
|
|
38
|
-
audioSeparate: true,
|
|
39
|
-
sessionId: "aaaaaaaabbbbcccc",
|
|
40
|
-
readWindowBytes: 32_000_000,
|
|
41
|
-
baseUrl: BASE
|
|
42
|
-
});
|
|
43
|
-
const url = new URL(inputUrl);
|
|
44
|
-
assert.equal(url.searchParams.get("reader"), "playback");
|
|
45
|
-
assert.equal(url.searchParams.get("windowBytes"), "32000000");
|
|
46
|
-
assert.equal(url.searchParams.get("session"), "aaaaaaaabbbbcccc");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("with no window measured the read states neither parameter", () => {
|
|
50
|
-
const picture = file(0);
|
|
51
|
-
const { inputUrl } = encoderInputs({
|
|
52
|
-
picture,
|
|
53
|
-
soundtrack: picture,
|
|
54
|
-
carries: "muxed",
|
|
55
|
-
audioSeparate: false,
|
|
56
|
-
sessionId: "aaaaaaaabbbbcccc",
|
|
57
|
-
readWindowBytes: 0,
|
|
58
|
-
baseUrl: BASE
|
|
59
|
-
});
|
|
60
|
-
const url = new URL(inputUrl);
|
|
61
|
-
assert.equal(url.searchParams.get("windowBytes"), null);
|
|
62
|
-
assert.equal(url.searchParams.get("reader"), null, "exactly as before: both are set together or neither");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("a soundtrack in its own file is read directly by its rendition, and is a second input only when muxed", () => {
|
|
66
|
-
const picture = file(0);
|
|
67
|
-
const dub = file(3);
|
|
68
|
-
const rendition = encoderInputs({
|
|
69
|
-
picture, soundtrack: dub, carries: "audio-only", audioSeparate: true,
|
|
70
|
-
sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
|
|
71
|
-
});
|
|
72
|
-
assert.equal(new URL(rendition.inputUrl).searchParams.get("fileIndex"), "3");
|
|
73
|
-
assert.equal(rendition.audioInputUrl, "");
|
|
74
|
-
const muxed = encoderInputs({
|
|
75
|
-
picture, soundtrack: dub, carries: "muxed", audioSeparate: false,
|
|
76
|
-
sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
|
|
77
|
-
});
|
|
78
|
-
assert.equal(new URL(muxed.inputUrl).searchParams.get("fileIndex"), "0");
|
|
79
|
-
assert.equal(new URL(muxed.audioInputUrl).searchParams.get("fileIndex"), "3");
|
|
80
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What an encoder is told to read.
|
|
3
|
+
*
|
|
4
|
+
* The encoder's own read of its source is the one read that tracks where the
|
|
5
|
+
* viewer is (`reader=playback`), and the only one sized in seconds of playback
|
|
6
|
+
* (`windowBytes`). Both travel on the input address. When the address stopped
|
|
7
|
+
* being kept on the session and started being built per run, both were left
|
|
8
|
+
* off it: every encoder read looked to the stream route like a probe, and the
|
|
9
|
+
* read window fell back to the reader's default.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { encoderInputs } from "../services/encode/run-inputs.js";
|
|
15
|
+
|
|
16
|
+
const BASE = "http://127.0.0.1:9090";
|
|
17
|
+
|
|
18
|
+
function file(fileIndex) {
|
|
19
|
+
return {
|
|
20
|
+
fileIndex,
|
|
21
|
+
streamUrl(baseUrl, { sessionId = "" } = {}) {
|
|
22
|
+
const url = new URL("/stream", `${baseUrl}/`);
|
|
23
|
+
url.searchParams.set("fileIndex", String(fileIndex));
|
|
24
|
+
if (sessionId) {
|
|
25
|
+
url.searchParams.set("session", sessionId);
|
|
26
|
+
}
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("the picture's read says it is the playback read, with its window", () => {
|
|
33
|
+
const picture = file(0);
|
|
34
|
+
const { inputUrl } = encoderInputs({
|
|
35
|
+
picture,
|
|
36
|
+
soundtrack: picture,
|
|
37
|
+
carries: "video-only",
|
|
38
|
+
audioSeparate: true,
|
|
39
|
+
sessionId: "aaaaaaaabbbbcccc",
|
|
40
|
+
readWindowBytes: 32_000_000,
|
|
41
|
+
baseUrl: BASE
|
|
42
|
+
});
|
|
43
|
+
const url = new URL(inputUrl);
|
|
44
|
+
assert.equal(url.searchParams.get("reader"), "playback");
|
|
45
|
+
assert.equal(url.searchParams.get("windowBytes"), "32000000");
|
|
46
|
+
assert.equal(url.searchParams.get("session"), "aaaaaaaabbbbcccc");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("with no window measured the read states neither parameter", () => {
|
|
50
|
+
const picture = file(0);
|
|
51
|
+
const { inputUrl } = encoderInputs({
|
|
52
|
+
picture,
|
|
53
|
+
soundtrack: picture,
|
|
54
|
+
carries: "muxed",
|
|
55
|
+
audioSeparate: false,
|
|
56
|
+
sessionId: "aaaaaaaabbbbcccc",
|
|
57
|
+
readWindowBytes: 0,
|
|
58
|
+
baseUrl: BASE
|
|
59
|
+
});
|
|
60
|
+
const url = new URL(inputUrl);
|
|
61
|
+
assert.equal(url.searchParams.get("windowBytes"), null);
|
|
62
|
+
assert.equal(url.searchParams.get("reader"), null, "exactly as before: both are set together or neither");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a soundtrack in its own file is read directly by its rendition, and is a second input only when muxed", () => {
|
|
66
|
+
const picture = file(0);
|
|
67
|
+
const dub = file(3);
|
|
68
|
+
const rendition = encoderInputs({
|
|
69
|
+
picture, soundtrack: dub, carries: "audio-only", audioSeparate: true,
|
|
70
|
+
sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
|
|
71
|
+
});
|
|
72
|
+
assert.equal(new URL(rendition.inputUrl).searchParams.get("fileIndex"), "3");
|
|
73
|
+
assert.equal(rendition.audioInputUrl, "");
|
|
74
|
+
const muxed = encoderInputs({
|
|
75
|
+
picture, soundtrack: dub, carries: "muxed", audioSeparate: false,
|
|
76
|
+
sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
|
|
77
|
+
});
|
|
78
|
+
assert.equal(new URL(muxed.inputUrl).searchParams.get("fileIndex"), "0");
|
|
79
|
+
assert.equal(new URL(muxed.audioInputUrl).searchParams.get("fileIndex"), "3");
|
|
80
|
+
});
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How far a run may work, and who decides it.
|
|
3
|
-
*
|
|
4
|
-
* These cases used to be asked of `planRunInterval`, a second authority inside
|
|
5
|
-
* the session manager that answered by its own rules: it walked the whole track
|
|
6
|
-
* for the first free number, MOVED the start there, and counted every live run
|
|
7
|
-
* as claiming up to `head + look-ahead`. It contradicted the plan directly, and
|
|
8
|
-
* the two together produced the field oscillation of 2026-09-05 — the plan
|
|
9
|
-
* commanded a start at #46, this moved it to #78, the plan killed the run for
|
|
10
|
-
* standing outside the window it had asked for, and the same start was
|
|
11
|
-
* commanded again, 350-700ms per cycle, no segment ever produced, the viewer's
|
|
12
|
-
* picture stopped for 125 seconds.
|
|
13
|
-
*
|
|
14
|
-
* It is gone. WHERE a run starts is the plan's decision and nothing moves it;
|
|
15
|
-
* HOW FAR it may work is a fact of the one coverage map, and that is what these
|
|
16
|
-
* cases now ask. The map is the same object the plan reads, so there is no
|
|
17
|
-
* second set of rules for the two to disagree about.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
23
|
-
|
|
24
|
-
/** A stand-in for a run: the map identifies one by being it. */
|
|
25
|
-
function run(name) {
|
|
26
|
-
return { name };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("with nothing made, a run gets everything from where it was asked", () => {
|
|
30
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
31
|
-
|
|
32
|
-
assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
|
|
33
|
-
assert.equal(coverage.firstGapFrom(0), 0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("a run stops before material that is already made", () => {
|
|
37
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
38
|
-
coverage.setReady([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
|
39
|
-
|
|
40
|
-
assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("a run stops before a stretch another live run was given", () => {
|
|
44
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
45
|
-
const other = run("other");
|
|
46
|
-
coverage.claim(other, 40, 60);
|
|
47
|
-
|
|
48
|
-
assert.equal(coverage.freeRunFrom(0), 40, "0..39, and the other run's claim begins at 40");
|
|
49
|
-
assert.equal(coverage.firstGapFrom(45), 61, "the first thing nobody holds after it");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("a run's own claim does not hold it back", () => {
|
|
53
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
54
|
-
const mine = run("mine");
|
|
55
|
-
coverage.claim(mine, 40, 60);
|
|
56
|
-
|
|
57
|
-
assert.equal(
|
|
58
|
-
coverage.freeRunFrom(40, mine),
|
|
59
|
-
60,
|
|
60
|
-
"asked by the run that holds it, the stretch is its own to work through"
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("a run that has ended holds nothing back", () => {
|
|
65
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
66
|
-
const dead = run("dead");
|
|
67
|
-
coverage.claim(dead, 40, 60);
|
|
68
|
-
coverage.release(dead);
|
|
69
|
-
|
|
70
|
-
assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("what a dead run DID finish stays made", () => {
|
|
74
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
75
|
-
const dead = run("dead");
|
|
76
|
-
coverage.claim(dead, 40, 60);
|
|
77
|
-
coverage.setReady([40, 41, 42]);
|
|
78
|
-
coverage.release(dead);
|
|
79
|
-
|
|
80
|
-
assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("nothing left to make is answered with nothing", () => {
|
|
84
|
-
const coverage = new CoverageMap({ segmentCount: 5 });
|
|
85
|
-
coverage.setReady([0, 1, 2, 3, 4]);
|
|
86
|
-
|
|
87
|
-
assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("a run without an end does not take the film away from a viewer further in", () => {
|
|
91
|
-
// The case that used to need a second authority's `head + look-ahead` guess.
|
|
92
|
-
// A run's claim is now the stretch the plan GAVE it, so a viewer opening the
|
|
93
|
-
// same film in the middle finds their own position free.
|
|
94
|
-
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
95
|
-
const first = run("first");
|
|
96
|
-
coverage.claim(first, 0, 499);
|
|
97
|
-
|
|
98
|
-
assert.equal(coverage.firstGapFrom(500), 500, "the second viewer's own position is free");
|
|
99
|
-
assert.equal(coverage.freeRunFrom(500), 500, "and everything from there to the end");
|
|
100
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file How far a run may work, and who decides it.
|
|
3
|
+
*
|
|
4
|
+
* These cases used to be asked of `planRunInterval`, a second authority inside
|
|
5
|
+
* the session manager that answered by its own rules: it walked the whole track
|
|
6
|
+
* for the first free number, MOVED the start there, and counted every live run
|
|
7
|
+
* as claiming up to `head + look-ahead`. It contradicted the plan directly, and
|
|
8
|
+
* the two together produced the field oscillation of 2026-09-05 — the plan
|
|
9
|
+
* commanded a start at #46, this moved it to #78, the plan killed the run for
|
|
10
|
+
* standing outside the window it had asked for, and the same start was
|
|
11
|
+
* commanded again, 350-700ms per cycle, no segment ever produced, the viewer's
|
|
12
|
+
* picture stopped for 125 seconds.
|
|
13
|
+
*
|
|
14
|
+
* It is gone. WHERE a run starts is the plan's decision and nothing moves it;
|
|
15
|
+
* HOW FAR it may work is a fact of the one coverage map, and that is what these
|
|
16
|
+
* cases now ask. The map is the same object the plan reads, so there is no
|
|
17
|
+
* second set of rules for the two to disagree about.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
23
|
+
|
|
24
|
+
/** A stand-in for a run: the map identifies one by being it. */
|
|
25
|
+
function run(name) {
|
|
26
|
+
return { name };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("with nothing made, a run gets everything from where it was asked", () => {
|
|
30
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
31
|
+
|
|
32
|
+
assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
|
|
33
|
+
assert.equal(coverage.firstGapFrom(0), 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a run stops before material that is already made", () => {
|
|
37
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
38
|
+
coverage.setReady([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
|
39
|
+
|
|
40
|
+
assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a run stops before a stretch another live run was given", () => {
|
|
44
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
45
|
+
const other = run("other");
|
|
46
|
+
coverage.claim(other, 40, 60);
|
|
47
|
+
|
|
48
|
+
assert.equal(coverage.freeRunFrom(0), 40, "0..39, and the other run's claim begins at 40");
|
|
49
|
+
assert.equal(coverage.firstGapFrom(45), 61, "the first thing nobody holds after it");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a run's own claim does not hold it back", () => {
|
|
53
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
54
|
+
const mine = run("mine");
|
|
55
|
+
coverage.claim(mine, 40, 60);
|
|
56
|
+
|
|
57
|
+
assert.equal(
|
|
58
|
+
coverage.freeRunFrom(40, mine),
|
|
59
|
+
60,
|
|
60
|
+
"asked by the run that holds it, the stretch is its own to work through"
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a run that has ended holds nothing back", () => {
|
|
65
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
66
|
+
const dead = run("dead");
|
|
67
|
+
coverage.claim(dead, 40, 60);
|
|
68
|
+
coverage.release(dead);
|
|
69
|
+
|
|
70
|
+
assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("what a dead run DID finish stays made", () => {
|
|
74
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
75
|
+
const dead = run("dead");
|
|
76
|
+
coverage.claim(dead, 40, 60);
|
|
77
|
+
coverage.setReady([40, 41, 42]);
|
|
78
|
+
coverage.release(dead);
|
|
79
|
+
|
|
80
|
+
assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("nothing left to make is answered with nothing", () => {
|
|
84
|
+
const coverage = new CoverageMap({ segmentCount: 5 });
|
|
85
|
+
coverage.setReady([0, 1, 2, 3, 4]);
|
|
86
|
+
|
|
87
|
+
assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a run without an end does not take the film away from a viewer further in", () => {
|
|
91
|
+
// The case that used to need a second authority's `head + look-ahead` guess.
|
|
92
|
+
// A run's claim is now the stretch the plan GAVE it, so a viewer opening the
|
|
93
|
+
// same film in the middle finds their own position free.
|
|
94
|
+
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
95
|
+
const first = run("first");
|
|
96
|
+
coverage.claim(first, 0, 499);
|
|
97
|
+
|
|
98
|
+
assert.equal(coverage.firstGapFrom(500), 500, "the second viewer's own position is free");
|
|
99
|
+
assert.equal(coverage.freeRunFrom(500), 500, "and everything from there to the end");
|
|
100
|
+
});
|