@torrent-tv/proxy 2.86.0 → 2.87.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 (382) hide show
  1. package/CHANGELOG.md +2138 -1915
  2. package/CLAUDE.md +318 -197
  3. package/assets/calibration/NOTICE.md +2 -2
  4. package/bin/cli.js +684 -616
  5. package/biome.json +409 -233
  6. package/docs/container-architecture.md +218 -213
  7. package/docs/download-architecture.md +7 -7
  8. package/docs/encode-architecture.md +287 -11
  9. package/docs/encode-run-state.md +2 -2
  10. package/docs/logs.md +97 -97
  11. package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
  12. package/openspec/changes/adaptive-bitrate/design.md +2 -2
  13. package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
  14. package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
  15. package/openspec/changes/chunked-request-bodies/design.md +1 -1
  16. package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
  17. package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
  18. package/openspec/changes/cold-start/design.md +1 -1
  19. package/openspec/changes/disk-cap/proposal.md +1 -1
  20. package/openspec/changes/proxy-observability/proposal.md +2 -2
  21. package/openspec/changes/track-selection/proposal.md +1 -1
  22. package/openspec/changes/transcode-quality/proposal.md +1 -1
  23. package/package.json +2 -2
  24. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  25. package/research/double-spawn-2026-09-10.md +171 -171
  26. package/routes/api/playback-plan/post.js +2 -2
  27. package/routes/api/sources/files/get.js +1 -1
  28. package/routes/api/sources/stats/get.js +1 -1
  29. package/routes/api/sources/warm/post.js +10 -3
  30. package/routes/api/subtitles/get.js +230 -230
  31. package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
  32. package/routes/api/transcode-sessions/net-report/post.js +129 -94
  33. package/routes/api/transcode-sessions/post.js +160 -139
  34. package/routes/api/transcode-sessions/progress/get.js +3 -3
  35. package/routes/api/transcode-sessions/release/post.js +3 -3
  36. package/routes/api/transcode-sessions/seek/post.js +12 -3
  37. package/routes/stream/get.js +2 -2
  38. package/routes/transcode/audio-file/get.js +15 -6
  39. package/routes/transcode/audio-warm/get.js +8 -5
  40. package/routes/transcode/session-file/get.js +429 -268
  41. package/routes/transcode/variant-file/get.js +47 -7
  42. package/routes/transcode/variant-warm/get.js +18 -6
  43. package/scripts/render-run-graph.js +1 -1
  44. package/server.js +720 -465
  45. package/services/encode/CoverageMap.js +428 -428
  46. package/services/encode/CushionReport.js +345 -0
  47. package/services/encode/EncodeAdmission.js +332 -0
  48. package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
  49. package/services/encode/EncodeRun.js +16 -9
  50. package/services/encode/EncodeRuns.js +1219 -0
  51. package/services/encode/Encoder.js +18 -2
  52. package/services/encode/EncoderSelection.js +38 -0
  53. package/services/encode/LocalObservations.js +359 -0
  54. package/services/encode/NvencEncoder.js +5 -0
  55. package/services/encode/OutputOpening.js +745 -0
  56. package/services/encode/OutputTimes.js +579 -0
  57. package/services/encode/Renditions.js +2162 -0
  58. package/services/encode/RunProgress.js +84 -0
  59. package/services/encode/SoftwareEncoder.js +12 -7
  60. package/services/encode/args.js +402 -186
  61. package/services/encode/calibration.js +361 -0
  62. package/services/encode/contention.js +128 -128
  63. package/services/encode/encode-run-state.js +16 -20
  64. package/services/encode/fingerprint.js +322 -0
  65. package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
  66. package/services/{learned-median.js → encode/learned-median.js} +11 -0
  67. package/services/encode/output/EncodedOutput.js +57 -0
  68. package/services/encode/output/Output.js +131 -0
  69. package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
  70. package/services/encode/output/OutputSpec.js +390 -0
  71. package/services/{output → encode/output}/Timeline.js +12 -37
  72. package/services/encode/output/index.js +20 -0
  73. package/services/encode/output/ladder.js +32 -0
  74. package/services/{output → encode/output}/playlists.js +3 -0
  75. package/services/encode/output-key-format.js +21 -0
  76. package/services/encode/quality/EncodeCost.js +1083 -0
  77. package/services/encode/quality/HostLoad.js +525 -0
  78. package/services/encode/quality/HostTimings.js +249 -0
  79. package/services/encode/quality/OutputQualityState.js +28 -0
  80. package/services/encode/quality/QualityController.js +931 -0
  81. package/services/encode/quality/QualityOffer.js +416 -0
  82. package/services/encode/quality/drain-threat.js +47 -0
  83. package/services/encode/quality/link-budget.js +254 -0
  84. package/services/encode/quality/output-format.js +266 -0
  85. package/services/encode/quality/serving-output.js +216 -0
  86. package/services/encode/quality/visible-rung.js +57 -0
  87. package/services/encode/run-command.js +23 -20
  88. package/services/encode/run-inputs.js +42 -0
  89. package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
  90. package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
  91. package/services/encode/segment-formats/init-compat.js +324 -0
  92. package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
  93. package/services/encode/throughput.js +97 -0
  94. package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
  95. package/services/media/KeyframeTables.js +316 -0
  96. package/services/{source → media}/SourceFile.js +7 -55
  97. package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
  98. package/services/media/SubtitleOrchestrator.js +167 -0
  99. package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
  100. package/services/{container → media/container}/AviContainer.js +266 -266
  101. package/services/{container → media/container}/Container.js +596 -601
  102. package/services/media/container/ContainerFactory.js +56 -0
  103. package/services/media/container/KeyframeTable.js +272 -0
  104. package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
  105. package/services/{container → media/container}/Mp4Container.js +1244 -1244
  106. package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
  107. package/services/media/keyframe-probe.js +157 -0
  108. package/services/media/media-info-probe.js +93 -0
  109. package/services/{playback-planner.js → media/playback-planner.js} +82 -39
  110. package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
  111. package/services/{tracks → media/tracks}/language-detect.js +174 -174
  112. package/services/server/OutputLifecycle.js +418 -0
  113. package/services/server/SegmentServing.js +1146 -0
  114. package/services/server/ViewerRequests.js +522 -0
  115. package/services/server/controllers/PlaybackController.js +36 -0
  116. package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
  117. package/services/server/wire-outputs.js +958 -0
  118. package/services/storage/Diagnostics.js +202 -0
  119. package/services/storage/MachineBudget.js +235 -0
  120. package/services/{piece-store → storage}/allowance.js +107 -107
  121. package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
  122. package/services/{files → storage/files}/CompletedFiles.js +116 -1
  123. package/services/{disk → storage}/free.js +33 -0
  124. package/services/storage/machine-memory.js +56 -0
  125. package/services/{memory-report.js → storage/memory-report.js} +575 -596
  126. package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
  127. package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
  128. package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
  129. package/services/storage/wire.js +175 -0
  130. package/services/{download → torrent/download}/SwarmSelection.js +2 -2
  131. package/services/{download → torrent/download}/registry.js +2 -2
  132. package/services/torrent/files.js +1 -1
  133. package/services/torrent/piece-store-of.js +29 -0
  134. package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
  135. package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
  136. package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
  137. package/services/{torrent-worker → torrent/worker}/client.js +77 -58
  138. package/services/torrent/worker/held-bytes.js +155 -0
  139. package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
  140. package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
  141. package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
  142. package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
  143. package/services/torrent/worker/resume-warm.js +107 -0
  144. package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
  145. package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
  146. package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
  147. package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
  148. package/services/{health-collector.js → transport/health-collector.js} +6 -35
  149. package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
  150. package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
  151. package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
  152. package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
  153. package/services/viewer/Assignments.js +340 -0
  154. package/services/{priority → viewer}/PriorityMap.js +390 -390
  155. package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
  156. package/services/viewer/Viewer.js +657 -483
  157. package/services/viewer/Viewers.js +368 -257
  158. package/services/viewer/active-output.js +37 -54
  159. package/services/viewer/audio-start.js +4 -3
  160. package/services/viewer/buffer-trend.js +81 -0
  161. package/services/viewer/choices.js +647 -0
  162. package/services/viewer/positions.js +126 -120
  163. package/services/viewer/report-intake.js +46 -45
  164. package/test/asset-paths.test.js +28 -0
  165. package/test/audio-inventory.test.js +3 -3
  166. package/test/audio-track-bitrate.test.js +40 -0
  167. package/test/auto-quality-step.test.js +707 -515
  168. package/test/available-share.test.js +1 -1
  169. package/test/background-fill.test.js +1 -1
  170. package/test/client-log-file.test.js +110 -110
  171. package/test/completed-files.test.js +1 -1
  172. package/test/concurrent-cost.test.js +13 -3
  173. package/test/container-media-info.test.js +3 -3
  174. package/test/core-dumps.test.js +1 -1
  175. package/test/coverage-follows-the-disk.test.js +40 -8
  176. package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
  177. package/test/cut-times-timeline.test.js +2 -2
  178. package/test/cuts-follow-published-grid.test.js +9 -8
  179. package/test/data-channel-frame.test.js +1 -1
  180. package/test/dead-torrent-handle.test.js +1 -1
  181. package/test/declared-rates.test.js +2 -2
  182. package/test/declared-tracks.test.js +2 -2
  183. package/test/decode-cost-fit.test.js +1 -1
  184. package/test/decode-cost.test.js +34 -25
  185. package/test/delivery-probe.test.js +1 -1
  186. package/test/demand-register.test.js +4 -4
  187. package/test/dht-bootstrap.test.js +1 -1
  188. package/test/drain-threat.test.js +65 -0
  189. package/test/duplicate-add.test.js +1 -1
  190. package/test/encode-admission.test.js +146 -0
  191. package/test/encode-cost.test.js +199 -10
  192. package/test/encode-orchestrator.test.js +39 -1
  193. package/test/encode-plan-viewers.test.js +2 -2
  194. package/test/encode-run.test.js +2 -2
  195. package/test/encode-slope.test.js +1 -1
  196. package/test/encoded-output.test.js +39 -0
  197. package/test/encoder-readings.test.js +1 -1
  198. package/test/every-claimant-is-told-its-share.test.js +152 -0
  199. package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
  200. package/test/failed-start-breaker.test.js +81 -30
  201. package/test/fastest-wires.test.js +1 -1
  202. package/test/ffmpeg-stream-counts.test.js +1 -1
  203. package/test/file-edges.test.js +3 -3
  204. package/test/fmp4-self-contained.test.js +1 -1
  205. package/test/fmp4-timestamps.test.js +1 -1
  206. package/test/fmp4-track-completeness.test.js +1 -1
  207. package/test/format-keyed-outputs.test.js +78 -0
  208. package/test/health-metrics.test.js +3 -2
  209. package/test/held-request-width.test.js +22 -23
  210. package/test/helpers/encode-run.js +136 -136
  211. package/test/helpers/held-file.js +29 -0
  212. package/test/helpers/manager.js +57 -0
  213. package/test/helpers/output-spec.js +63 -0
  214. package/test/host-load.test.js +1 -1
  215. package/test/index-accuracy-is-about-the-file.test.js +128 -0
  216. package/test/init-compat.test.js +219 -0
  217. package/test/input-lost-quiets-the-plan.test.js +1 -1
  218. package/test/input-unavailable.test.js +1 -1
  219. package/test/keeping-period.test.js +4 -4
  220. package/test/keyframe-index-accuracy.test.js +97 -45
  221. package/test/keyframe-table-budget.test.js +28 -33
  222. package/test/keyframes-belong-to-the-file.test.js +174 -57
  223. package/test/learned-median.test.js +1 -1
  224. package/test/letting-go-asks-who-is-watching.test.js +191 -0
  225. package/test/limit-row-by-area.test.js +60 -0
  226. package/test/link-budget.test.js +111 -0
  227. package/test/link-probe.test.js +56 -56
  228. package/test/local-encoding-data.test.js +273 -0
  229. package/test/look-ahead-coverage.test.js +1 -1
  230. package/test/manager-surface.test.js +294 -0
  231. package/test/matroska-blocks.test.js +0 -0
  232. package/test/matroska-cues-track.test.js +1 -1
  233. package/test/memory-budget.test.js +2 -2
  234. package/test/mp4-composition-times.test.js +0 -0
  235. package/test/mp4-subtitles.test.js +1 -1
  236. package/test/one-authority.test.js +45 -41
  237. package/test/one-budget.test.js +149 -0
  238. package/test/one-output-one-session.test.js +204 -0
  239. package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
  240. package/test/one-serving-output-per-height.test.js +458 -0
  241. package/test/orchestrator-wired.test.js +46 -45
  242. package/test/output-catalog.test.js +39 -0
  243. package/test/output-shape.test.js +4 -51
  244. package/test/output-spec.test.js +251 -157
  245. package/test/output-speed.test.js +17 -15
  246. package/test/outputs-being-prepared.test.js +40 -0
  247. package/test/packet-witness-ring.test.js +2 -2
  248. package/test/packet-witness.test.js +1 -1
  249. package/test/piece-disk-store.test.js +2 -2
  250. package/test/piece-from-whole-file.test.js +1 -1
  251. package/test/piece-lru.test.js +1 -1
  252. package/test/piece-reader.test.js +2 -2
  253. package/test/piece-store-eviction.test.js +1 -1
  254. package/test/piece-store-never-refuses.test.js +1 -1
  255. package/test/piece-store-reservations.test.js +1 -1
  256. package/test/piece-store-slow-disk.test.js +1 -1
  257. package/test/piece-tail.test.js +1 -1
  258. package/test/plan-host-timings.test.js +1 -1
  259. package/test/plan-is-steady.test.js +225 -225
  260. package/test/priority-map-download.test.js +4 -4
  261. package/test/priority-map-emptied.test.js +204 -205
  262. package/test/priority-map-per-output.test.js +215 -209
  263. package/test/priority-map.test.js +1 -1
  264. package/test/probe-wedge-certainty.test.js +1 -1
  265. package/test/produced-copy-choice.test.js +15 -60
  266. package/test/quality-offer.test.js +162 -0
  267. package/test/quality-variants.test.js +1229 -1167
  268. package/test/rate-control-in-the-output.test.js +180 -0
  269. package/test/read-survives-withdrawal.test.js +2 -2
  270. package/test/read-window.test.js +10 -3
  271. package/test/resume-warm.test.js +1 -1
  272. package/test/run-inputs.test.js +80 -0
  273. package/test/run-position-follows-published-grid.test.js +13 -11
  274. package/test/run-progress.test.js +65 -0
  275. package/test/same-height-switch.test.js +664 -0
  276. package/test/seek-frees-the-viewers-requests.test.js +217 -0
  277. package/test/seek-landing.test.js +13 -13
  278. package/test/seek-target-not-superseded.test.js +28 -29
  279. package/test/segment-closed-on-both-branches.test.js +96 -0
  280. package/test/segment-cut-times.test.js +1 -1
  281. package/test/segment-hold-supersede.test.js +28 -35
  282. package/test/segment-judged-by-its-own-header.test.js +221 -0
  283. package/test/segment-serve-wiring.test.js +130 -39
  284. package/test/segment-store-eviction.test.js +22 -7
  285. package/test/segment-store.test.js +98 -14
  286. package/test/segment-true-start.test.js +1 -1
  287. package/test/segments-are-shared.test.js +17 -16
  288. package/test/send-chunk-bytes.test.js +1 -1
  289. package/test/serving-existing-output.test.js +102 -0
  290. package/test/serving-output.test.js +102 -0
  291. package/test/shared-piece-store.test.js +1 -1
  292. package/test/source-file.test.js +1 -1
  293. package/test/stale-request-after-seek.test.js +17 -21
  294. package/test/startup-readings.test.js +10 -5
  295. package/test/subtitle-cue-framing.test.js +5 -5
  296. package/test/subtitle-cue-source.test.js +36 -38
  297. package/test/subtitle-cue-walk.test.js +11 -10
  298. package/test/subtitle-defaults.test.js +1 -1
  299. package/test/subtitle-language.test.js +4 -4
  300. package/test/subtitle-track-numbering.test.js +5 -4
  301. package/test/supply-margin.test.js +1 -1
  302. package/test/swarm-follows-readers.test.js +3 -3
  303. package/test/swarm-reach.test.js +1 -1
  304. package/test/swarm-selection.test.js +3 -3
  305. package/test/tail-duplication.test.js +1 -1
  306. package/test/the-channel-has-both-ends.test.js +91 -0
  307. package/test/the-torrent-thread-serves-bytes.test.js +107 -0
  308. package/test/thread-pool.test.js +3 -3
  309. package/test/timeline.test.js +1 -12
  310. package/test/torrent-cost.test.js +1 -1
  311. package/test/tracks-begin-together.test.js +38 -24
  312. package/test/tunnel-renewal.test.js +1 -1
  313. package/test/two-viewers-one-picture.test.js +120 -87
  314. package/test/upload-hurry.test.js +3 -3
  315. package/test/usrsctp-state.test.js +1 -1
  316. package/test/video-facts.test.js +1 -1
  317. package/test/viewer-assignments.test.js +143 -0
  318. package/test/viewer-outputs.test.js +60 -52
  319. package/test/viewer-presence.test.js +22 -25
  320. package/test/viewer-requests-admission.test.js +89 -0
  321. package/test/viewer-states.test.js +189 -202
  322. package/test/viewer-subtitle-subscription.test.js +1 -1
  323. package/test/viewer.test.js +139 -136
  324. package/test/viewing-generations.test.js +390 -0
  325. package/test/visible-rung.test.js +37 -0
  326. package/test/wait-ledger.test.js +1 -1
  327. package/test/webrtc-shim.test.js +3 -3
  328. package/test/wedge-certainty.test.js +2 -2
  329. package/test/withdraw-piece-claim.test.js +2 -2
  330. package/test/worker-channel.test.js +1 -31
  331. package/utils/client-log-file.js +151 -151
  332. package/scripts/read-container-index.mjs +0 -108
  333. package/services/container/ContainerFactory.js +0 -122
  334. package/services/controllers/PlaybackController.js +0 -33
  335. package/services/disk/DiskSpace.js +0 -150
  336. package/services/disk/wire.js +0 -61
  337. package/services/hls-session-manager.js +0 -9608
  338. package/services/orchestrators/SubtitleOrchestrator.js +0 -160
  339. package/services/output/Output.js +0 -94
  340. package/services/output/OutputSpec.js +0 -202
  341. package/services/output/index.js +0 -1
  342. package/services/output/ladder.js +0 -26
  343. package/services/produced-index.js +0 -222
  344. package/services/quality/EncodeCost.js +0 -555
  345. package/services/torrent-worker/container-tracks.js +0 -414
  346. package/services/viewer/link-readings.js +0 -56
  347. package/test/produced-index.test.js +0 -142
  348. /package/{services → bin}/thread-pool.js +0 -0
  349. /package/services/{available-share.js → encode/available-share.js} +0 -0
  350. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  351. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  352. /package/services/{host-load.js → encode/host-load.js} +0 -0
  353. /package/services/{output → encode/output}/cut-grid.js +0 -0
  354. /package/services/{output → encode/output}/rates.js +0 -0
  355. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  356. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  357. /package/services/{container → media/container}/ebml-reader.js +0 -0
  358. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  359. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  360. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  361. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  362. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  364. /package/services/{tracks → media/tracks}/index.js +0 -0
  365. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  366. /package/services/{disk → storage}/keep.js +0 -0
  367. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  368. /package/services/{disk → storage}/returns.js +0 -0
  369. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  370. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  371. /package/services/{demand → torrent/demand}/Window.js +0 -0
  372. /package/services/{demand → torrent/demand}/index.js +0 -0
  373. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  374. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  375. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  376. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  377. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  378. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  379. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  380. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  381. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  382. /package/services/{priority → viewer}/WaitLedger.js +0 -0
