@torrent-tv/proxy 2.85.1 → 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 (381) hide show
  1. package/CHANGELOG.md +2138 -1909
  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 -489
  157. package/services/viewer/Viewers.js +368 -257
  158. package/services/viewer/active-output.js +37 -54
  159. package/services/viewer/audio-start.js +46 -0
  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 -44
  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 -518
  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 -172
  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 -9689
  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/test/produced-index.test.js +0 -142
  347. /package/{services → bin}/thread-pool.js +0 -0
  348. /package/services/{available-share.js → encode/available-share.js} +0 -0
  349. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  350. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  351. /package/services/{host-load.js → encode/host-load.js} +0 -0
  352. /package/services/{output → encode/output}/cut-grid.js +0 -0
  353. /package/services/{output → encode/output}/rates.js +0 -0
  354. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  355. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  356. /package/services/{container → media/container}/ebml-reader.js +0 -0
  357. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  358. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  359. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  360. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  361. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  362. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/index.js +0 -0
  364. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  365. /package/services/{disk → storage}/keep.js +0 -0
  366. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  367. /package/services/{disk → storage}/returns.js +0 -0
  368. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  369. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  370. /package/services/{demand → torrent/demand}/Window.js +0 -0
  371. /package/services/{demand → torrent/demand}/index.js +0 -0
  372. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  373. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  374. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  375. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  376. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  377. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  378. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  379. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  380. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  381. /package/services/{priority → viewer}/WaitLedger.js +0 -0
