@torrent-tv/proxy 2.87.21 → 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.
Files changed (474) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +4 -0
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/browser-proxy-contract.md +133 -133
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +50 -50
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/files/get.js +106 -100
  87. package/routes/api/sources/post.js +36 -36
  88. package/routes/api/sources/stats/get.js +132 -132
  89. package/routes/api/sources/warm/post.js +217 -217
  90. package/routes/api/subtitles/get.js +230 -230
  91. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  92. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  93. package/routes/api/transcode-sessions/post.js +173 -173
  94. package/routes/api/transcode-sessions/progress/get.js +27 -27
  95. package/routes/api/transcode-sessions/release/post.js +41 -41
  96. package/routes/api/transcode-sessions/seek/post.js +63 -63
  97. package/routes/health/get.js +13 -13
  98. package/routes/healthz/get.js +13 -13
  99. package/routes/stream/get.js +380 -380
  100. package/routes/transcode/audio-file/get.js +64 -64
  101. package/routes/transcode/audio-warm/get.js +84 -84
  102. package/routes/transcode/session-file/get.js +52 -52
  103. package/routes/transcode/variant-file/get.js +95 -95
  104. package/routes/transcode/variant-warm/get.js +84 -84
  105. package/scripts/render-run-graph.js +173 -173
  106. package/server.js +725 -725
  107. package/services/encode/CoverageMap.js +428 -428
  108. package/services/encode/CushionReport.js +345 -345
  109. package/services/encode/EncodeAdmission.js +376 -376
  110. package/services/encode/EncodeOrchestrator.js +1217 -1217
  111. package/services/encode/EncodePlan.js +1366 -1366
  112. package/services/encode/EncodeRun.js +813 -813
  113. package/services/encode/EncodeRuns.js +1235 -1235
  114. package/services/encode/Encoder.js +142 -142
  115. package/services/encode/EncoderSelection.js +38 -38
  116. package/services/encode/LocalObservations.js +359 -359
  117. package/services/encode/NvencEncoder.js +50 -50
  118. package/services/encode/OutputOpening.js +758 -758
  119. package/services/encode/OutputTimes.js +579 -579
  120. package/services/encode/QsvEncoder.js +58 -58
  121. package/services/encode/Renditions.js +2184 -2184
  122. package/services/encode/RunProgress.js +84 -84
  123. package/services/encode/SegmentDemand.js +95 -95
  124. package/services/encode/SoftwareEncoder.js +121 -121
  125. package/services/encode/V4l2m2mEncoder.js +53 -53
  126. package/services/encode/VaapiEncoder.js +66 -66
  127. package/services/encode/args.js +416 -416
  128. package/services/encode/available-share.js +89 -89
  129. package/services/encode/calibration.js +361 -361
  130. package/services/encode/contention.js +128 -128
  131. package/services/encode/decode-cost-fit.js +363 -363
  132. package/services/encode/encode-exit.js +114 -114
  133. package/services/encode/encode-run-state.js +551 -551
  134. package/services/encode/encoder-readings.js +45 -45
  135. package/services/encode/fingerprint.js +322 -322
  136. package/services/encode/host-load.js +174 -174
  137. package/services/encode/hwaccel.js +1894 -1894
  138. package/services/encode/index.js +9 -9
  139. package/services/encode/learned-median.js +90 -90
  140. package/services/encode/output/EncodedOutput.js +57 -57
  141. package/services/encode/output/Output.js +131 -131
  142. package/services/encode/output/OutputCatalog.js +414 -414
  143. package/services/encode/output/OutputSpec.js +390 -390
  144. package/services/encode/output/Timeline.js +308 -308
  145. package/services/encode/output/cut-grid.js +107 -107
  146. package/services/encode/output/index.js +20 -20
  147. package/services/encode/output/ladder.js +32 -32
  148. package/services/encode/output/playlists.js +284 -284
  149. package/services/encode/output/rates.js +125 -125
  150. package/services/encode/output-key-format.js +21 -21
  151. package/services/encode/quality/EncodeCost.js +1141 -1141
  152. package/services/encode/quality/HostLoad.js +525 -525
  153. package/services/encode/quality/HostTimings.js +249 -249
  154. package/services/encode/quality/OutputQualityState.js +28 -28
  155. package/services/encode/quality/QualityController.js +935 -935
  156. package/services/encode/quality/QualityOffer.js +416 -416
  157. package/services/encode/quality/drain-threat.js +47 -47
  158. package/services/encode/quality/link-budget.js +254 -254
  159. package/services/encode/quality/output-format.js +315 -315
  160. package/services/encode/quality/serving-output.js +216 -216
  161. package/services/encode/quality/visible-rung.js +57 -57
  162. package/services/encode/run-budget.js +97 -97
  163. package/services/encode/run-command.js +754 -754
  164. package/services/encode/run-costs.js +155 -155
  165. package/services/encode/run-inputs.js +42 -42
  166. package/services/encode/segment-formats/fmp4.js +456 -456
  167. package/services/encode/segment-formats/index.js +85 -85
  168. package/services/encode/segment-formats/init-compat.js +324 -324
  169. package/services/encode/segment-formats/mp4-boxes.js +515 -515
  170. package/services/encode/segment-formats/mpegts.js +134 -134
  171. package/services/encode/start-stop-cost.js +178 -178
  172. package/services/encode/throughput.js +97 -97
  173. package/services/encode/torrent-cost.js +88 -88
  174. package/services/identity/installation-id.js +38 -38
  175. package/services/media/ContainerOrchestrator.js +149 -149
  176. package/services/media/KeyframeTables.js +316 -316
  177. package/services/media/SourceFile.js +298 -298
  178. package/services/media/SubtitleCues.js +520 -520
  179. package/services/media/SubtitleOrchestrator.js +167 -167
  180. package/services/media/audio-inventory.js +245 -245
  181. package/services/media/container/AviContainer.js +266 -266
  182. package/services/media/container/Container.js +603 -603
  183. package/services/media/container/ContainerFactory.js +56 -56
  184. package/services/media/container/KeyframeTable.js +272 -272
  185. package/services/media/container/MatroskaContainer.js +1494 -1494
  186. package/services/media/container/Mp4Container.js +1244 -1244
  187. package/services/media/container/SubtitleFileContainer.js +322 -322
  188. package/services/media/container/ebml-reader.js +159 -159
  189. package/services/media/ffmpeg-banner.js +260 -260
  190. package/services/media/keyframe-probe.js +157 -157
  191. package/services/media/media-info-probe.js +93 -93
  192. package/services/media/playback-planner.js +931 -931
  193. package/services/media/tracks/AudioTrack.js +131 -131
  194. package/services/media/tracks/ContainerTrack.js +75 -75
  195. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  196. package/services/media/tracks/SubtitleTrack.js +38 -38
  197. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  198. package/services/media/tracks/VideoTrack.js +33 -33
  199. package/services/media/tracks/index.js +15 -15
  200. package/services/media/tracks/language-detect.js +174 -174
  201. package/services/server/OutputLifecycle.js +418 -418
  202. package/services/server/SegmentServing.js +1150 -1150
  203. package/services/server/ViewerRequests.js +760 -760
  204. package/services/server/WaitLedger.js +142 -142
  205. package/services/server/controllers/PlaybackController.js +40 -40
  206. package/services/server/controllers/SubtitleController.js +109 -109
  207. package/services/server/http-responses.js +60 -60
  208. package/services/server/playback-readiness.js +696 -696
  209. package/services/server/transcode-session-files.js +325 -325
  210. package/services/server/wire-outputs.js +992 -992
  211. package/services/storage/Diagnostics.js +202 -202
  212. package/services/storage/MachineBudget.js +235 -235
  213. package/services/storage/allowance.js +107 -107
  214. package/services/storage/core-dumps.js +106 -106
  215. package/services/storage/files/CompletedFiles.js +391 -391
  216. package/services/storage/files/piece-from-whole-file.js +118 -118
  217. package/services/storage/free.js +71 -71
  218. package/services/storage/keep.js +48 -48
  219. package/services/storage/machine-memory.js +56 -56
  220. package/services/storage/memory-report.js +575 -575
  221. package/services/storage/piece-store/piece-disk-store.js +539 -539
  222. package/services/storage/piece-store/piece-lru.js +495 -495
  223. package/services/storage/piece-store/shared-piece-store.js +1931 -1931
  224. package/services/storage/returns.js +103 -103
  225. package/services/storage/segment-store/SegmentStore.js +1240 -1240
  226. package/services/storage/wire.js +175 -175
  227. package/services/torrent/Contents.js +382 -324
  228. package/services/torrent/demand/DemandRegister.js +202 -202
  229. package/services/torrent/demand/Urgency.js +137 -137
  230. package/services/torrent/demand/Window.js +118 -118
  231. package/services/torrent/demand/index.js +8 -8
  232. package/services/torrent/demand/pieces.js +140 -140
  233. package/services/torrent/download/SwarmSelection.js +386 -386
  234. package/services/torrent/download/registry.js +102 -102
  235. package/services/torrent/download/withdraw-claim.js +80 -80
  236. package/services/torrent/episode-naming.js +182 -0
  237. package/services/torrent/files.js +455 -455
  238. package/services/torrent/naming.js +619 -619
  239. package/services/torrent/piece-store-of.js +29 -29
  240. package/services/torrent/supply-margin.js +262 -262
  241. package/services/torrent/torrent-pool.js +2675 -2675
  242. package/services/torrent/worker/background-fill.js +213 -213
  243. package/services/torrent/worker/channel.js +271 -271
  244. package/services/torrent/worker/client.js +765 -765
  245. package/services/torrent/worker/fastest-wires.js +342 -342
  246. package/services/torrent/worker/handle-state.js +23 -23
  247. package/services/torrent/worker/held-bytes.js +155 -155
  248. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  249. package/services/torrent/worker/piece-reader.js +1078 -1078
  250. package/services/torrent/worker/pool-adapter.js +371 -371
  251. package/services/torrent/worker/protocol.js +184 -184
  252. package/services/torrent/worker/resume-warm.js +107 -107
  253. package/services/torrent/worker/webrtc-shim.js +219 -219
  254. package/services/torrent/worker/worker.js +1129 -1129
  255. package/services/transport/data-channel-handler.js +1369 -1369
  256. package/services/transport/delivery-probe.js +842 -842
  257. package/services/transport/health-collector.js +53 -53
  258. package/services/transport/nat-classifier.js +231 -231
  259. package/services/transport/packet-witness.js +808 -808
  260. package/services/transport/port-mapper.js +235 -235
  261. package/services/transport/registry-api.js +81 -81
  262. package/services/transport/tunnel-client.js +447 -447
  263. package/services/transport/usrsctp-state.js +175 -175
  264. package/services/transport/webrtc-manager.js +484 -484
  265. package/services/viewer/Assignments.js +340 -340
  266. package/services/viewer/PriorityMap.js +390 -390
  267. package/services/viewer/PriorityOrchestrator.js +325 -325
  268. package/services/viewer/Viewer.js +691 -691
  269. package/services/viewer/Viewers.js +368 -368
  270. package/services/viewer/active-output.js +37 -37
  271. package/services/viewer/audio-start.js +46 -46
  272. package/services/viewer/buffer-trend.js +81 -81
  273. package/services/viewer/choices.js +647 -647
  274. package/services/viewer/positions.js +126 -126
  275. package/services/viewer/report-intake.js +46 -46
  276. package/store/source-registry.js +71 -71
  277. package/test/asset-paths.test.js +28 -28
  278. package/test/audio-inventory.test.js +212 -212
  279. package/test/audio-track-bitrate.test.js +40 -40
  280. package/test/auto-quality-step.test.js +707 -707
  281. package/test/available-share.test.js +76 -76
  282. package/test/background-fill.test.js +190 -190
  283. package/test/client-log-file.test.js +110 -110
  284. package/test/completed-files.test.js +115 -115
  285. package/test/concurrent-cost.test.js +148 -148
  286. package/test/container-media-info.test.js +228 -228
  287. package/test/contention.test.js +76 -76
  288. package/test/core-dumps.test.js +57 -57
  289. package/test/coverage-follows-the-disk.test.js +228 -228
  290. package/test/coverage-map.test.js +195 -195
  291. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  292. package/test/cut-times-timeline.test.js +65 -65
  293. package/test/cuts-follow-published-grid.test.js +109 -109
  294. package/test/data-channel-frame.test.js +63 -63
  295. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  296. package/test/dead-torrent-handle.test.js +52 -52
  297. package/test/declared-rates.test.js +116 -116
  298. package/test/declared-tracks.test.js +43 -43
  299. package/test/decode-cost-fit.test.js +202 -202
  300. package/test/decode-cost.test.js +462 -462
  301. package/test/delivery-probe.test.js +393 -393
  302. package/test/demand-register.test.js +206 -206
  303. package/test/dht-bootstrap.test.js +72 -72
  304. package/test/drain-threat.test.js +65 -65
  305. package/test/duplicate-add.test.js +48 -48
  306. package/test/encode-admission.test.js +146 -146
  307. package/test/encode-cost.test.js +283 -283
  308. package/test/encode-exit.test.js +111 -111
  309. package/test/encode-orchestrator.test.js +419 -419
  310. package/test/encode-plan-viewers.test.js +718 -718
  311. package/test/encode-plan.test.js +545 -545
  312. package/test/encode-run-state.test.js +314 -314
  313. package/test/encode-run.test.js +162 -162
  314. package/test/encode-slope.test.js +46 -46
  315. package/test/encoded-output.test.js +39 -39
  316. package/test/encoder-kinds.test.js +122 -122
  317. package/test/encoder-readings.test.js +46 -46
  318. package/test/episode-naming.test.js +131 -0
  319. package/test/every-claimant-is-told-its-share.test.js +152 -152
  320. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  321. package/test/failed-start-breaker.test.js +202 -202
  322. package/test/fastest-wires.test.js +143 -143
  323. package/test/ffmpeg-stream-counts.test.js +78 -78
  324. package/test/file-edges.test.js +191 -191
  325. package/test/fmp4-presentation-clock.test.js +147 -147
  326. package/test/fmp4-self-contained.test.js +80 -80
  327. package/test/fmp4-timestamps.test.js +140 -140
  328. package/test/fmp4-track-completeness.test.js +41 -41
  329. package/test/format-keyed-outputs.test.js +78 -78
  330. package/test/health-metrics.test.js +38 -38
  331. package/test/held-request-width.test.js +157 -157
  332. package/test/helpers/encode-run.js +136 -136
  333. package/test/helpers/held-file.js +29 -29
  334. package/test/helpers/manager.js +57 -57
  335. package/test/helpers/output-spec.js +63 -63
  336. package/test/host-load.test.js +68 -68
  337. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  338. package/test/init-compat.test.js +219 -219
  339. package/test/input-lost-quiets-the-plan.test.js +261 -261
  340. package/test/input-unavailable.test.js +48 -48
  341. package/test/installation-id.test.js +39 -39
  342. package/test/keeping-period.test.js +83 -83
  343. package/test/keyframe-index-accuracy.test.js +188 -188
  344. package/test/keyframe-table-budget.test.js +102 -102
  345. package/test/keyframes-belong-to-the-file.test.js +249 -249
  346. package/test/learned-median.test.js +37 -37
  347. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  348. package/test/limit-row-by-area.test.js +60 -60
  349. package/test/link-budget.test.js +111 -111
  350. package/test/link-probe.test.js +56 -56
  351. package/test/local-encoding-data.test.js +273 -273
  352. package/test/logger-repeats.test.js +134 -134
  353. package/test/look-ahead-coverage.test.js +56 -56
  354. package/test/manager-surface.test.js +292 -292
  355. package/test/matroska-cues-track.test.js +192 -192
  356. package/test/memory-budget.test.js +390 -390
  357. package/test/move-cost.test.js +216 -216
  358. package/test/mp4-subtitles.test.js +173 -173
  359. package/test/one-authority.test.js +285 -285
  360. package/test/one-budget.test.js +149 -149
  361. package/test/one-output-one-session.test.js +204 -204
  362. package/test/one-resource-divided.test.js +185 -185
  363. package/test/one-serving-output-per-height.test.js +458 -458
  364. package/test/orchestrator-wired.test.js +206 -206
  365. package/test/output-catalog.test.js +39 -39
  366. package/test/output-shape.test.js +21 -21
  367. package/test/output-spec.test.js +251 -251
  368. package/test/output-speed.test.js +88 -88
  369. package/test/outputs-being-prepared.test.js +40 -40
  370. package/test/packet-witness-ring.test.js +236 -236
  371. package/test/packet-witness.test.js +148 -148
  372. package/test/piece-disk-store.test.js +401 -401
  373. package/test/piece-from-whole-file.test.js +129 -129
  374. package/test/piece-lru.test.js +319 -319
  375. package/test/piece-reader.test.js +162 -162
  376. package/test/piece-selection-offset.test.js +85 -85
  377. package/test/piece-store-eviction.test.js +595 -595
  378. package/test/piece-store-never-refuses.test.js +153 -153
  379. package/test/piece-store-reservations.test.js +403 -403
  380. package/test/piece-store-slow-disk.test.js +159 -159
  381. package/test/piece-tail.test.js +97 -97
  382. package/test/plan-host-timings.test.js +68 -68
  383. package/test/plan-is-steady.test.js +225 -225
  384. package/test/playback-readiness.test.js +453 -453
  385. package/test/playback-start-regressions.test.js +99 -99
  386. package/test/priority-map-download.test.js +174 -174
  387. package/test/priority-map-emptied.test.js +204 -204
  388. package/test/priority-map-per-output.test.js +215 -215
  389. package/test/priority-map.test.js +163 -163
  390. package/test/probe-wedge-certainty.test.js +60 -60
  391. package/test/produced-copy-choice.test.js +213 -213
  392. package/test/published-timeline.test.js +83 -83
  393. package/test/quality-offer.test.js +162 -162
  394. package/test/quality-variants.test.js +1229 -1229
  395. package/test/rate-control-in-the-output.test.js +180 -180
  396. package/test/read-survives-withdrawal.test.js +164 -164
  397. package/test/read-window.test.js +365 -365
  398. package/test/resume-warm.test.js +39 -39
  399. package/test/run-graph-drift.test.js +37 -37
  400. package/test/run-inputs.test.js +80 -80
  401. package/test/run-intervals.test.js +100 -100
  402. package/test/run-position-follows-published-grid.test.js +89 -89
  403. package/test/run-progress.test.js +65 -65
  404. package/test/same-height-switch.test.js +664 -664
  405. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  406. package/test/seek-landing.test.js +109 -109
  407. package/test/seek-target-not-superseded.test.js +195 -195
  408. package/test/segment-closed-on-both-branches.test.js +96 -96
  409. package/test/segment-cut-times.test.js +63 -63
  410. package/test/segment-demand.test.js +80 -80
  411. package/test/segment-hold-supersede.test.js +91 -91
  412. package/test/segment-judged-by-its-own-header.test.js +221 -221
  413. package/test/segment-serve-wiring.test.js +475 -475
  414. package/test/segment-store-eviction.test.js +247 -247
  415. package/test/segment-store.test.js +323 -323
  416. package/test/segment-true-start.test.js +86 -86
  417. package/test/segments-are-shared.test.js +176 -176
  418. package/test/send-chunk-bytes.test.js +86 -86
  419. package/test/serving-existing-output.test.js +102 -102
  420. package/test/serving-output.test.js +102 -102
  421. package/test/shared-piece-store.test.js +310 -310
  422. package/test/sidecar-naming.test.js +142 -142
  423. package/test/source-file.test.js +133 -133
  424. package/test/source-files-route.test.js +12 -2
  425. package/test/source-registry.test.js +18 -18
  426. package/test/split-a-stretch.test.js +139 -139
  427. package/test/stale-request-after-seek.test.js +158 -158
  428. package/test/startup-readings.test.js +124 -124
  429. package/test/stream-route.test.js +209 -209
  430. package/test/subtitle-cue-framing.test.js +200 -200
  431. package/test/subtitle-cue-source.test.js +102 -102
  432. package/test/subtitle-cue-walk.test.js +370 -370
  433. package/test/subtitle-cursor.test.js +63 -63
  434. package/test/subtitle-defaults.test.js +118 -118
  435. package/test/subtitle-language.test.js +252 -252
  436. package/test/subtitle-track-numbering.test.js +386 -386
  437. package/test/supply-margin.test.js +177 -177
  438. package/test/swarm-follows-readers.test.js +174 -174
  439. package/test/swarm-reach.test.js +130 -130
  440. package/test/swarm-selection.test.js +220 -220
  441. package/test/tail-duplication.test.js +167 -167
  442. package/test/the-channel-has-both-ends.test.js +91 -91
  443. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  444. package/test/thread-pool.test.js +53 -53
  445. package/test/timeline.test.js +84 -84
  446. package/test/torrent-contents.test.js +235 -235
  447. package/test/torrent-cost.test.js +71 -71
  448. package/test/torrent-files.test.js +221 -221
  449. package/test/torrent-naming.test.js +255 -255
  450. package/test/tracks-begin-together.test.js +209 -209
  451. package/test/tunnel-renewal.test.js +167 -167
  452. package/test/two-viewers-one-picture.test.js +407 -407
  453. package/test/upload-hurry.test.js +186 -186
  454. package/test/usrsctp-state.test.js +131 -131
  455. package/test/video-facts.test.js +102 -102
  456. package/test/viewer-assignments.test.js +143 -143
  457. package/test/viewer-outputs.test.js +285 -285
  458. package/test/viewer-presence.test.js +116 -116
  459. package/test/viewer-requests-admission.test.js +89 -89
  460. package/test/viewer-states.test.js +189 -189
  461. package/test/viewer-subtitle-subscription.test.js +96 -96
  462. package/test/viewer.test.js +139 -139
  463. package/test/viewing-generations.test.js +390 -390
  464. package/test/visible-rung.test.js +37 -37
  465. package/test/wait-ledger.test.js +100 -100
  466. package/test/webrtc-shim.test.js +149 -149
  467. package/test/wedge-certainty.test.js +131 -131
  468. package/test/withdraw-piece-claim.test.js +212 -212
  469. package/test/worker-channel.test.js +158 -158
  470. package/utils/client-log-file.js +151 -151
  471. package/utils/logger.js +312 -312
  472. package/utils/parse-range.js +25 -25
  473. package/utils/perf.js +59 -59
  474. package/utils/torrent-source-key.js +38 -38