@@ -79,21 +79,26 @@ test("nothing about the machine is asked only of a software encoder", () => {
79
79
  }
80
80
  });
81
81
 
82
- test("the throughput benchmark is given the encoder that will actually run", () => {
82
+ test("the calibration is given the encoder that will actually run, and software beside it", () => {
83
83
  const server = readFileSync(path.join(HERE, "..", "server.js"), "utf8");
84
84
  assert.match(
85
85
  server,
86
- /benchmarkSoftwarePresets\(\{[^}]*encoder: videoEncoder/,
87
- "the benchmark is not told which encoder to measure"
86
+ /calibrateEncoder\(\{[^}]*encoder: videoEncoder/,
87
+ "the calibration is not told which encoder to measure"
88
+ );
89
+ assert.match(
90
+ server,
91
+ /calibrateEncoder\(\{[^}]*encoder: softwareDescriptor\(\)/,
92
+ "software, which a failing device falls back to, is not calibrated"
88
93
  );
89
94
  });
90
95
 
91
96
  test("a start and a stop are measured before any viewer, and reach the plan", () => {
92
97
  const server = readFileSync(path.join(HERE, "..", "server.js"), "utf8");
93
98
  assert.match(server, /await measureStartAndStop\(/, "nothing measures a start at startup");
94
- assert.match(server, /\n startStopCost,/, "the reading never reaches the session manager");
99
+ assert.match(server, /\n startStopCost,/, "the reading never reaches the wiring of the encoding");
95
100
 
96
- const manager = readFileSync(path.join(HERE, "..", "services", "hls-session-manager.js"), "utf8");
101
+ const manager = readFileSync(path.join(HERE, "..", "services", "server", "wire-outputs.js"), "utf8");
97
102
  assert.match(
98
103
  manager,
99
104
  /noteStartupCosts\(startStopCost\)/,
@@ -18,11 +18,11 @@
18
18
  import test from "node:test";
19
19
  import assert from "node:assert/strict";
20
20
 
21
- import { AviContainer } from "../services/container/AviContainer.js";
22
- import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
23
- import { Mp4Container } from "../services/container/Mp4Container.js";
24
- import { SubtitleFileContainer } from "../services/container/SubtitleFileContainer.js";
25
- import { MarkupKind, TextSubtitleTrack } from "../services/tracks/TextSubtitleTrack.js";
21
+ import { AviContainer } from "../services/media/container/AviContainer.js";
22
+ import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
23
+ import { Mp4Container } from "../services/media/container/Mp4Container.js";
24
+ import { SubtitleFileContainer } from "../services/media/container/SubtitleFileContainer.js";
25
+ import { MarkupKind, TextSubtitleTrack } from "../services/media/tracks/TextSubtitleTrack.js";
26
26
 
27
27
  /** The line from the field report, as Matroska stores it. */
28
28
  const FIELD_BLOCK =
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file WHICH THREAD answers a pull for subtitle cues.
2
+ * @file WHAT answers a pull for subtitle cues — the walk, and nothing else.
3
3
  *
4
4
  * Field 2026-09-03, on `[HorribleSubs] Drifters - 04 [1080p].mkv`. The file had
5
5
  * been downloaded in an earlier sitting, so the torrent worker's cluster walk
@@ -10,25 +10,27 @@
10
10
  * indexed. So the viewer watched the first 82 s with no subtitles and the rest
11
11
  * of the episode with them.
12
12
  *
13
- * The pull ran on the MAIN thread, where the torrent is a stand-in carrying
14
- * `infoHash`, `name` and a `files` list — no `bitfield`, no `pieceLength`. The
15
- * walk decides what it may read from those two, so every range read as "not
16
- * downloaded", nothing was walked, and an empty document came back. An empty
17
- * document is also the right answer for a file that holds no cues yet, which is
18
- * why nothing reported a failure.
13
+ * The pull was answered by something that could not read pieces. Whatever a
14
+ * pull is handed, the walk decides what it may read from it, so every range
15
+ * read as "not downloaded", nothing was walked, and an empty document came
16
+ * back. An empty document is also the right answer for a file that holds no
17
+ * cues yet, which is why nothing reported a failure.
19
18
  *
20
- * These checks pin the two halves of the repair: the pull is addressed to the
21
- * thread that owns the torrent, and a walk asked of a torrent that cannot say
22
- * what it holds says so rather than answering emptily.
19
+ * The thread stopped being the distinction on 2026-09-15 — the walk runs beside
20
+ * the sessions now, handed which byte ranges of a file are downloaded whole —
21
+ * but the property it was standing for did not: a pull is answered by the SAME
22
+ * walk that pushes, its own figures travel back, and anything that cannot read
23
+ * says so rather than answering emptily.
23
24
  */
24
25
 
25
26
  import test from "node:test";
26
27
  import assert from "node:assert/strict";
27
28
 
28
- import { SubtitleOrchestrator } from "../services/orchestrators/SubtitleOrchestrator.js";
29
- import { cuesHeldFor } from "../services/torrent-worker/subtitle-cues.js";
29
+ import { SubtitleOrchestrator } from "../services/media/SubtitleOrchestrator.js";
30
+ import { cuesHeldFor } from "../services/media/SubtitleCues.js";
31
+ import { heldFileOver } from "./helpers/held-file.js";
30
32
 
31
- /** The stand-in the main thread holds: a name, a file list, and no pieces. */
33
+ /** A torrent that cannot say which of itself is downloaded. */
32
34
  function mainThreadTorrent() {
33
35
  return {
34
36
  infoHash: "0".repeat(40),
@@ -38,24 +40,22 @@ function mainThreadTorrent() {
38
40
  };
39
41
  }
40
42
 
41
- test("a pull is answered by the thread that owns the torrent, not walked here", async () => {
43
+ test("a pull is answered by the walk, and the walk's own figures travel back", async () => {
42
44
  const asked = [];
43
- const pool = {
44
- async getSubtitleCues(torrent, fileIndex, trackNumber) {
45
- asked.push({ sourceKey: torrent.sourceKey, fileIndex, trackNumber });
45
+ const cues = {
46
+ async held(torrent, fileIndex, sourceKey, trackNumber) {
47
+ asked.push({ sourceKey, fileIndex, trackNumber });
46
48
  return {
47
49
  cues: [{ startSeconds: 5.4, endSeconds: 9.1, text: "So what if you brought them over?", seq: 1 }],
48
50
  coveredClusters: 283,
49
51
  indexedClusters: 283,
50
- codecId: "S_TEXT/ASS",
51
- codecPrivate: "",
52
- language: ""
52
+ track: { codecId: "S_TEXT/ASS", codecPrivate: "", language: "" }
53
53
  };
54
54
  }
55
55
  };
56
56
 
57
- const orchestrator = new SubtitleOrchestrator({ forget() {} });
58
- const held = await orchestrator.getCues(pool, mainThreadTorrent(), 33, "a".repeat(40), 3);
57
+ const orchestrator = new SubtitleOrchestrator({ forget() {} }, cues);
58
+ const held = await orchestrator.getCues(mainThreadTorrent(), 33, "a".repeat(40), 3);
59
59
 
60
60
  assert.deepEqual(asked, [{ sourceKey: "a".repeat(40), fileIndex: 33, trackNumber: 3 }]);
61
61
  assert.equal(held.cues.length, 1);
@@ -66,11 +66,11 @@ test("a pull is answered by the thread that owns the torrent, not walked here",
66
66
  });
67
67
 
68
68
  test("the cursor of a pulled cue is the found-order the worker assigned", async () => {
69
- // The browser mixes cursors from pulls and pushes. Walking a second time on
70
- // another thread would start a second `seq` counter and the two would not be
71
- // comparable, which is the deeper reason the pull is not served locally.
72
- const pool = {
73
- async getSubtitleCues() {
69
+ // The browser mixes cursors from pulls and pushes. A second walk would start
70
+ // a second `seq` counter and the two would not be comparable — which is why
71
+ // both ends go through the one walk, with one per-file state.
72
+ const cues = {
73
+ async held() {
74
74
  return {
75
75
  cues: [
76
76
  { startSeconds: 5.4, endSeconds: 9.1, text: "one", seq: 1 },
@@ -78,27 +78,25 @@ test("the cursor of a pulled cue is the found-order the worker assigned", async
78
78
  ],
79
79
  coveredClusters: 12,
80
80
  indexedClusters: 283,
81
- codecId: "S_TEXT/ASS",
82
- codecPrivate: "",
83
- language: ""
81
+ track: { codecId: "S_TEXT/ASS", codecPrivate: "", language: "" }
84
82
  };
85
83
  }
86
84
  };
87
- const orchestrator = new SubtitleOrchestrator({ forget() {} });
88
- const held = await orchestrator.getCues(pool, mainThreadTorrent(), 33, "a".repeat(40), 3);
85
+ const orchestrator = new SubtitleOrchestrator({ forget() {} }, cues);
86
+ const held = await orchestrator.getCues(mainThreadTorrent(), 33, "a".repeat(40), 3);
89
87
  assert.deepEqual(held.cues.map((cue) => cue.seq), [1, 31]);
90
88
  });
91
89
 
92
- test("a pool with no channel to the worker is refused, not answered emptily", async () => {
90
+ test("an orchestrator with no walk is refused, not answered emptily", async () => {
93
91
  const orchestrator = new SubtitleOrchestrator({ forget() {} });
94
- const held = await orchestrator.getCues({}, mainThreadTorrent(), 33, "a".repeat(40), 3);
92
+ const held = await orchestrator.getCues(mainThreadTorrent(), 33, "a".repeat(40), 3);
95
93
  assert.deepEqual(held, { cues: [], coveredClusters: 0, indexedClusters: 0, track: null });
96
94
  });
97
95
 
98
96
  test("the walk refuses a torrent that cannot say which pieces it holds", async () => {
99
- // Called directly, as the main thread used to call it. Without this guard the
100
- // answer is an empty list indistinguishable from a file with no cues, which
101
- // is what hid the defect for a whole session.
102
- const held = await cuesHeldFor(mainThreadTorrent(), 33, "b".repeat(40), 3);
97
+ // Called directly. Without this guard the answer is an empty list
98
+ // indistinguishable from a file with no cues, which is what hid the defect
99
+ // for a whole session.
100
+ const held = await cuesHeldFor(heldFileOver(mainThreadTorrent(), 33, "b".repeat(40)), 3);
103
101
  assert.deepEqual(held, { cues: [], coveredClusters: 0, indexedClusters: 0, track: null });
104
102
  });
@@ -19,7 +19,8 @@ import test from "node:test";
19
19
  import assert from "node:assert/strict";
20
20
  import { Readable } from "node:stream";
21
21
 
22
- import { cuesHeldFor, warmSubtitleCues, forgetSubtitles } from "../services/torrent-worker/subtitle-cues.js";
22
+ import { cuesHeldFor, warmSubtitleCues, forgetSubtitles } from "../services/media/SubtitleCues.js";
23
+ import { heldFileOver } from "./helpers/held-file.js";
23
24
 
24
25
  const ID_EBML = 0x1a45dfa3;
25
26
  const ID_SEGMENT = 0x18538067;
@@ -255,7 +256,7 @@ test("a walk of a downloaded file gives up every cue, unframed, with its times",
255
256
  const sourceKey = "a".repeat(40);
256
257
  forgetSubtitles(sourceKey);
257
258
  try {
258
- const plain = await cuesHeldFor(torrent, 0, sourceKey, 2);
259
+ const plain = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
259
260
  assert.deepEqual(
260
261
  plain.cues.map((cue) => [cue.startSeconds, cue.endSeconds, cue.text]),
261
262
  [
@@ -264,7 +265,7 @@ test("a walk of a downloaded file gives up every cue, unframed, with its times",
264
265
  ]
265
266
  );
266
267
 
267
- const ass = await cuesHeldFor(torrent, 0, sourceKey, 3);
268
+ const ass = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3);
268
269
  assert.deepEqual(
269
270
  ass.cues.map((cue) => [cue.startSeconds, cue.endSeconds, cue.text]),
270
271
  [
@@ -284,7 +285,7 @@ test("the cursor is the order cues were FOUND, and it never repeats", async () =
284
285
  const sourceKey = "b".repeat(40);
285
286
  forgetSubtitles(sourceKey);
286
287
  try {
287
- const held = await cuesHeldFor(torrent, 0, sourceKey, 2);
288
+ const held = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
288
289
  const cursors = held.cues.map((cue) => cue.seq);
289
290
  assert.deepEqual(cursors, [1, 2], "two cues found, in the order they were read");
290
291
  assert.equal(new Set(cursors).size, cursors.length, "no cue shares a cursor with another");
@@ -300,7 +301,7 @@ test("a cluster whose bytes are not downloaded is left for next time", async ()
300
301
  const sourceKey = "c".repeat(40);
301
302
  forgetSubtitles(sourceKey);
302
303
  try {
303
- const held = await cuesHeldFor(torrent, 0, sourceKey, 2);
304
+ const held = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
304
305
  assert.deepEqual(
305
306
  held.cues.map((cue) => cue.text),
306
307
  ["First English line"],
@@ -319,10 +320,10 @@ test("one walk fills every track, and a second call reads no cluster again", asy
319
320
  const sourceKey = "d".repeat(40);
320
321
  forgetSubtitles(sourceKey);
321
322
  try {
322
- await cuesHeldFor(torrent, 0, sourceKey, 2);
323
+ await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
323
324
  const clusterReads = reads.filter((range) => clusterAt.includes(range.start)).length;
324
325
 
325
- const other = await cuesHeldFor(torrent, 0, sourceKey, 3);
326
+ const other = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3);
326
327
  assert.equal(other.cues.length, 2, "the other track was filled by the same walk");
327
328
  assert.equal(
328
329
  reads.filter((range) => clusterAt.includes(range.start)).length,
@@ -340,14 +341,14 @@ test("the warm pass reports what is new, by the number the browser knows", async
340
341
  const sourceKey = "e".repeat(40);
341
342
  forgetSubtitles(sourceKey);
342
343
  try {
343
- const first = await warmSubtitleCues(torrent, 0, sourceKey);
344
+ const first = await warmSubtitleCues(heldFileOver(torrent, 0, sourceKey));
344
345
  assert.deepEqual(
345
346
  first.map((entry) => [entry.trackIndex, entry.cues.length, entry.language]).sort(),
346
347
  [[0, 2, "eng"], [1, 2, "rus"]].sort(),
347
348
  "both text tracks gained two cues, numbered as ffmpeg numbers them"
348
349
  );
349
350
 
350
- const again = await warmSubtitleCues(torrent, 0, sourceKey);
351
+ const again = await warmSubtitleCues(heldFileOver(torrent, 0, sourceKey));
351
352
  assert.deepEqual(again, [], "nothing is new the second time round");
352
353
  } finally {
353
354
  forgetSubtitles(sourceKey);
@@ -360,7 +361,7 @@ test("a torrent that cannot say which pieces it holds is refused, not answered e
360
361
  const sourceKey = "f".repeat(40);
361
362
  forgetSubtitles(sourceKey);
362
363
  try {
363
- const held = await cuesHeldFor({ ...torrent, bitfield: null }, 0, sourceKey, 2);
364
+ const held = await cuesHeldFor({ ...heldFileOver(torrent, 0, sourceKey), heldRanges: null }, 2);
364
365
  assert.deepEqual(held.cues, []);
365
366
  assert.equal(held.track, null, "the answer says nothing was read, not that there is nothing");
366
367
  } finally {
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { Container } from "../services/container/Container.js";
3
+ import { Container } from "../services/media/container/Container.js";
4
4
 
5
5
  test("a flag the container wrote is carried through, and one it did not is not", () => {
6
6
  const banner = [
@@ -24,10 +24,10 @@
24
24
  import test from "node:test";
25
25
  import assert from "node:assert/strict";
26
26
 
27
- import { detectLanguage } from "../services/tracks/language-detect.js";
28
- import { SubtitleFileContainer } from "../services/container/SubtitleFileContainer.js";
29
- import { TextSubtitleTrack } from "../services/tracks/TextSubtitleTrack.js";
30
- import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
27
+ import { detectLanguage } from "../services/media/tracks/language-detect.js";
28
+ import { SubtitleFileContainer } from "../services/media/container/SubtitleFileContainer.js";
29
+ import { TextSubtitleTrack } from "../services/media/tracks/TextSubtitleTrack.js";
30
+ import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
31
31
 
32
32
  /** Varied Russian dialogue, the length a few minutes of an episode carries. */
33
33
  const RUSSIAN_DIALOGUE = [
@@ -20,8 +20,9 @@
20
20
  import test from "node:test";
21
21
  import assert from "node:assert/strict";
22
22
  import { Readable } from "node:stream";
23
- import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
24
- import { cuesHeldFor, forgetSubtitles } from "../services/torrent-worker/subtitle-cues.js";
23
+ import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
24
+ import { cuesHeldFor, forgetSubtitles } from "../services/media/SubtitleCues.js";
25
+ import { heldFileOver } from "./helpers/held-file.js";
25
26
 
26
27
  const ID_EBML = 0x1a45dfa3;
27
28
  const ID_SEGMENT = 0x18538067;
@@ -263,8 +264,8 @@ test("two walks of one file at the same time read each cluster once", async () =
263
264
  // Both text tracks at once, which is what the warmup does on every verified
264
265
  // piece and every three seconds.
265
266
  const [first, second] = await Promise.all([
266
- cuesHeldFor(torrent, 0, sourceKey, 3),
267
- cuesHeldFor(torrent, 0, sourceKey, 4)
267
+ cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3),
268
+ cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 4)
268
269
  ]);
269
270
 
270
271
  assert.equal(first.coveredClusters, 1, "the cluster the table names was walked");
@@ -10,7 +10,7 @@
10
10
  import assert from "node:assert/strict";
11
11
  import test from "node:test";
12
12
 
13
- import { minimumBufferFrom, requiredSpeedFrom } from "../services/supply-margin.js";
13
+ import { minimumBufferFrom, requiredSpeedFrom } from "../services/torrent/supply-margin.js";
14
14
 
15
15
  /**
16
16
  * Waits spaced by `intervalSec`, each lasting `waitSec`.
@@ -32,9 +32,9 @@ import {
32
32
  rejoinSwarm,
33
33
  stateFileEdges,
34
34
  swarmDecisionFor
35
- } from "../services/torrent-pool.js";
36
- import { demandFor, forgetTorrent } from "../services/download/registry.js";
37
- import { Urgency } from "../services/demand/index.js";
35
+ } from "../services/torrent/torrent-pool.js";
36
+ import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
37
+ import { Urgency } from "../services/torrent/demand/index.js";
38
38
 
39
39
  /**
40
40
  * A torrent that records what was done to it. Only the surface the rule
@@ -19,7 +19,7 @@
19
19
  import assert from "node:assert/strict";
20
20
  import test from "node:test";
21
21
 
22
- import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent-pool.js";
22
+ import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
23
23
 
24
24
  test("connected and known are separate numbers", () => {
25
25
  const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
@@ -9,9 +9,9 @@
9
9
  import test from "node:test";
10
10
  import assert from "node:assert/strict";
11
11
 
12
- import { DemandRegister } from "../services/demand/DemandRegister.js";
13
- import { Urgency } from "../services/demand/Urgency.js";
14
- import { SwarmSelection } from "../services/download/SwarmSelection.js";
12
+ import { DemandRegister } from "../services/torrent/demand/DemandRegister.js";
13
+ import { Urgency } from "../services/torrent/demand/Urgency.js";
14
+ import { SwarmSelection } from "../services/torrent/download/SwarmSelection.js";
15
15
 
16
16
  const PIECE = 1024;
17
17
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  import test from "node:test";
13
13
  import assert from "node:assert/strict";
14
- import { duplicateTailFor } from "../services/torrent-worker/fastest-wires.js";
14
+ import { duplicateTailFor } from "../services/torrent/worker/fastest-wires.js";
15
15
 
16
16
  /** A piece with `chunks` blocks, of which `missing` at the end are absent. */
17
17
  function piece(chunks, missing) {
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file Every call across the thread boundary has something to land on.
3
+ *
4
+ * The boundary is three files that have to agree, and nothing made them:
5
+ * `protocol.js` names the commands, `worker.js` answers them, `client.js` asks,
6
+ * and `pool-adapter.js` is what the main thread calls. A mismatch anywhere in
7
+ * that chain is invisible to every other check in this repository, because a
8
+ * unit test of either end alone passes and the failure needs a real worker.
9
+ *
10
+ * It was not hypothetical. Removing three commands on 2026-09-15 took an
11
+ * unrelated neighbour with them — `client.fillFile`, which the adapter went on
12
+ * calling — and left two adapter methods calling commands that no longer
13
+ * existed. One of those two is the browser's own subtitle request, and it did
14
+ * not even throw where anybody would see it: the orchestrator caught the error
15
+ * and answered an EMPTY document, which is indistinguishable from a file that
16
+ * genuinely holds no cues. That is the exact failure the whole subtitle path is
17
+ * written to avoid, and it would have reached the field.
18
+ *
19
+ * These are structural because the property is: there is no input that makes a
20
+ * call land on a method that is not there.
21
+ */
22
+
23
+ import test from "node:test";
24
+ import assert from "node:assert/strict";
25
+ import { readFileSync } from "node:fs";
26
+ import path from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+
29
+ const WORKER = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../services/torrent/worker");
30
+
31
+ /**
32
+ * @param {string} name
33
+ * @returns {string}
34
+ */
35
+ function source(name) {
36
+ return readFileSync(path.join(WORKER, name), "utf8");
37
+ }
38
+
39
+ /**
40
+ * Names a file defines at the top level of a class: methods, and fields it
41
+ * assigns to itself. Both are callable or readable from outside.
42
+ *
43
+ * @param {string} text
44
+ * @returns {Set<string>}
45
+ */
46
+ function membersOf(text) {
47
+ const names = new Set();
48
+ for (const match of text.matchAll(/^\s{2}(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?([a-zA-Z#][a-zA-Z0-9]*)\s*[(=]/gm)) {
49
+ names.add(match[1]);
50
+ }
51
+ // Fields a constructor or a method assigns rather than declares.
52
+ for (const match of text.matchAll(/this\.([a-zA-Z][a-zA-Z0-9]*)\s*=/g)) {
53
+ names.add(match[1]);
54
+ }
55
+ return names;
56
+ }
57
+
58
+ test("every client method the adapter calls exists", () => {
59
+ const have = membersOf(source("client.js"));
60
+ const used = [...source("pool-adapter.js").matchAll(/#client\.([a-zA-Z][a-zA-Z0-9]*)/g)].map((m) => m[1]);
61
+
62
+ assert.deepEqual([...new Set(used)].filter((name) => !have.has(name)), []);
63
+ });
64
+
65
+ test("every command the client sends is answered by the worker", () => {
66
+ const protocol = source("protocol.js");
67
+ const answered = new Set([...source("worker.js").matchAll(/case Command\.([A-Z_]+):/g)].map((m) => m[1]));
68
+ const sent = [...source("client.js").matchAll(/Command\.([A-Z_]+)/g)].map((m) => m[1]);
69
+
70
+ const unanswered = [...new Set(sent)].filter((name) => !answered.has(name));
71
+ assert.deepEqual(unanswered, [], "a command nobody answers waits for a reply that never comes");
72
+
73
+ const undeclared = [...new Set(sent)].filter((name) => !protocol.includes(` ${name}:`));
74
+ assert.deepEqual(undeclared, [], "and one the protocol does not name is `undefined` on the wire");
75
+ });
76
+
77
+ test("every event the worker posts is received somewhere on the main thread", () => {
78
+ // TWO receivers, and they answer different shapes. `channel.js` matches the
79
+ // replies to a request by its id — a result, an error, a chunk of a body —
80
+ // while `client.js` has a `case` for each event that belongs to nobody's
81
+ // request. An event handled by neither is a fact announced into silence.
82
+ const receiving = `${source("client.js")}
83
+ ${source("channel.js")}`;
84
+ const handled = new Set([
85
+ ...[...receiving.matchAll(/case Event\.([A-Z_]+):/g)].map((m) => m[1]),
86
+ ...[...receiving.matchAll(/message\.type === Event\.([A-Z_]+)/g)].map((m) => m[1])
87
+ ]);
88
+ const posted = [...source("worker.js").matchAll(/type:\s*Event\.([A-Z_]+)/g)].map((m) => m[1]);
89
+
90
+ assert.deepEqual([...new Set(posted)].filter((name) => !handled.has(name)), []);
91
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file The torrent thread serves bytes. It does not read what a file says.
3
+ *
4
+ * Both halves of that were true of the design and only one was true of the
5
+ * code: pieces have lived in shared memory since the reader was rewritten, and
6
+ * the torrent thread ALSO parsed containers — the track table, the media info
7
+ * and the keyframe index — and carried each answer back over the channel.
8
+ *
9
+ * The stated reason was "the main thread cannot open a read stream on one of
10
+ * its files". That is true of WebTorrent's own API and not of the bytes: a
11
+ * container is built from one function, `readRange(start, end)`, and the read
12
+ * that serves every segment already waits for what has not arrived and steers
13
+ * the swarm toward it. What the split cost was three commands, a second
14
+ * `ContainerOrchestrator` in that thread, and — because an answer then had to
15
+ * be carried across — the question of where each such answer is KEPT, which is
16
+ * how one fact came to be stored twice.
17
+ *
18
+ * These checks are structural because the property is structural: there is no
19
+ * input that makes a parse happen in the wrong thread, only an import.
20
+ */
21
+
22
+ import test from "node:test";
23
+ import assert from "node:assert/strict";
24
+ import { readFileSync, readdirSync } from "node:fs";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const WORKER = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../services/torrent/worker");
29
+
30
+ /**
31
+ * @param {string} name
32
+ * @returns {string}
33
+ */
34
+ function source(name) {
35
+ return readFileSync(path.join(WORKER, name), "utf8");
36
+ }
37
+
38
+ test("no command asks the torrent thread what a file declares", () => {
39
+ const protocol = source("protocol.js");
40
+
41
+ for (const gone of [
42
+ "CONTAINER_TRACKS",
43
+ "CONTAINER_MEDIA_INFO",
44
+ "CONTAINER_KEYFRAMES",
45
+ "SUBTITLE_TRACKS",
46
+ "SUBTITLE_CUES"
47
+ ]) {
48
+ assert.equal(
49
+ protocol.includes(gone),
50
+ false,
51
+ `${gone} is a question about the FILE, and it is answered where the sessions are`
52
+ );
53
+ }
54
+ });
55
+
56
+ test("nothing in that thread reaches into the media layer at all", () => {
57
+ // The last one to was the subtitle cue walk, and it moved with the rest: it
58
+ // is handed which ranges of a file are downloaded whole and how to read one
59
+ // of those, and it has no use for a torrent beyond that. What stayed behind
60
+ // is the two things only this thread can answer — the bitfield as a list of
61
+ // ranges, and a store read that never fetches (`held-bytes.js`).
62
+ const reaching = readdirSync(WORKER)
63
+ .filter((name) => name.endsWith(".js"))
64
+ .filter((name) => /from "(?:\.\.\/)+media\//.test(source(name)));
65
+
66
+ assert.deepEqual(reaching, []);
67
+ });
68
+
69
+ test("what the walk may read is answered as a list, not asked per cluster", () => {
70
+ // A pass asks about every cluster of the file — hundreds on the field files,
71
+ // every three seconds. One list per pass costs one message; the same answers
72
+ // one at a time would cost hundreds of round trips for a walk that is meant
73
+ // to be free when there is nothing new to find.
74
+ const held = source("held-bytes.js");
75
+
76
+ assert.match(held, /export function heldRangesOf\(torrent, fileIndex\)/);
77
+ assert.match(held, /export function readHeldBytes\(/);
78
+ });
79
+
80
+ test("the walk's read never fetches", () => {
81
+ // The ordinary range read declares demand and steers the swarm, which is
82
+ // right for a viewer waiting on a segment and wrong here: switching subtitles
83
+ // on must not pull bytes the viewer is not waiting for.
84
+ const held = source("held-bytes.js");
85
+
86
+ assert.equal(held.includes("demandFor"), false);
87
+ assert.equal(held.includes("readFragments"), false);
88
+ });
89
+
90
+ test("the resume warm is TOLD how long the file runs", () => {
91
+ // It used to read the container itself to find out, which is what a file
92
+ // states about itself. What is left here is the part that really is the
93
+ // torrent's: a position in seconds becomes a byte offset, and that region is
94
+ // pulled off the swarm.
95
+ const warm = source("resume-warm.js");
96
+
97
+ assert.match(warm, /options\.durationSeconds/);
98
+ assert.equal(warm.includes("containerOrchestrator"), false);
99
+ assert.equal(warm.includes("ContainerFactory"), false);
100
+ });
101
+
102
+ test("the main thread can read a byte range of a file", () => {
103
+ // The one thing the move needed, and the whole of what a container is built
104
+ // from. Without it the parse has nowhere to get its bytes and the commands
105
+ // come back.
106
+ assert.match(source("pool-adapter.js"), /async readRangeOf\(torrent, fileIndex, start, end\)/);
107
+ });
@@ -21,7 +21,7 @@ test("the pool is set, and left alone when the deployment states its own", async
21
21
  const previous = process.env.UV_THREADPOOL_SIZE;
22
22
  try {
23
23
  process.env.UV_THREADPOOL_SIZE = "";
24
- await import(`../services/thread-pool.js?first=${Date.now()}`);
24
+ await import(`../bin/thread-pool.js?first=${Date.now()}`);
25
25
  assert.equal(
26
26
  Number(process.env.UV_THREADPOOL_SIZE) >= 16,
27
27
  true,
@@ -29,7 +29,7 @@ test("the pool is set, and left alone when the deployment states its own", async
29
29
  );
30
30
 
31
31
  process.env.UV_THREADPOOL_SIZE = "8";
32
- await import(`../services/thread-pool.js?stated=${Date.now()}`);
32
+ await import(`../bin/thread-pool.js?stated=${Date.now()}`);
33
33
  assert.equal(process.env.UV_THREADPOOL_SIZE, "8", "a stated size is the deployment's to choose");
34
34
  } finally {
35
35
  if (previous === undefined) {
@@ -47,7 +47,7 @@ test("the entry point imports it before anything that could create the pool", as
47
47
 
48
48
  assert.equal(
49
49
  imports[0],
50
- "../services/thread-pool.js",
50
+ "./thread-pool.js",
51
51
  "a module's imports run before its body, so this cannot be a statement in cli.js"
52
52
  );
53
53
  });
@@ -11,7 +11,7 @@
11
11
 
12
12
  import test from "node:test";
13
13
  import assert from "node:assert/strict";
14
- import { Timeline, Timelines } from "../services/output/Timeline.js";
14
+ import { Timeline, Timelines } from "../services/encode/output/Timeline.js";
15
15
 
16
16
  /**
17
17
  * @returns {Timeline}
@@ -73,17 +73,6 @@ test("which segment holds a moment", () => {
73
73
  assert.equal(timeline.indexForTime(99), 3, "past the end is the last segment, not an error");
74
74
  });
75
75
 
76
- test("a fresh table starts with an empty tally of how well its index matched", () => {
77
- const timeline = fourSecondGrid();
78
-
79
- // The tally is a fact about the FILE and its index: asked per session it
80
- // would be answered a different number of times for one film depending on
81
- // how many people happened to watch it.
82
- assert.equal(timeline.indexCheck.checked, 0);
83
- assert.equal(timeline.indexCheck.disagreed, 0);
84
- assert.equal(timeline.indexCheck.firstDisagreementIndex, -1);
85
- });
86
-
87
76
  test("a timeline nobody holds is dropped", () => {
88
77
  const timelines = new Timelines();
89
78
  const kept = timelines.get(Timelines.keyFor("torrent:abc", 0, "keyframe"), fourSecondGrid);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import assert from "node:assert/strict";
10
10
  import { test } from "node:test";
11
- import { baseDrawFrom, costPerMegabyteFrom } from "../services/torrent-cost.js";
11
+ import { baseDrawFrom, costPerMegabyteFrom } from "../services/encode/torrent-cost.js";
12
12
 
13
13
  test("the base draw is the share of a core spent with nothing to do", () => {
14
14
  assert.equal(baseDrawFrom({ cpuSeconds: 0.5, elapsedSeconds: 5 }), 0.1);