@@ -1,205 +1,204 @@
1
- /**
2
- * @file WHEN NOBODY WANTS A FILE ANY MORE, THE MAP SAYS SO.
3
- *
4
- * The map is the only statement of what is wanted, and until 2.83.4 it could
5
- * make only one of the two statements it exists for. A file whose viewers had
6
- * gone was deleted from the orchestrator's own memory and published nowhere, so
7
- * the bands it had stated on the swarm's behalf stood until the torrent itself
8
- * was removed — read from the demand register, a film nobody had watched for an
9
- * hour was indistinguishable from one being watched now.
10
- *
11
- * Two halves, and both are here: the side that builds the map has to say it,
12
- * and the side that receives it has to act on it without needing the file's
13
- * length, the torrent's list, or the torrent to exist at all.
14
- */
15
-
16
- import test from "node:test";
17
- import assert from "node:assert/strict";
18
- import { PriorityOrchestrator } from "../services/priority/PriorityOrchestrator.js";
19
- import { Viewers } from "../services/viewer/Viewers.js";
20
- import { viewersOf } from "../services/viewer/Viewer.js";
21
- import { TorrentPool } from "../services/torrent-pool.js";
22
- import { demandFor, forgetTorrent } from "../services/download/registry.js";
23
- import { Urgency } from "../services/demand/index.js";
24
-
25
- /**
26
- * A session, as much of one as the orchestrator reads.
27
- *
28
- * @param {{ id: string, fileIndex?: number }} params
29
- * @returns {object}
30
- */
31
- function outputOf({ id, fileIndex = 0 }) {
32
- return {
33
- id,
34
- outputKey: `out:${id}`,
35
- sourceKey: "source-1",
36
- fileIndex,
37
- file: { key: `film-${fileIndex}`, durationSeconds: 600 }
38
- };
39
- }
40
-
41
- /**
42
- * The real orchestrator, with everything it publishes kept.
43
- *
44
- * @param {object[]} sessions
45
- * @returns {{ priority: PriorityOrchestrator, viewers: Viewers, published: object[], publish: (sessions?: object[]) => void }}
46
- */
47
- function over(sessions) {
48
- const published = [];
49
- const viewers = new Viewers();
50
- const priority = new PriorityOrchestrator({
51
- publish: (one) => published.push(one),
52
- viewersOf: (session) => viewersOf(session),
53
- allowanceFor: () => 10
54
- });
55
- return {
56
- priority,
57
- viewers,
58
- published,
59
- publish: (live = sessions) =>
60
- priority.publishFor({ sessionGroups: [live] })
61
- };
62
- }
63
-
64
- test("a file whose viewers have all gone is published as wanting nothing", () => {
65
- const picture = outputOf({ id: "pic" });
66
- const { viewers, published, publish } = over([picture]);
67
- const person = viewers.of(picture, "p");
68
- person.moveTo(300);
69
- publish();
70
- assert.ok(published.at(-1).zones.length > 0, "somebody is watching, so something is wanted");
71
-
72
- // They leave. The session is still there — it outlives them by half an hour —
73
- // so this is the case that used to say nothing at all.
74
- viewers.leaves(picture, "p");
75
- publish();
76
-
77
- const last = published.at(-1);
78
- assert.deepEqual(last.zones, []);
79
- assert.equal(last.sourceKey, "source-1");
80
- assert.equal(last.fileIndex, 0);
81
- });
82
-
83
- test("a file whose session has gone is published as wanting nothing, once", () => {
84
- const picture = outputOf({ id: "pic" });
85
- const { viewers, published, publish } = over([picture]);
86
- viewers.of(picture, "p").moveTo(300);
87
- publish();
88
- const said = published.length;
89
-
90
- // The session is disposed: it is not among the live ones any more.
91
- publish([]);
92
- assert.deepEqual(published.at(-1).zones, []);
93
- assert.equal(published.length, said + 1);
94
-
95
- // And it is not repeated on every pass afterwards — the file is gone from
96
- // this class's memory, and a departure is said once.
97
- publish([]);
98
- publish([]);
99
- assert.equal(published.length, said + 1);
100
- });
101
-
102
- test("an unchanged map is still not republished", () => {
103
- const picture = outputOf({ id: "pic" });
104
- const { viewers, published, publish } = over([picture]);
105
- viewers.of(picture, "p").moveTo(300);
106
- publish();
107
- const said = published.length;
108
- publish();
109
- publish();
110
- assert.equal(published.length, said, "the downloading rebuilds its requests on every one");
111
- });
112
-
113
- /** A torrent that is nothing but one file of a known length. */
114
- function torrentOf({ length = 1_000_000 } = {}) {
115
- return {
116
- infoHash: `hash-${Math.random().toString(36).slice(2)}`,
117
- pieceLength: 1024,
118
- files: [{ offset: 0, length, name: "film.mkv" }],
119
- _selections: { _items: [] },
120
- _select() {},
121
- _deselect() {},
122
- critical() {}
123
- };
124
- }
125
-
126
- const applyPriorityMap = TorrentPool.prototype.applyPriorityMap;
127
-
128
- /**
129
- * @param {object} torrent
130
- * @returns {object[]}
131
- */
132
- function stated(torrent) {
133
- return demandFor(torrent)
134
- .register.windows()
135
- .filter((one) => String(one.claimant).startsWith("priority-map:"));
136
- }
137
-
138
- test("a map with nothing in it withdraws everything that file had stated", () => {
139
- const torrent = torrentOf();
140
- try {
141
- applyPriorityMap.call(null, torrent, 0, [
142
- { from: 0, to: 100, priority: 100 },
143
- { from: 100, to: 600, priority: 50 }
144
- ], 600);
145
- assert.equal(stated(torrent).length, 2);
146
-
147
- applyPriorityMap.call(null, torrent, 0, [], 600);
148
- assert.equal(stated(torrent).length, 0);
149
- } finally {
150
- forgetTorrent(torrent);
151
- }
152
- });
153
-
154
- test("it withdraws that file's bands and nobody else's", () => {
155
- const torrent = torrentOf();
156
- try {
157
- const { register } = demandFor(torrent);
158
- applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 600, priority: 100 }], 600);
159
- // A read stopped on a piece right now, which only a read can say, and the
160
- // background fill, which the pool states for itself. Neither is the map's
161
- // to withdraw.
162
- register.state({
163
- claimant: "read-7:blocked",
164
- fileIndex: 0,
165
- byteStart: 0,
166
- byteEnd: 999,
167
- urgency: Urgency.BLOCKED
168
- });
169
- register.state({
170
- claimant: "background-fill:0",
171
- fileIndex: 0,
172
- byteStart: 1000,
173
- byteEnd: 9999,
174
- urgency: Urgency.TAIL
175
- });
176
-
177
- applyPriorityMap.call(null, torrent, 0, [], 600);
178
-
179
- assert.equal(stated(torrent).length, 0);
180
- assert.deepEqual(
181
- register.windows().map((one) => one.claimant).sort(),
182
- ["background-fill:0", "read-7:blocked"]
183
- );
184
- } finally {
185
- forgetTorrent(torrent);
186
- }
187
- });
188
-
189
- test("a departure is answered even when nothing else about the file is known", () => {
190
- // The length, the duration and the torrent's own list are what the ordinary
191
- // path needs to turn seconds into bytes. A departure needs none of them, and
192
- // it arrives exactly when they are going away: the map that says it is
193
- // published as the last viewer leaves.
194
- const torrent = torrentOf();
195
- try {
196
- applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 600, priority: 100 }], 600);
197
- assert.equal(stated(torrent).length, 1);
198
-
199
- torrent.files = [];
200
- applyPriorityMap.call(null, torrent, 0, [], 0);
201
- assert.equal(stated(torrent).length, 0);
202
- } finally {
203
- forgetTorrent(torrent);
204
- }
205
- });
1
+ /**
2
+ * @file WHEN NOBODY WANTS A FILE ANY MORE, THE MAP SAYS SO.
3
+ *
4
+ * The map is the only statement of what is wanted, and until 2.83.4 it could
5
+ * make only one of the two statements it exists for. A file whose viewers had
6
+ * gone was deleted from the orchestrator's own memory and published nowhere, so
7
+ * the bands it had stated on the swarm's behalf stood until the torrent itself
8
+ * was removed — read from the demand register, a film nobody had watched for an
9
+ * hour was indistinguishable from one being watched now.
10
+ *
11
+ * Two halves, and both are here: the side that builds the map has to say it,
12
+ * and the side that receives it has to act on it without needing the file's
13
+ * length, the torrent's list, or the torrent to exist at all.
14
+ */
15
+
16
+ import test from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import { PriorityOrchestrator } from "../services/viewer/PriorityOrchestrator.js";
19
+ import { Viewers } from "../services/viewer/Viewers.js";
20
+ import { TorrentPool } from "../services/torrent/torrent-pool.js";
21
+ import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
22
+ import { Urgency } from "../services/torrent/demand/index.js";
23
+
24
+ /**
25
+ * A session, as much of one as the orchestrator reads.
26
+ *
27
+ * @param {{ id: string, fileIndex?: number }} params
28
+ * @returns {object}
29
+ */
30
+ function outputOf({ id, fileIndex = 0 }) {
31
+ return {
32
+ id,
33
+ outputKey: `out:${id}`,
34
+ sourceKey: "source-1",
35
+ fileIndex,
36
+ file: { key: `film-${fileIndex}`, durationSeconds: 600 }
37
+ };
38
+ }
39
+
40
+ /**
41
+ * The real orchestrator, with everything it publishes kept.
42
+ *
43
+ * @param {object[]} sessions
44
+ * @returns {{ priority: PriorityOrchestrator, viewers: Viewers, published: object[], publish: (sessions?: object[]) => void }}
45
+ */
46
+ function over(sessions) {
47
+ const published = [];
48
+ const viewers = new Viewers();
49
+ const priority = new PriorityOrchestrator({
50
+ publish: (one) => published.push(one),
51
+ viewers,
52
+ allowanceFor: () => 10
53
+ });
54
+ return {
55
+ priority,
56
+ viewers,
57
+ published,
58
+ publish: (live = sessions) =>
59
+ priority.publishFor({ sessionGroups: [live] })
60
+ };
61
+ }
62
+
63
+ test("a file whose viewers have all gone is published as wanting nothing", () => {
64
+ const picture = outputOf({ id: "pic" });
65
+ const { viewers, published, publish } = over([picture]);
66
+ const person = viewers.of(picture, "p");
67
+ person.moveTo(300);
68
+ publish();
69
+ assert.ok(published.at(-1).zones.length > 0, "somebody is watching, so something is wanted");
70
+
71
+ // They leave. The session is still there — it outlives them by half an hour —
72
+ // so this is the case that used to say nothing at all.
73
+ viewers.leaves(picture, "p");
74
+ publish();
75
+
76
+ const last = published.at(-1);
77
+ assert.deepEqual(last.zones, []);
78
+ assert.equal(last.sourceKey, "source-1");
79
+ assert.equal(last.fileIndex, 0);
80
+ });
81
+
82
+ test("a file whose session has gone is published as wanting nothing, once", () => {
83
+ const picture = outputOf({ id: "pic" });
84
+ const { viewers, published, publish } = over([picture]);
85
+ viewers.of(picture, "p").moveTo(300);
86
+ publish();
87
+ const said = published.length;
88
+
89
+ // The session is disposed: it is not among the live ones any more.
90
+ publish([]);
91
+ assert.deepEqual(published.at(-1).zones, []);
92
+ assert.equal(published.length, said + 1);
93
+
94
+ // And it is not repeated on every pass afterwards — the file is gone from
95
+ // this class's memory, and a departure is said once.
96
+ publish([]);
97
+ publish([]);
98
+ assert.equal(published.length, said + 1);
99
+ });
100
+
101
+ test("an unchanged map is still not republished", () => {
102
+ const picture = outputOf({ id: "pic" });
103
+ const { viewers, published, publish } = over([picture]);
104
+ viewers.of(picture, "p").moveTo(300);
105
+ publish();
106
+ const said = published.length;
107
+ publish();
108
+ publish();
109
+ assert.equal(published.length, said, "the downloading rebuilds its requests on every one");
110
+ });
111
+
112
+ /** A torrent that is nothing but one file of a known length. */
113
+ function torrentOf({ length = 1_000_000 } = {}) {
114
+ return {
115
+ infoHash: `hash-${Math.random().toString(36).slice(2)}`,
116
+ pieceLength: 1024,
117
+ files: [{ offset: 0, length, name: "film.mkv" }],
118
+ _selections: { _items: [] },
119
+ _select() {},
120
+ _deselect() {},
121
+ critical() {}
122
+ };
123
+ }
124
+
125
+ const applyPriorityMap = TorrentPool.prototype.applyPriorityMap;
126
+
127
+ /**
128
+ * @param {object} torrent
129
+ * @returns {object[]}
130
+ */
131
+ function stated(torrent) {
132
+ return demandFor(torrent)
133
+ .register.windows()
134
+ .filter((one) => String(one.claimant).startsWith("priority-map:"));
135
+ }
136
+
137
+ test("a map with nothing in it withdraws everything that file had stated", () => {
138
+ const torrent = torrentOf();
139
+ try {
140
+ applyPriorityMap.call(null, torrent, 0, [
141
+ { from: 0, to: 100, priority: 100 },
142
+ { from: 100, to: 600, priority: 50 }
143
+ ], 600);
144
+ assert.equal(stated(torrent).length, 2);
145
+
146
+ applyPriorityMap.call(null, torrent, 0, [], 600);
147
+ assert.equal(stated(torrent).length, 0);
148
+ } finally {
149
+ forgetTorrent(torrent);
150
+ }
151
+ });
152
+
153
+ test("it withdraws that file's bands and nobody else's", () => {
154
+ const torrent = torrentOf();
155
+ try {
156
+ const { register } = demandFor(torrent);
157
+ applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 600, priority: 100 }], 600);
158
+ // A read stopped on a piece right now, which only a read can say, and the
159
+ // background fill, which the pool states for itself. Neither is the map's
160
+ // to withdraw.
161
+ register.state({
162
+ claimant: "read-7:blocked",
163
+ fileIndex: 0,
164
+ byteStart: 0,
165
+ byteEnd: 999,
166
+ urgency: Urgency.BLOCKED
167
+ });
168
+ register.state({
169
+ claimant: "background-fill:0",
170
+ fileIndex: 0,
171
+ byteStart: 1000,
172
+ byteEnd: 9999,
173
+ urgency: Urgency.TAIL
174
+ });
175
+
176
+ applyPriorityMap.call(null, torrent, 0, [], 600);
177
+
178
+ assert.equal(stated(torrent).length, 0);
179
+ assert.deepEqual(
180
+ register.windows().map((one) => one.claimant).sort(),
181
+ ["background-fill:0", "read-7:blocked"]
182
+ );
183
+ } finally {
184
+ forgetTorrent(torrent);
185
+ }
186
+ });
187
+
188
+ test("a departure is answered even when nothing else about the file is known", () => {
189
+ // The length, the duration and the torrent's own list are what the ordinary
190
+ // path needs to turn seconds into bytes. A departure needs none of them, and
191
+ // it arrives exactly when they are going away: the map that says it is
192
+ // published as the last viewer leaves.
193
+ const torrent = torrentOf();
194
+ try {
195
+ applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 600, priority: 100 }], 600);
196
+ assert.equal(stated(torrent).length, 1);
197
+
198
+ torrent.files = [];
199
+ applyPriorityMap.call(null, torrent, 0, [], 0);
200
+ assert.equal(stated(torrent).length, 0);
201
+ } finally {
202
+ forgetTorrent(torrent);
203
+ }
204
+ });