@@ -1,718 +1,718 @@
1
- /**
2
- * @file The placement model under one, two and three viewers, each of them
3
- * playing, seeking and paused.
4
- *
5
- * THE MODEL, restated so a failure here can be read against it:
6
- *
7
- * - each segment number carries a deadline `D(x)`, the seconds until somebody
8
- * needs it. A viewer moving forward covers a second of film in a second, so
9
- * `D(x)` is the distance to them. `Infinity` where nobody is coming;
10
- * - an encoder placed at `a` delivers `a + j` at `(j + 1) / r`, `r` segments per
11
- * second. The same expression answers "when would the one already placed get
12
- * here";
13
- * - a number is late when it arrives after its deadline. Placement is first-fit
14
- * left to right: give it to an encoder that arrives in time, else open one
15
- * exactly there.
16
- *
17
- * Three behaviours of the map follow from that and are checked here rather than
18
- * assumed. A peak MOVES FORWARD on its own, because the deadline is a distance
19
- * and the distance shrinks as the viewer watches. It JUMPS on a seek. It
20
- * FLATTENS on a pause, because a viewer who is not moving has no time by which
21
- * anything must exist.
22
- *
23
- * Nothing here spawns ffmpeg, touches a disk or reads a clock. That is the layer
24
- * check, made executable: this layer is exercised with plain values alone.
25
- */
26
-
27
- import test from "node:test";
28
- import assert from "node:assert/strict";
29
- import { EventEmitter } from "node:events";
30
- import { EncodeRun } from "../services/encode/EncodeRun.js";
31
- import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
32
- import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
33
- import { mapForViewer, mergeMaps, runsOf } from "../services/viewer/PriorityMap.js";
34
- import { penaltiesFrom } from "../services/encode/contention.js";
35
-
36
- // WHAT A SECOND ENCODER COSTS THE FIRST — measured, never a formula.
37
- //
38
- // Addon host, 2026-09-03: 854x480 through libx264 `ultrafast` ran at 7.12x with
39
- // the machine to itself, and at 4.20x and 4.16x when two ran at once. The
40
- // penalty is read off that reading by the SAME two functions production uses,
41
- // so nothing here invents a shape: beyond what was measured the reading is held
42
- // rather than extrapolated.
43
- const MEASURED_PENALTIES = penaltiesFrom(7.12, [{ others: 1, speed: 4.18 }]);
44
-
45
- // What a start and a stop cost, measured on the same host: a spawn with its
46
- // input open is 0.12 s there.
47
- const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
48
-
49
-
50
- const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
51
- const SEGMENT_SECONDS = 4;
52
- const FILM_SECONDS = 4000;
53
- const SEGMENTS = FILM_SECONDS / SEGMENT_SECONDS;
54
-
55
- class FakeProcess extends EventEmitter {
56
- constructor() {
57
- super();
58
- this.pid = 1;
59
- }
60
-
61
- kill(signal) {
62
- this.emit("close", null, signal);
63
- }
64
- }
65
-
66
- /** @param {{ maxRuns?: number }} [options] */
67
- function orchestrator({ maxRuns = 3 } = {}) {
68
- /** @type {EncodeOrchestrator} */
69
- let made;
70
- const build = ({ address, from, to }) => new EncodeRun({
71
- address,
72
- encoder: new SoftwareEncoder(),
73
- from,
74
- to,
75
- buildArgs: () => ["-i", "in", "out"],
76
- spawn: () => new FakeProcess(),
77
- logger: { info() {}, warn() {} },
78
- now: () => 1000,
79
- onEnded: (ended) => made.noteEnded(ended)
80
- });
81
- made = new EncodeOrchestrator({
82
- maxRunsFor: () => maxRuns,
83
- segmentSeconds: SEGMENT_SECONDS,
84
- ...RUN_COSTS,
85
- // A swarm with room for several encoders. The figure is the measured cost
86
- // of fetching a second of film again, in seconds of swarm time; at 0.25 one
87
- // encoder at 6x already takes one and a half swarms, so the budget would be
88
- // one process and the placement would have nothing to place — which is a
89
- // real limit, checked in its own test below, and not the subject of these.
90
- refetchSecPerFilmSecond: () => 0.02,
91
- // What the startup benchmark says this host encodes at. It exists before
92
- // any viewer, so the plan is never without a speed.
93
- startingSpeedFor: () => 2,
94
- // What a second encoder costs the first, measured on the addon host. The
95
- // orchestrator reads the table with the same pure function production uses.
96
- // Without it an extra process is free and the score always wants more of
97
- // them — and fewer encoders can genuinely finish sooner.
98
- contentionPenalties: MEASURED_PENALTIES,
99
- now: () => 1000,
100
- logger: { info() {}, warn() {} },
101
- makeRun: build
102
- });
103
- made.setSegmentCount(PICTURE, SEGMENTS);
104
- /** Who is watching, by name. The map is rebuilt from all of them on change. */
105
- const watching = new Map();
106
- const watches = (who, { atSeconds, playing = true }) => {
107
- watching.set(who, { atSeconds, playing });
108
- stateMap(made, watching);
109
- };
110
- const leaves = (who) => {
111
- watching.delete(who);
112
- stateMap(made, watching);
113
- };
114
- return { made, build, watches, leaves };
115
- }
116
-
117
- /**
118
- * Everybody watching, as ONE map, the way the product builds it.
119
- *
120
- * A viewer's own map is in seconds of film; the maps of all of them are merged
121
- * into one, and only then converted into an output's own numbering. Doing it
122
- * here rather than stating windows by hand is the point: the pause and the seek
123
- * are the MAP's behaviour, and a test that stated windows directly would be
124
- * checking its own arithmetic.
125
- *
126
- * @param {EncodeOrchestrator} made
127
- * @param {Map<string, { atSeconds: number, playing: boolean }>} watching
128
- */
129
- function stateMap(made, watching) {
130
- const map = mergeMaps(
131
- [...watching.values()].map((viewer) => mapForViewer({
132
- atSeconds: viewer.atSeconds,
133
- durationSeconds: FILM_SECONDS,
134
- allowanceSeconds: 8,
135
- playing: viewer.playing
136
- }))
137
- );
138
- // Seconds of film into this output's own piece numbers. The product does it
139
- // with the cut table, which knows where the pieces fall; here the pieces are a
140
- // uniform grid, so it is a division — and doing it at all is the point, since
141
- // the map is per FILM and every consumer numbers differently.
142
- made.notePriorityMap(PICTURE, runsOf(map).map((zone) => ({
143
- from: Math.floor(zone.from / SEGMENT_SECONDS),
144
- to: Math.max(
145
- Math.floor(zone.from / SEGMENT_SECONDS),
146
- Math.ceil(zone.to / SEGMENT_SECONDS) - 1
147
- ),
148
- priority: zone.priority,
149
- withinSeconds: zone.withinSeconds,
150
- behind: zone.behind
151
- })));
152
- }
153
-
154
- /**
155
- * How many encoders are working the stretch around one number.
156
- *
157
- * The count that matters for a viewer is not how many exist — spare capacity is
158
- * meant to be spent finishing the film, so a machine that holds three should be
159
- * holding three — but how many are crowded onto the place they are standing.
160
- *
161
- * @param {EncodeOrchestrator} made
162
- * @param {number} segment
163
- */
164
- function onTheStretchOf(made, segment) {
165
- return made.runsOn(PICTURE).filter((run) => {
166
- const to = run.to < run.from ? Number.POSITIVE_INFINITY : run.to;
167
- return run.from <= segment && segment <= to;
168
- }).length;
169
- }
170
-
171
- /** @param {EncodeOrchestrator} made */
172
- function placements(made) {
173
- return made.runsOn(PICTURE).map((run) => run.from).sort((left, right) => left - right);
174
- }
175
-
176
- /**
177
- * Every live encoder's stretch, so that "they never share a number" can be
178
- * asserted as the consequence it is rather than trusted.
179
- *
180
- * @param {EncodeOrchestrator} made
181
- */
182
- function assertNoOverlap(made) {
183
- const spans = made.runsOn(PICTURE)
184
- .map((run) => [run.from, run.to < run.from ? Number.POSITIVE_INFINITY : run.to])
185
- .sort((left, right) => left[0] - right[0]);
186
- for (let index = 0; index < spans.length - 1; index += 1) {
187
- assert.ok(
188
- spans[index][1] < spans[index + 1][0],
189
- `#${spans[index][0]}..#${spans[index][1]} overlaps #${spans[index + 1][0]}`
190
- );
191
- }
192
- }
193
-
194
- // ---------------------------------------------------------------- one viewer
195
-
196
- test("one viewer playing: one encoder, exactly where they are", () => {
197
- const { made, watches } = orchestrator();
198
- watches("one", { atSeconds: 400 });
199
- made.reconcile();
200
- assert.deepEqual(placements(made), [100], "at their own position, 400s / 4s");
201
- assertNoOverlap(made);
202
- });
203
-
204
- test("one viewer playing: the encoder keeping up buys no second one", () => {
205
- // The peak moves forward on its own as they watch, and an encoder that stays
206
- // in front of it is never late. This is the case that must NOT spend a
207
- // process, and the one the old head-as-a-barrier rule got wrong.
208
- const { made, watches } = orchestrator();
209
- watches("one", { atSeconds: 400 });
210
- made.reconcile();
211
- const [run] = made.runsOn(PICTURE);
212
- run.noteSpeed(6);
213
- for (let index = 100; index < 130; index += 1) {
214
- made.noteProduced(PICTURE, index);
215
- }
216
- watches("one", { atSeconds: 480 });
217
- made.reconcile();
218
- // Asked of the first number nobody has, not of the one they are standing on:
219
- // that one is already made, and nothing needs to be covering it.
220
- assert.equal(onTheStretchOf(made, 130), 1, "one encoder on the film in front of them");
221
- assertNoOverlap(made);
222
- });
223
-
224
- test("one viewer seeking far ahead: an encoder is placed there", () => {
225
- const { made, watches } = orchestrator();
226
- watches("one", { atSeconds: 400 });
227
- made.reconcile();
228
- made.runsOn(PICTURE)[0].noteSpeed(6);
229
- made.noteProduced(PICTURE, 100);
230
-
231
- // The peak JUMPS. Nothing about the old place is wanted now.
232
- watches("one", { atSeconds: 3000 });
233
- made.reconcile();
234
-
235
- assert.ok(placements(made).includes(750), "an encoder where they landed, 3000s / 4s");
236
- assertNoOverlap(made);
237
- });
238
-
239
- test("one viewer paused: nothing is late, so no encoder is added", () => {
240
- // A paused viewer states the whole film at one undifferentiated rank and no
241
- // time by which any of it must exist. The encoder already working goes on
242
- // encoding the track; nothing justifies a second process.
243
- const { made, watches } = orchestrator();
244
- watches("one", { atSeconds: 400 });
245
- made.reconcile();
246
- made.runsOn(PICTURE)[0].noteSpeed(1);
247
- made.noteProduced(PICTURE, 100);
248
-
249
- watches("one", { atSeconds: 404, playing: false });
250
- made.reconcile();
251
-
252
- assert.equal(onTheStretchOf(made, 101), 1, "one encoder where they are standing");
253
- assertNoOverlap(made);
254
- });
255
-
256
- test("one viewer paused states no deadline anywhere", () => {
257
- // The map's own answer, checked directly, because every placement decision
258
- // below rests on it.
259
- const map = mapForViewer({
260
- atSeconds: 400,
261
- durationSeconds: FILM_SECONDS,
262
- allowanceSeconds: 8,
263
- playing: false
264
- });
265
- assert.ok(map.durationSeconds > 0, "a paused viewer still wants the film");
266
- for (let second = 0; second < map.durationSeconds; second += 1) {
267
- assert.equal(map.secondsUntilPlayed[second], Number.POSITIVE_INFINITY,
268
- "but no second of it has a time by which it must exist");
269
- }
270
- // Their POSITION survives the pause, and with it the rule that what is in
271
- // front of them is made before what is behind.
272
- assert.equal(map.behind[399], 1, "what they have watched is behind them");
273
- assert.equal(map.behind[400], 0, "and what they have not is in front");
274
- assert.ok(map.priority[400] > map.priority[399], "which is what the numbers say");
275
- });
276
-
277
- // --------------------------------------------------------------- two viewers
278
-
279
- test("two viewers close together share one encoder", () => {
280
- // Film both of them want is made once. This is what merging the map is for,
281
- // and it must survive the deadline being carried alongside the rank.
282
- const { made, watches } = orchestrator();
283
- watches("one", { atSeconds: 400 });
284
- watches("two", { atSeconds: 408 });
285
- made.reconcile();
286
- // One encoder for the pair — asked of the film they are both about to watch.
287
- // The machine may well be running others further on: what it has spare goes to
288
- // finishing the file, and that is a different question from crowding.
289
- assert.equal(onTheStretchOf(made, 100), 1, "one encoder for the pair");
290
- assert.equal(onTheStretchOf(made, 102), 1, "and one for the film just in front of them");
291
- assertNoOverlap(made);
292
- });
293
-
294
- test("two viewers far apart get an encoder each", () => {
295
- const { made, watches } = orchestrator();
296
- watches("one", { atSeconds: 400 });
297
- made.reconcile();
298
- made.runsOn(PICTURE)[0].noteSpeed(6);
299
- made.noteProduced(PICTURE, 100);
300
-
301
- watches("two", { atSeconds: 3000 });
302
- made.reconcile();
303
-
304
- assert.ok(placements(made).includes(750), "the far one is served where they stand");
305
- assert.equal(onTheStretchOf(made, 750), 1, "and by one encoder, not a crowd");
306
- assertNoOverlap(made);
307
- });
308
-
309
- test("two viewers: one seeking does not take the other's encoder", () => {
310
- const { made, watches } = orchestrator();
311
- watches("one", { atSeconds: 400 });
312
- made.reconcile();
313
- // A speed has to be measured before a second encoder can be justified: how
314
- // long one takes to reach a number is the whole comparison. Real life
315
- // measures it from the first run; a test that stated both viewers before any
316
- // run existed would be asking the plan to buy a process on no evidence.
317
- made.runsOn(PICTURE)[0].noteSpeed(6);
318
- made.noteProduced(PICTURE, 100);
319
- watches("two", { atSeconds: 3000 });
320
- made.reconcile();
321
- for (const run of made.runsOn(PICTURE)) {
322
- run.noteSpeed(6);
323
- }
324
- assert.ok(made.runsOn(PICTURE).length >= 2, "one each to begin with");
325
-
326
- // The far one seeks somewhere else entirely.
327
- watches("two", { atSeconds: 2000 });
328
- made.reconcile();
329
-
330
- // Asked of the coverage, not of a run's first number: a run whose road was
331
- // shortened begins again at its own head, so "is this viewer served" is a
332
- // question about the map and never about where a process happens to start.
333
- const coverage = made.coverageOf(PICTURE);
334
- assert.equal(coverage.stateOf(105), "making", "the one who did not move is still served");
335
- assert.ok(placements(made).includes(500), "and the one who moved is served where they landed");
336
- assertNoOverlap(made);
337
- });
338
-
339
- test("two viewers: one pausing leaves the other served", () => {
340
- const { made, watches } = orchestrator();
341
- watches("one", { atSeconds: 400 });
342
- watches("two", { atSeconds: 3000 });
343
- made.reconcile();
344
- for (const run of made.runsOn(PICTURE)) {
345
- run.noteSpeed(1);
346
- }
347
-
348
- watches("two", { atSeconds: 3000, playing: false });
349
- made.reconcile();
350
-
351
- assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
352
- "the one still watching keeps their encoder");
353
- assertNoOverlap(made);
354
- });
355
-
356
- test("two viewers: the one who leaves takes nothing from the one who stays", () => {
357
- const { made, watches, leaves } = orchestrator();
358
- watches("one", { atSeconds: 400 });
359
- watches("two", { atSeconds: 3000 });
360
- made.reconcile();
361
- for (const run of made.runsOn(PICTURE)) {
362
- run.noteSpeed(1);
363
- }
364
-
365
- leaves("two");
366
- made.reconcile();
367
-
368
- assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
369
- "the one who stayed is still served");
370
- assertNoOverlap(made);
371
- });
372
-
373
- // ------------------------------------------------------------- three viewers
374
-
375
- test("three viewers far apart get an encoder each when the machine affords it", () => {
376
- const { made, watches } = orchestrator({ maxRuns: 3 });
377
- watches("one", { atSeconds: 400 });
378
- watches("two", { atSeconds: 2000 });
379
- watches("three", { atSeconds: 3600 });
380
- made.reconcile();
381
- for (const run of made.runsOn(PICTURE)) {
382
- run.noteSpeed(6);
383
- }
384
- made.reconcile();
385
-
386
- const where = placements(made);
387
- for (const at of [100, 500, 900]) {
388
- assert.ok(where.includes(at), `somebody is making what the viewer at #${at} needs`);
389
- assert.equal(onTheStretchOf(made, at), 1, `and one encoder there, not a crowd`);
390
- }
391
- assertNoOverlap(made);
392
- });
393
-
394
- test("three viewers, a machine that affords two: the budget binds, not the map", () => {
395
- // The number of encoders is the smaller of what the map asks and what the
396
- // machine can hold. Which two are served follows from the order the work is
397
- // taken in; what must not happen is a third process on a host that cannot
398
- // hold it, or two processes writing one number.
399
- const { made, watches } = orchestrator({ maxRuns: 2 });
400
- watches("one", { atSeconds: 400 });
401
- watches("two", { atSeconds: 2000 });
402
- watches("three", { atSeconds: 3600 });
403
- made.reconcile();
404
- for (const run of made.runsOn(PICTURE)) {
405
- run.noteSpeed(1);
406
- }
407
- made.reconcile();
408
-
409
- assert.ok(made.runsOn(PICTURE).length <= 2, "never more than the machine affords");
410
- assert.ok(made.runsOn(PICTURE).length >= 1, "and never nothing while people wait");
411
- assertNoOverlap(made);
412
- });
413
-
414
- test("three viewers: one seeks onto another, and the two of them share", () => {
415
- const { made, watches } = orchestrator({ maxRuns: 3 });
416
- watches("one", { atSeconds: 400 });
417
- made.reconcile();
418
- made.runsOn(PICTURE)[0].noteSpeed(6);
419
- made.noteProduced(PICTURE, 100);
420
- watches("two", { atSeconds: 2000 });
421
- watches("three", { atSeconds: 3600 });
422
- made.reconcile();
423
- for (const run of made.runsOn(PICTURE)) {
424
- run.noteSpeed(6);
425
- }
426
- made.reconcile();
427
- assert.equal(made.runsOn(PICTURE).length, 3);
428
-
429
- // The third joins the second. Two peaks where there were three.
430
- watches("three", { atSeconds: 2008 });
431
- made.reconcile();
432
-
433
- assert.ok(made.runsOn(PICTURE).length <= 3, "no process is added by people converging");
434
- assertNoOverlap(made);
435
- });
436
-
437
- test("three viewers: all paused, and no encoder is added for any of them", () => {
438
- const { made, watches } = orchestrator({ maxRuns: 3 });
439
- watches("one", { atSeconds: 400 });
440
- made.reconcile();
441
- made.runsOn(PICTURE)[0].noteSpeed(1);
442
- const before = made.runsOn(PICTURE).length;
443
-
444
- watches("one", { atSeconds: 400, playing: false });
445
- watches("two", { atSeconds: 2000, playing: false });
446
- watches("three", { atSeconds: 3600, playing: false });
447
- made.reconcile();
448
-
449
- // Nobody is coming anywhere, so nothing can be late — and the machine does not
450
- // therefore fall idle: what it has spare goes on finishing the file, which is
451
- // what makes a seek back into a made part start playing at once.
452
- assert.ok(made.runsOn(PICTURE).length >= before, "the file goes on being finished");
453
- assertNoOverlap(made);
454
- });
455
-
456
- test("the plan is a function of the state: the same state twice gives the same answer", () => {
457
- // What stops a moving map from becoming a thrash: the decision is arithmetic
458
- // over the state, so a pass that finds nothing changed changes nothing.
459
- const { made, watches } = orchestrator({ maxRuns: 3 });
460
- watches("one", { atSeconds: 400 });
461
- watches("two", { atSeconds: 2000 });
462
- made.reconcile();
463
- for (const run of made.runsOn(PICTURE)) {
464
- run.noteSpeed(1);
465
- }
466
- made.reconcile();
467
- const first = placements(made);
468
- made.reconcile();
469
- made.reconcile();
470
- assert.deepEqual(placements(made), first, "three more passes moved nothing");
471
- });
472
-
473
- // ---------------------------------------------------------------- the edge
474
-
475
- test("exactly realtime arrives exactly on time, and no second encoder is bought", () => {
476
- // The knife edge, and it lands on "in time" by construction: the deadline of
477
- // a number is the distance to it, and an encoder at 1.0x covers that distance
478
- // in exactly that time. So the arithmetic says nothing is late and nothing is
479
- // bought. Worth pinning, because the formula this model replaced said
480
- // something quite different — `speed / (1 - speed)` divides by zero here and
481
- // claims the encoder stays ahead FOR EVER, which is the same answer arrived
482
- // at by nonsense.
483
- const { made, watches } = orchestrator({ maxRuns: 3 });
484
- watches("one", { atSeconds: 400 });
485
- made.reconcile();
486
- made.runsOn(PICTURE)[0].noteSpeed(1);
487
- made.noteProduced(PICTURE, 100);
488
- made.reconcile();
489
-
490
- assert.equal(onTheStretchOf(made, 150), 1,
491
- "just in time everywhere, so one encoder on the viewer's stretch is enough");
492
- assertNoOverlap(made);
493
- });
494
-
495
- test("below realtime, whether a second encoder helps is arithmetic, and here it does not", () => {
496
- // The side of the edge that matters, and the answer is not the one this check
497
- // asserted when it was written. An encoder at half realtime loses ground on
498
- // the viewer every second, so everything ahead of it is late — and a second
499
- // process does not fix that, because on this host each of two runs at 1.70x
500
- // slower than one. The near film gets worse by more than the far film gets
501
- // better, so the total seconds of waiting go UP, and the score refuses.
502
- //
503
- // What this case actually needs is the answer the user gave for it: a machine
504
- // that cannot serve its viewers without stopping the picture says so, and the
505
- // viewer is moved to another proxy. That is not built. Adding processes is not
506
- // a substitute for it and the model correctly declines to pretend otherwise.
507
- const { made, watches, leaves } = orchestrator({ maxRuns: 3 });
508
- watches("one", { atSeconds: 400 });
509
- made.reconcile();
510
- made.runsOn(PICTURE)[0].noteSpeed(0.5);
511
- made.noteProduced(PICTURE, 100);
512
- made.reconcile();
513
-
514
- assert.equal(made.runsOn(PICTURE).length, 1,
515
- "one encoder, because two of them would leave the viewer waiting longer");
516
- assert.equal(made.coverageOf(PICTURE).stateOf(101), "making",
517
- "and it is on the film in front of them");
518
- assertNoOverlap(made);
519
- void leaves;
520
- });
521
-
522
- test("two viewers arriving together on a cold output get one encoder, then are measured", () => {
523
- // Nothing has measured how fast this machine encodes, so how long one encoder
524
- // would take to reach the second viewer is not a known quantity. Buying a
525
- // process on that is buying it on no evidence, which is refused; one is placed
526
- // and the speed it reports is what justifies the next.
527
- const { made, watches } = orchestrator({ maxRuns: 3 });
528
- watches("one", { atSeconds: 400 });
529
- watches("two", { atSeconds: 3000 });
530
- made.reconcile();
531
- assert.ok(made.runsOn(PICTURE).length >= 1, "at least one, until something is measured");
532
- assert.equal(onTheStretchOf(made, 100), 1, "and not two on the same place");
533
-
534
- made.runsOn(PICTURE)[0].noteSpeed(6);
535
- made.noteProduced(PICTURE, 100);
536
- made.reconcile();
537
- assert.ok(placements(made).includes(750), "and now the far one is served too");
538
- assertNoOverlap(made);
539
- });
540
-
541
- test("an encoder comfortably faster than realtime is left to do the whole stretch", () => {
542
- const { made, watches } = orchestrator({ maxRuns: 3 });
543
- watches("one", { atSeconds: 400 });
544
- made.reconcile();
545
- made.runsOn(PICTURE)[0].noteSpeed(6);
546
- made.noteProduced(PICTURE, 100);
547
- made.reconcile();
548
-
549
- assert.equal(onTheStretchOf(made, 150), 1, "one encoder on the stretch they are watching");
550
- assertNoOverlap(made);
551
- });
552
-
553
- test("a swarm that feeds one encoder moves it to whoever is late, rather than serving nobody", () => {
554
- // The budget binds. The one encoder there is stands where nothing is due —
555
- // the viewer left — so it is moved to the soonest number that IS due. Without
556
- // this the viewer who seeked was served by nobody at all: the run kept its
557
- // road because nothing covered what lay in front of IT.
558
- const { made, watches } = orchestrator({ maxRuns: 3 });
559
- watches("one", { atSeconds: 400 });
560
- made.reconcile();
561
- const [run] = made.runsOn(PICTURE);
562
- // 6x against a swarm charging 0.25 s per second of film: one encoder already
563
- // takes one and a half of what is delivered, so the budget is one.
564
- run.noteSpeed(6);
565
- made.refetchSecPerFilmSecond = () => 0.25;
566
- made.noteProduced(PICTURE, 100);
567
-
568
- watches("one", { atSeconds: 3000 });
569
- made.reconcile();
570
-
571
- assert.equal(made.runsOn(PICTURE).length, 1, "still one, because that is what the swarm feeds");
572
- assert.deepEqual(placements(made), [750], "and it is where the viewer now stands");
573
- assertNoOverlap(made);
574
- });
575
-
576
- // ------------------------------------------------------- seeking BACKWARD
577
-
578
- test("one viewer seeking back into film that exists is served from it, with no encoder", () => {
579
- // The case the whole layer was built for. Everything behind them has been
580
- // made, so nothing is late anywhere they are going, and the score says the
581
- // cheapest arrangement is the one that changes nothing. A restart here is the
582
- // 647-second stall of 2026-09-06 in miniature.
583
- const { made, watches } = orchestrator();
584
- watches("one", { atSeconds: 400 });
585
- made.reconcile();
586
- made.runsOn(PICTURE)[0].noteSpeed(6);
587
- for (let index = 100; index <= 160; index += 1) {
588
- made.noteProduced(PICTURE, index);
589
- }
590
- const before = made.runsOn(PICTURE);
591
-
592
- watches("one", { atSeconds: 440 });
593
- made.reconcile();
594
-
595
- // Not "the count is unchanged": with nothing late anywhere, the machine is
596
- // free to spend what it has spare on finishing the file, and that is what the
597
- // user asked for. What must not happen is a process put on film that already
598
- // exists, or the run they were behind restarted — which is the 647-second
599
- // stall of 2026-09-06 in miniature.
600
- for (const was of before) {
601
- assert.ok(made.runsOn(PICTURE).includes(was),
602
- "the encoder they were behind is not restarted");
603
- }
604
- for (const run_ of made.runsOn(PICTURE)) {
605
- // Where it is WORKING, not where it was created: the surviving run was
606
- // started at #100 and has since produced up to #161.
607
- assert.equal(made.coverageOf(PICTURE).isReady(run_.head), false,
608
- `no process is bought for film that exists (#${run_.head})`);
609
- }
610
- assertNoOverlap(made);
611
- });
612
-
613
- test("one viewer seeking back into film nobody has gets an encoder there", () => {
614
- // Behind them is not the same as made. Where the film was never encoded, going
615
- // back is exactly as bare as going forward, and the arithmetic is the same one.
616
- const { made, watches } = orchestrator();
617
- watches("one", { atSeconds: 3000 });
618
- made.reconcile();
619
- made.runsOn(PICTURE)[0].noteSpeed(6);
620
- made.noteProduced(PICTURE, 750);
621
-
622
- watches("one", { atSeconds: 400 });
623
- made.reconcile();
624
-
625
- const coverage = made.coverageOf(PICTURE);
626
- assert.equal(coverage.stateOf(100), "making", "somebody is making where they landed");
627
- assertNoOverlap(made);
628
- });
629
-
630
- test("two viewers: one seeks back onto film the other already had made", () => {
631
- const { made, watches } = orchestrator({ maxRuns: 3 });
632
- watches("one", { atSeconds: 400 });
633
- made.reconcile();
634
- made.runsOn(PICTURE)[0].noteSpeed(6);
635
- for (let index = 100; index <= 200; index += 1) {
636
- made.noteProduced(PICTURE, index);
637
- }
638
- watches("two", { atSeconds: 3000 });
639
- made.reconcile();
640
- for (const run of made.runsOn(PICTURE)) {
641
- run.noteSpeed(6);
642
- }
643
- const before = made.runsOn(PICTURE).length;
644
-
645
- // The far one comes back to where the first one has already been.
646
- watches("two", { atSeconds: 500 });
647
- made.reconcile();
648
-
649
- assert.ok(made.runsOn(PICTURE).length <= before,
650
- "coming back onto made film buys nobody an encoder");
651
- assertNoOverlap(made);
652
- });
653
-
654
- test("three viewers: one forward, one back, one paused", () => {
655
- // All three motions at once, which is the state a real proxy is in most of the
656
- // time. What must hold is what always must: never two encoders on one number,
657
- // never more than the machine affords, and somebody making what the moving
658
- // viewers are about to need.
659
- const { made, watches } = orchestrator({ maxRuns: 3 });
660
- watches("one", { atSeconds: 400 });
661
- watches("two", { atSeconds: 2000 });
662
- watches("three", { atSeconds: 3600 });
663
- made.reconcile();
664
- for (const run of made.runsOn(PICTURE)) {
665
- run.noteSpeed(6);
666
- }
667
- made.reconcile();
668
-
669
- watches("one", { atSeconds: 800 });
670
- watches("two", { atSeconds: 1200 });
671
- watches("three", { atSeconds: 3600, playing: false });
672
- made.reconcile();
673
-
674
- assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
675
- assertNoOverlap(made);
676
- const coverage = made.coverageOf(PICTURE);
677
- assert.equal(coverage.stateOf(200), "making", "the one who went forward is served");
678
- assert.equal(coverage.stateOf(300), "making", "and so is the one who came back");
679
- });
680
-
681
- test("a viewer scrubbing back and forth does not accumulate encoders", () => {
682
- // A person dragging the time bar states a new position every few hundred
683
- // milliseconds, and each of those is a state the plan answers. What must hold
684
- // is that the answers do not pile up: an encoder bought for a place the viewer
685
- // passed through is not still running when they have gone back.
686
- //
687
- // Not that the arrangement is identical to the one before the scrub — the
688
- // model does not promise that and nothing here should claim it. What it
689
- // promises is that no arrangement costs more than it is worth.
690
- const { made, watches } = orchestrator({ maxRuns: 3 });
691
- watches("one", { atSeconds: 400 });
692
- made.reconcile();
693
- made.runsOn(PICTURE)[0].noteSpeed(6);
694
- made.noteProduced(PICTURE, 100);
695
- made.reconcile();
696
- const settled = placements(made);
697
-
698
- watches("one", { atSeconds: 2000 });
699
- made.reconcile();
700
- for (const run of made.runsOn(PICTURE)) {
701
- run.noteSpeed(6);
702
- }
703
- watches("one", { atSeconds: 404 });
704
- made.reconcile();
705
- watches("one", { atSeconds: 400 });
706
- made.reconcile();
707
- made.reconcile();
708
-
709
- // Not "no more encoders" — the machine is allowed to spend what it has spare on
710
- // finishing the file, and after a scrub there is more of the file it has seen.
711
- // What must not happen is two of them on one stretch, or more than the machine
712
- // holds, or the viewer left unserved.
713
- assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
714
- assert.equal(onTheStretchOf(made, 105), 1, "one encoder on the film in front of them");
715
- assert.equal(made.coverageOf(PICTURE).stateOf(105), "making", "and the viewer is served");
716
- assertNoOverlap(made);
717
- void settled;
718
- });
1
+ /**
2
+ * @file The placement model under one, two and three viewers, each of them
3
+ * playing, seeking and paused.
4
+ *
5
+ * THE MODEL, restated so a failure here can be read against it:
6
+ *
7
+ * - each segment number carries a deadline `D(x)`, the seconds until somebody
8
+ * needs it. A viewer moving forward covers a second of film in a second, so
9
+ * `D(x)` is the distance to them. `Infinity` where nobody is coming;
10
+ * - an encoder placed at `a` delivers `a + j` at `(j + 1) / r`, `r` segments per
11
+ * second. The same expression answers "when would the one already placed get
12
+ * here";
13
+ * - a number is late when it arrives after its deadline. Placement is first-fit
14
+ * left to right: give it to an encoder that arrives in time, else open one
15
+ * exactly there.
16
+ *
17
+ * Three behaviours of the map follow from that and are checked here rather than
18
+ * assumed. A peak MOVES FORWARD on its own, because the deadline is a distance
19
+ * and the distance shrinks as the viewer watches. It JUMPS on a seek. It
20
+ * FLATTENS on a pause, because a viewer who is not moving has no time by which
21
+ * anything must exist.
22
+ *
23
+ * Nothing here spawns ffmpeg, touches a disk or reads a clock. That is the layer
24
+ * check, made executable: this layer is exercised with plain values alone.
25
+ */
26
+
27
+ import test from "node:test";
28
+ import assert from "node:assert/strict";
29
+ import { EventEmitter } from "node:events";
30
+ import { EncodeRun } from "../services/encode/EncodeRun.js";
31
+ import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
32
+ import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
33
+ import { mapForViewer, mergeMaps, runsOf } from "../services/viewer/PriorityMap.js";
34
+ import { penaltiesFrom } from "../services/encode/contention.js";
35
+
36
+ // WHAT A SECOND ENCODER COSTS THE FIRST — measured, never a formula.
37
+ //
38
+ // Addon host, 2026-09-03: 854x480 through libx264 `ultrafast` ran at 7.12x with
39
+ // the machine to itself, and at 4.20x and 4.16x when two ran at once. The
40
+ // penalty is read off that reading by the SAME two functions production uses,
41
+ // so nothing here invents a shape: beyond what was measured the reading is held
42
+ // rather than extrapolated.
43
+ const MEASURED_PENALTIES = penaltiesFrom(7.12, [{ others: 1, speed: 4.18 }]);
44
+
45
+ // What a start and a stop cost, measured on the same host: a spawn with its
46
+ // input open is 0.12 s there.
47
+ const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
48
+
49
+
50
+ const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
51
+ const SEGMENT_SECONDS = 4;
52
+ const FILM_SECONDS = 4000;
53
+ const SEGMENTS = FILM_SECONDS / SEGMENT_SECONDS;
54
+
55
+ class FakeProcess extends EventEmitter {
56
+ constructor() {
57
+ super();
58
+ this.pid = 1;
59
+ }
60
+
61
+ kill(signal) {
62
+ this.emit("close", null, signal);
63
+ }
64
+ }
65
+
66
+ /** @param {{ maxRuns?: number }} [options] */
67
+ function orchestrator({ maxRuns = 3 } = {}) {
68
+ /** @type {EncodeOrchestrator} */
69
+ let made;
70
+ const build = ({ address, from, to }) => new EncodeRun({
71
+ address,
72
+ encoder: new SoftwareEncoder(),
73
+ from,
74
+ to,
75
+ buildArgs: () => ["-i", "in", "out"],
76
+ spawn: () => new FakeProcess(),
77
+ logger: { info() {}, warn() {} },
78
+ now: () => 1000,
79
+ onEnded: (ended) => made.noteEnded(ended)
80
+ });
81
+ made = new EncodeOrchestrator({
82
+ maxRunsFor: () => maxRuns,
83
+ segmentSeconds: SEGMENT_SECONDS,
84
+ ...RUN_COSTS,
85
+ // A swarm with room for several encoders. The figure is the measured cost
86
+ // of fetching a second of film again, in seconds of swarm time; at 0.25 one
87
+ // encoder at 6x already takes one and a half swarms, so the budget would be
88
+ // one process and the placement would have nothing to place — which is a
89
+ // real limit, checked in its own test below, and not the subject of these.
90
+ refetchSecPerFilmSecond: () => 0.02,
91
+ // What the startup benchmark says this host encodes at. It exists before
92
+ // any viewer, so the plan is never without a speed.
93
+ startingSpeedFor: () => 2,
94
+ // What a second encoder costs the first, measured on the addon host. The
95
+ // orchestrator reads the table with the same pure function production uses.
96
+ // Without it an extra process is free and the score always wants more of
97
+ // them — and fewer encoders can genuinely finish sooner.
98
+ contentionPenalties: MEASURED_PENALTIES,
99
+ now: () => 1000,
100
+ logger: { info() {}, warn() {} },
101
+ makeRun: build
102
+ });
103
+ made.setSegmentCount(PICTURE, SEGMENTS);
104
+ /** Who is watching, by name. The map is rebuilt from all of them on change. */
105
+ const watching = new Map();
106
+ const watches = (who, { atSeconds, playing = true }) => {
107
+ watching.set(who, { atSeconds, playing });
108
+ stateMap(made, watching);
109
+ };
110
+ const leaves = (who) => {
111
+ watching.delete(who);
112
+ stateMap(made, watching);
113
+ };
114
+ return { made, build, watches, leaves };
115
+ }
116
+
117
+ /**
118
+ * Everybody watching, as ONE map, the way the product builds it.
119
+ *
120
+ * A viewer's own map is in seconds of film; the maps of all of them are merged
121
+ * into one, and only then converted into an output's own numbering. Doing it
122
+ * here rather than stating windows by hand is the point: the pause and the seek
123
+ * are the MAP's behaviour, and a test that stated windows directly would be
124
+ * checking its own arithmetic.
125
+ *
126
+ * @param {EncodeOrchestrator} made
127
+ * @param {Map<string, { atSeconds: number, playing: boolean }>} watching
128
+ */
129
+ function stateMap(made, watching) {
130
+ const map = mergeMaps(
131
+ [...watching.values()].map((viewer) => mapForViewer({
132
+ atSeconds: viewer.atSeconds,
133
+ durationSeconds: FILM_SECONDS,
134
+ allowanceSeconds: 8,
135
+ playing: viewer.playing
136
+ }))
137
+ );
138
+ // Seconds of film into this output's own piece numbers. The product does it
139
+ // with the cut table, which knows where the pieces fall; here the pieces are a
140
+ // uniform grid, so it is a division — and doing it at all is the point, since
141
+ // the map is per FILM and every consumer numbers differently.
142
+ made.notePriorityMap(PICTURE, runsOf(map).map((zone) => ({
143
+ from: Math.floor(zone.from / SEGMENT_SECONDS),
144
+ to: Math.max(
145
+ Math.floor(zone.from / SEGMENT_SECONDS),
146
+ Math.ceil(zone.to / SEGMENT_SECONDS) - 1
147
+ ),
148
+ priority: zone.priority,
149
+ withinSeconds: zone.withinSeconds,
150
+ behind: zone.behind
151
+ })));
152
+ }
153
+
154
+ /**
155
+ * How many encoders are working the stretch around one number.
156
+ *
157
+ * The count that matters for a viewer is not how many exist — spare capacity is
158
+ * meant to be spent finishing the film, so a machine that holds three should be
159
+ * holding three — but how many are crowded onto the place they are standing.
160
+ *
161
+ * @param {EncodeOrchestrator} made
162
+ * @param {number} segment
163
+ */
164
+ function onTheStretchOf(made, segment) {
165
+ return made.runsOn(PICTURE).filter((run) => {
166
+ const to = run.to < run.from ? Number.POSITIVE_INFINITY : run.to;
167
+ return run.from <= segment && segment <= to;
168
+ }).length;
169
+ }
170
+
171
+ /** @param {EncodeOrchestrator} made */
172
+ function placements(made) {
173
+ return made.runsOn(PICTURE).map((run) => run.from).sort((left, right) => left - right);
174
+ }
175
+
176
+ /**
177
+ * Every live encoder's stretch, so that "they never share a number" can be
178
+ * asserted as the consequence it is rather than trusted.
179
+ *
180
+ * @param {EncodeOrchestrator} made
181
+ */
182
+ function assertNoOverlap(made) {
183
+ const spans = made.runsOn(PICTURE)
184
+ .map((run) => [run.from, run.to < run.from ? Number.POSITIVE_INFINITY : run.to])
185
+ .sort((left, right) => left[0] - right[0]);
186
+ for (let index = 0; index < spans.length - 1; index += 1) {
187
+ assert.ok(
188
+ spans[index][1] < spans[index + 1][0],
189
+ `#${spans[index][0]}..#${spans[index][1]} overlaps #${spans[index + 1][0]}`
190
+ );
191
+ }
192
+ }
193
+
194
+ // ---------------------------------------------------------------- one viewer
195
+
196
+ test("one viewer playing: one encoder, exactly where they are", () => {
197
+ const { made, watches } = orchestrator();
198
+ watches("one", { atSeconds: 400 });
199
+ made.reconcile();
200
+ assert.deepEqual(placements(made), [100], "at their own position, 400s / 4s");
201
+ assertNoOverlap(made);
202
+ });
203
+
204
+ test("one viewer playing: the encoder keeping up buys no second one", () => {
205
+ // The peak moves forward on its own as they watch, and an encoder that stays
206
+ // in front of it is never late. This is the case that must NOT spend a
207
+ // process, and the one the old head-as-a-barrier rule got wrong.
208
+ const { made, watches } = orchestrator();
209
+ watches("one", { atSeconds: 400 });
210
+ made.reconcile();
211
+ const [run] = made.runsOn(PICTURE);
212
+ run.noteSpeed(6);
213
+ for (let index = 100; index < 130; index += 1) {
214
+ made.noteProduced(PICTURE, index);
215
+ }
216
+ watches("one", { atSeconds: 480 });
217
+ made.reconcile();
218
+ // Asked of the first number nobody has, not of the one they are standing on:
219
+ // that one is already made, and nothing needs to be covering it.
220
+ assert.equal(onTheStretchOf(made, 130), 1, "one encoder on the film in front of them");
221
+ assertNoOverlap(made);
222
+ });
223
+
224
+ test("one viewer seeking far ahead: an encoder is placed there", () => {
225
+ const { made, watches } = orchestrator();
226
+ watches("one", { atSeconds: 400 });
227
+ made.reconcile();
228
+ made.runsOn(PICTURE)[0].noteSpeed(6);
229
+ made.noteProduced(PICTURE, 100);
230
+
231
+ // The peak JUMPS. Nothing about the old place is wanted now.
232
+ watches("one", { atSeconds: 3000 });
233
+ made.reconcile();
234
+
235
+ assert.ok(placements(made).includes(750), "an encoder where they landed, 3000s / 4s");
236
+ assertNoOverlap(made);
237
+ });
238
+
239
+ test("one viewer paused: nothing is late, so no encoder is added", () => {
240
+ // A paused viewer states the whole film at one undifferentiated rank and no
241
+ // time by which any of it must exist. The encoder already working goes on
242
+ // encoding the track; nothing justifies a second process.
243
+ const { made, watches } = orchestrator();
244
+ watches("one", { atSeconds: 400 });
245
+ made.reconcile();
246
+ made.runsOn(PICTURE)[0].noteSpeed(1);
247
+ made.noteProduced(PICTURE, 100);
248
+
249
+ watches("one", { atSeconds: 404, playing: false });
250
+ made.reconcile();
251
+
252
+ assert.equal(onTheStretchOf(made, 101), 1, "one encoder where they are standing");
253
+ assertNoOverlap(made);
254
+ });
255
+
256
+ test("one viewer paused states no deadline anywhere", () => {
257
+ // The map's own answer, checked directly, because every placement decision
258
+ // below rests on it.
259
+ const map = mapForViewer({
260
+ atSeconds: 400,
261
+ durationSeconds: FILM_SECONDS,
262
+ allowanceSeconds: 8,
263
+ playing: false
264
+ });
265
+ assert.ok(map.durationSeconds > 0, "a paused viewer still wants the film");
266
+ for (let second = 0; second < map.durationSeconds; second += 1) {
267
+ assert.equal(map.secondsUntilPlayed[second], Number.POSITIVE_INFINITY,
268
+ "but no second of it has a time by which it must exist");
269
+ }
270
+ // Their POSITION survives the pause, and with it the rule that what is in
271
+ // front of them is made before what is behind.
272
+ assert.equal(map.behind[399], 1, "what they have watched is behind them");
273
+ assert.equal(map.behind[400], 0, "and what they have not is in front");
274
+ assert.ok(map.priority[400] > map.priority[399], "which is what the numbers say");
275
+ });
276
+
277
+ // --------------------------------------------------------------- two viewers
278
+
279
+ test("two viewers close together share one encoder", () => {
280
+ // Film both of them want is made once. This is what merging the map is for,
281
+ // and it must survive the deadline being carried alongside the rank.
282
+ const { made, watches } = orchestrator();
283
+ watches("one", { atSeconds: 400 });
284
+ watches("two", { atSeconds: 408 });
285
+ made.reconcile();
286
+ // One encoder for the pair — asked of the film they are both about to watch.
287
+ // The machine may well be running others further on: what it has spare goes to
288
+ // finishing the file, and that is a different question from crowding.
289
+ assert.equal(onTheStretchOf(made, 100), 1, "one encoder for the pair");
290
+ assert.equal(onTheStretchOf(made, 102), 1, "and one for the film just in front of them");
291
+ assertNoOverlap(made);
292
+ });
293
+
294
+ test("two viewers far apart get an encoder each", () => {
295
+ const { made, watches } = orchestrator();
296
+ watches("one", { atSeconds: 400 });
297
+ made.reconcile();
298
+ made.runsOn(PICTURE)[0].noteSpeed(6);
299
+ made.noteProduced(PICTURE, 100);
300
+
301
+ watches("two", { atSeconds: 3000 });
302
+ made.reconcile();
303
+
304
+ assert.ok(placements(made).includes(750), "the far one is served where they stand");
305
+ assert.equal(onTheStretchOf(made, 750), 1, "and by one encoder, not a crowd");
306
+ assertNoOverlap(made);
307
+ });
308
+
309
+ test("two viewers: one seeking does not take the other's encoder", () => {
310
+ const { made, watches } = orchestrator();
311
+ watches("one", { atSeconds: 400 });
312
+ made.reconcile();
313
+ // A speed has to be measured before a second encoder can be justified: how
314
+ // long one takes to reach a number is the whole comparison. Real life
315
+ // measures it from the first run; a test that stated both viewers before any
316
+ // run existed would be asking the plan to buy a process on no evidence.
317
+ made.runsOn(PICTURE)[0].noteSpeed(6);
318
+ made.noteProduced(PICTURE, 100);
319
+ watches("two", { atSeconds: 3000 });
320
+ made.reconcile();
321
+ for (const run of made.runsOn(PICTURE)) {
322
+ run.noteSpeed(6);
323
+ }
324
+ assert.ok(made.runsOn(PICTURE).length >= 2, "one each to begin with");
325
+
326
+ // The far one seeks somewhere else entirely.
327
+ watches("two", { atSeconds: 2000 });
328
+ made.reconcile();
329
+
330
+ // Asked of the coverage, not of a run's first number: a run whose road was
331
+ // shortened begins again at its own head, so "is this viewer served" is a
332
+ // question about the map and never about where a process happens to start.
333
+ const coverage = made.coverageOf(PICTURE);
334
+ assert.equal(coverage.stateOf(105), "making", "the one who did not move is still served");
335
+ assert.ok(placements(made).includes(500), "and the one who moved is served where they landed");
336
+ assertNoOverlap(made);
337
+ });
338
+
339
+ test("two viewers: one pausing leaves the other served", () => {
340
+ const { made, watches } = orchestrator();
341
+ watches("one", { atSeconds: 400 });
342
+ watches("two", { atSeconds: 3000 });
343
+ made.reconcile();
344
+ for (const run of made.runsOn(PICTURE)) {
345
+ run.noteSpeed(1);
346
+ }
347
+
348
+ watches("two", { atSeconds: 3000, playing: false });
349
+ made.reconcile();
350
+
351
+ assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
352
+ "the one still watching keeps their encoder");
353
+ assertNoOverlap(made);
354
+ });
355
+
356
+ test("two viewers: the one who leaves takes nothing from the one who stays", () => {
357
+ const { made, watches, leaves } = orchestrator();
358
+ watches("one", { atSeconds: 400 });
359
+ watches("two", { atSeconds: 3000 });
360
+ made.reconcile();
361
+ for (const run of made.runsOn(PICTURE)) {
362
+ run.noteSpeed(1);
363
+ }
364
+
365
+ leaves("two");
366
+ made.reconcile();
367
+
368
+ assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
369
+ "the one who stayed is still served");
370
+ assertNoOverlap(made);
371
+ });
372
+
373
+ // ------------------------------------------------------------- three viewers
374
+
375
+ test("three viewers far apart get an encoder each when the machine affords it", () => {
376
+ const { made, watches } = orchestrator({ maxRuns: 3 });
377
+ watches("one", { atSeconds: 400 });
378
+ watches("two", { atSeconds: 2000 });
379
+ watches("three", { atSeconds: 3600 });
380
+ made.reconcile();
381
+ for (const run of made.runsOn(PICTURE)) {
382
+ run.noteSpeed(6);
383
+ }
384
+ made.reconcile();
385
+
386
+ const where = placements(made);
387
+ for (const at of [100, 500, 900]) {
388
+ assert.ok(where.includes(at), `somebody is making what the viewer at #${at} needs`);
389
+ assert.equal(onTheStretchOf(made, at), 1, `and one encoder there, not a crowd`);
390
+ }
391
+ assertNoOverlap(made);
392
+ });
393
+
394
+ test("three viewers, a machine that affords two: the budget binds, not the map", () => {
395
+ // The number of encoders is the smaller of what the map asks and what the
396
+ // machine can hold. Which two are served follows from the order the work is
397
+ // taken in; what must not happen is a third process on a host that cannot
398
+ // hold it, or two processes writing one number.
399
+ const { made, watches } = orchestrator({ maxRuns: 2 });
400
+ watches("one", { atSeconds: 400 });
401
+ watches("two", { atSeconds: 2000 });
402
+ watches("three", { atSeconds: 3600 });
403
+ made.reconcile();
404
+ for (const run of made.runsOn(PICTURE)) {
405
+ run.noteSpeed(1);
406
+ }
407
+ made.reconcile();
408
+
409
+ assert.ok(made.runsOn(PICTURE).length <= 2, "never more than the machine affords");
410
+ assert.ok(made.runsOn(PICTURE).length >= 1, "and never nothing while people wait");
411
+ assertNoOverlap(made);
412
+ });
413
+
414
+ test("three viewers: one seeks onto another, and the two of them share", () => {
415
+ const { made, watches } = orchestrator({ maxRuns: 3 });
416
+ watches("one", { atSeconds: 400 });
417
+ made.reconcile();
418
+ made.runsOn(PICTURE)[0].noteSpeed(6);
419
+ made.noteProduced(PICTURE, 100);
420
+ watches("two", { atSeconds: 2000 });
421
+ watches("three", { atSeconds: 3600 });
422
+ made.reconcile();
423
+ for (const run of made.runsOn(PICTURE)) {
424
+ run.noteSpeed(6);
425
+ }
426
+ made.reconcile();
427
+ assert.equal(made.runsOn(PICTURE).length, 3);
428
+
429
+ // The third joins the second. Two peaks where there were three.
430
+ watches("three", { atSeconds: 2008 });
431
+ made.reconcile();
432
+
433
+ assert.ok(made.runsOn(PICTURE).length <= 3, "no process is added by people converging");
434
+ assertNoOverlap(made);
435
+ });
436
+
437
+ test("three viewers: all paused, and no encoder is added for any of them", () => {
438
+ const { made, watches } = orchestrator({ maxRuns: 3 });
439
+ watches("one", { atSeconds: 400 });
440
+ made.reconcile();
441
+ made.runsOn(PICTURE)[0].noteSpeed(1);
442
+ const before = made.runsOn(PICTURE).length;
443
+
444
+ watches("one", { atSeconds: 400, playing: false });
445
+ watches("two", { atSeconds: 2000, playing: false });
446
+ watches("three", { atSeconds: 3600, playing: false });
447
+ made.reconcile();
448
+
449
+ // Nobody is coming anywhere, so nothing can be late — and the machine does not
450
+ // therefore fall idle: what it has spare goes on finishing the file, which is
451
+ // what makes a seek back into a made part start playing at once.
452
+ assert.ok(made.runsOn(PICTURE).length >= before, "the file goes on being finished");
453
+ assertNoOverlap(made);
454
+ });
455
+
456
+ test("the plan is a function of the state: the same state twice gives the same answer", () => {
457
+ // What stops a moving map from becoming a thrash: the decision is arithmetic
458
+ // over the state, so a pass that finds nothing changed changes nothing.
459
+ const { made, watches } = orchestrator({ maxRuns: 3 });
460
+ watches("one", { atSeconds: 400 });
461
+ watches("two", { atSeconds: 2000 });
462
+ made.reconcile();
463
+ for (const run of made.runsOn(PICTURE)) {
464
+ run.noteSpeed(1);
465
+ }
466
+ made.reconcile();
467
+ const first = placements(made);
468
+ made.reconcile();
469
+ made.reconcile();
470
+ assert.deepEqual(placements(made), first, "three more passes moved nothing");
471
+ });
472
+
473
+ // ---------------------------------------------------------------- the edge
474
+
475
+ test("exactly realtime arrives exactly on time, and no second encoder is bought", () => {
476
+ // The knife edge, and it lands on "in time" by construction: the deadline of
477
+ // a number is the distance to it, and an encoder at 1.0x covers that distance
478
+ // in exactly that time. So the arithmetic says nothing is late and nothing is
479
+ // bought. Worth pinning, because the formula this model replaced said
480
+ // something quite different — `speed / (1 - speed)` divides by zero here and
481
+ // claims the encoder stays ahead FOR EVER, which is the same answer arrived
482
+ // at by nonsense.
483
+ const { made, watches } = orchestrator({ maxRuns: 3 });
484
+ watches("one", { atSeconds: 400 });
485
+ made.reconcile();
486
+ made.runsOn(PICTURE)[0].noteSpeed(1);
487
+ made.noteProduced(PICTURE, 100);
488
+ made.reconcile();
489
+
490
+ assert.equal(onTheStretchOf(made, 150), 1,
491
+ "just in time everywhere, so one encoder on the viewer's stretch is enough");
492
+ assertNoOverlap(made);
493
+ });
494
+
495
+ test("below realtime, whether a second encoder helps is arithmetic, and here it does not", () => {
496
+ // The side of the edge that matters, and the answer is not the one this check
497
+ // asserted when it was written. An encoder at half realtime loses ground on
498
+ // the viewer every second, so everything ahead of it is late — and a second
499
+ // process does not fix that, because on this host each of two runs at 1.70x
500
+ // slower than one. The near film gets worse by more than the far film gets
501
+ // better, so the total seconds of waiting go UP, and the score refuses.
502
+ //
503
+ // What this case actually needs is the answer the user gave for it: a machine
504
+ // that cannot serve its viewers without stopping the picture says so, and the
505
+ // viewer is moved to another proxy. That is not built. Adding processes is not
506
+ // a substitute for it and the model correctly declines to pretend otherwise.
507
+ const { made, watches, leaves } = orchestrator({ maxRuns: 3 });
508
+ watches("one", { atSeconds: 400 });
509
+ made.reconcile();
510
+ made.runsOn(PICTURE)[0].noteSpeed(0.5);
511
+ made.noteProduced(PICTURE, 100);
512
+ made.reconcile();
513
+
514
+ assert.equal(made.runsOn(PICTURE).length, 1,
515
+ "one encoder, because two of them would leave the viewer waiting longer");
516
+ assert.equal(made.coverageOf(PICTURE).stateOf(101), "making",
517
+ "and it is on the film in front of them");
518
+ assertNoOverlap(made);
519
+ void leaves;
520
+ });
521
+
522
+ test("two viewers arriving together on a cold output get one encoder, then are measured", () => {
523
+ // Nothing has measured how fast this machine encodes, so how long one encoder
524
+ // would take to reach the second viewer is not a known quantity. Buying a
525
+ // process on that is buying it on no evidence, which is refused; one is placed
526
+ // and the speed it reports is what justifies the next.
527
+ const { made, watches } = orchestrator({ maxRuns: 3 });
528
+ watches("one", { atSeconds: 400 });
529
+ watches("two", { atSeconds: 3000 });
530
+ made.reconcile();
531
+ assert.ok(made.runsOn(PICTURE).length >= 1, "at least one, until something is measured");
532
+ assert.equal(onTheStretchOf(made, 100), 1, "and not two on the same place");
533
+
534
+ made.runsOn(PICTURE)[0].noteSpeed(6);
535
+ made.noteProduced(PICTURE, 100);
536
+ made.reconcile();
537
+ assert.ok(placements(made).includes(750), "and now the far one is served too");
538
+ assertNoOverlap(made);
539
+ });
540
+
541
+ test("an encoder comfortably faster than realtime is left to do the whole stretch", () => {
542
+ const { made, watches } = orchestrator({ maxRuns: 3 });
543
+ watches("one", { atSeconds: 400 });
544
+ made.reconcile();
545
+ made.runsOn(PICTURE)[0].noteSpeed(6);
546
+ made.noteProduced(PICTURE, 100);
547
+ made.reconcile();
548
+
549
+ assert.equal(onTheStretchOf(made, 150), 1, "one encoder on the stretch they are watching");
550
+ assertNoOverlap(made);
551
+ });
552
+
553
+ test("a swarm that feeds one encoder moves it to whoever is late, rather than serving nobody", () => {
554
+ // The budget binds. The one encoder there is stands where nothing is due —
555
+ // the viewer left — so it is moved to the soonest number that IS due. Without
556
+ // this the viewer who seeked was served by nobody at all: the run kept its
557
+ // road because nothing covered what lay in front of IT.
558
+ const { made, watches } = orchestrator({ maxRuns: 3 });
559
+ watches("one", { atSeconds: 400 });
560
+ made.reconcile();
561
+ const [run] = made.runsOn(PICTURE);
562
+ // 6x against a swarm charging 0.25 s per second of film: one encoder already
563
+ // takes one and a half of what is delivered, so the budget is one.
564
+ run.noteSpeed(6);
565
+ made.refetchSecPerFilmSecond = () => 0.25;
566
+ made.noteProduced(PICTURE, 100);
567
+
568
+ watches("one", { atSeconds: 3000 });
569
+ made.reconcile();
570
+
571
+ assert.equal(made.runsOn(PICTURE).length, 1, "still one, because that is what the swarm feeds");
572
+ assert.deepEqual(placements(made), [750], "and it is where the viewer now stands");
573
+ assertNoOverlap(made);
574
+ });
575
+
576
+ // ------------------------------------------------------- seeking BACKWARD
577
+
578
+ test("one viewer seeking back into film that exists is served from it, with no encoder", () => {
579
+ // The case the whole layer was built for. Everything behind them has been
580
+ // made, so nothing is late anywhere they are going, and the score says the
581
+ // cheapest arrangement is the one that changes nothing. A restart here is the
582
+ // 647-second stall of 2026-09-06 in miniature.
583
+ const { made, watches } = orchestrator();
584
+ watches("one", { atSeconds: 400 });
585
+ made.reconcile();
586
+ made.runsOn(PICTURE)[0].noteSpeed(6);
587
+ for (let index = 100; index <= 160; index += 1) {
588
+ made.noteProduced(PICTURE, index);
589
+ }
590
+ const before = made.runsOn(PICTURE);
591
+
592
+ watches("one", { atSeconds: 440 });
593
+ made.reconcile();
594
+
595
+ // Not "the count is unchanged": with nothing late anywhere, the machine is
596
+ // free to spend what it has spare on finishing the file, and that is what the
597
+ // user asked for. What must not happen is a process put on film that already
598
+ // exists, or the run they were behind restarted — which is the 647-second
599
+ // stall of 2026-09-06 in miniature.
600
+ for (const was of before) {
601
+ assert.ok(made.runsOn(PICTURE).includes(was),
602
+ "the encoder they were behind is not restarted");
603
+ }
604
+ for (const run_ of made.runsOn(PICTURE)) {
605
+ // Where it is WORKING, not where it was created: the surviving run was
606
+ // started at #100 and has since produced up to #161.
607
+ assert.equal(made.coverageOf(PICTURE).isReady(run_.head), false,
608
+ `no process is bought for film that exists (#${run_.head})`);
609
+ }
610
+ assertNoOverlap(made);
611
+ });
612
+
613
+ test("one viewer seeking back into film nobody has gets an encoder there", () => {
614
+ // Behind them is not the same as made. Where the film was never encoded, going
615
+ // back is exactly as bare as going forward, and the arithmetic is the same one.
616
+ const { made, watches } = orchestrator();
617
+ watches("one", { atSeconds: 3000 });
618
+ made.reconcile();
619
+ made.runsOn(PICTURE)[0].noteSpeed(6);
620
+ made.noteProduced(PICTURE, 750);
621
+
622
+ watches("one", { atSeconds: 400 });
623
+ made.reconcile();
624
+
625
+ const coverage = made.coverageOf(PICTURE);
626
+ assert.equal(coverage.stateOf(100), "making", "somebody is making where they landed");
627
+ assertNoOverlap(made);
628
+ });
629
+
630
+ test("two viewers: one seeks back onto film the other already had made", () => {
631
+ const { made, watches } = orchestrator({ maxRuns: 3 });
632
+ watches("one", { atSeconds: 400 });
633
+ made.reconcile();
634
+ made.runsOn(PICTURE)[0].noteSpeed(6);
635
+ for (let index = 100; index <= 200; index += 1) {
636
+ made.noteProduced(PICTURE, index);
637
+ }
638
+ watches("two", { atSeconds: 3000 });
639
+ made.reconcile();
640
+ for (const run of made.runsOn(PICTURE)) {
641
+ run.noteSpeed(6);
642
+ }
643
+ const before = made.runsOn(PICTURE).length;
644
+
645
+ // The far one comes back to where the first one has already been.
646
+ watches("two", { atSeconds: 500 });
647
+ made.reconcile();
648
+
649
+ assert.ok(made.runsOn(PICTURE).length <= before,
650
+ "coming back onto made film buys nobody an encoder");
651
+ assertNoOverlap(made);
652
+ });
653
+
654
+ test("three viewers: one forward, one back, one paused", () => {
655
+ // All three motions at once, which is the state a real proxy is in most of the
656
+ // time. What must hold is what always must: never two encoders on one number,
657
+ // never more than the machine affords, and somebody making what the moving
658
+ // viewers are about to need.
659
+ const { made, watches } = orchestrator({ maxRuns: 3 });
660
+ watches("one", { atSeconds: 400 });
661
+ watches("two", { atSeconds: 2000 });
662
+ watches("three", { atSeconds: 3600 });
663
+ made.reconcile();
664
+ for (const run of made.runsOn(PICTURE)) {
665
+ run.noteSpeed(6);
666
+ }
667
+ made.reconcile();
668
+
669
+ watches("one", { atSeconds: 800 });
670
+ watches("two", { atSeconds: 1200 });
671
+ watches("three", { atSeconds: 3600, playing: false });
672
+ made.reconcile();
673
+
674
+ assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
675
+ assertNoOverlap(made);
676
+ const coverage = made.coverageOf(PICTURE);
677
+ assert.equal(coverage.stateOf(200), "making", "the one who went forward is served");
678
+ assert.equal(coverage.stateOf(300), "making", "and so is the one who came back");
679
+ });
680
+
681
+ test("a viewer scrubbing back and forth does not accumulate encoders", () => {
682
+ // A person dragging the time bar states a new position every few hundred
683
+ // milliseconds, and each of those is a state the plan answers. What must hold
684
+ // is that the answers do not pile up: an encoder bought for a place the viewer
685
+ // passed through is not still running when they have gone back.
686
+ //
687
+ // Not that the arrangement is identical to the one before the scrub — the
688
+ // model does not promise that and nothing here should claim it. What it
689
+ // promises is that no arrangement costs more than it is worth.
690
+ const { made, watches } = orchestrator({ maxRuns: 3 });
691
+ watches("one", { atSeconds: 400 });
692
+ made.reconcile();
693
+ made.runsOn(PICTURE)[0].noteSpeed(6);
694
+ made.noteProduced(PICTURE, 100);
695
+ made.reconcile();
696
+ const settled = placements(made);
697
+
698
+ watches("one", { atSeconds: 2000 });
699
+ made.reconcile();
700
+ for (const run of made.runsOn(PICTURE)) {
701
+ run.noteSpeed(6);
702
+ }
703
+ watches("one", { atSeconds: 404 });
704
+ made.reconcile();
705
+ watches("one", { atSeconds: 400 });
706
+ made.reconcile();
707
+ made.reconcile();
708
+
709
+ // Not "no more encoders" — the machine is allowed to spend what it has spare on
710
+ // finishing the file, and after a scrub there is more of the file it has seen.
711
+ // What must not happen is two of them on one stretch, or more than the machine
712
+ // holds, or the viewer left unserved.
713
+ assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
714
+ assert.equal(onTheStretchOf(made, 105), 1, "one encoder on the film in front of them");
715
+ assert.equal(made.coverageOf(PICTURE).stateOf(105), "making", "and the viewer is served");
716
+ assertNoOverlap(made);
717
+ void settled;
718
+ });