@torrent-tv/proxy 2.86.0 → 2.87.1

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 (385) hide show
  1. package/CHANGELOG.md +2142 -1915
  2. package/CLAUDE.md +322 -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/browser-proxy-contract.md +94 -0
  7. package/docs/container-architecture.md +218 -213
  8. package/docs/download-architecture.md +7 -7
  9. package/docs/encode-architecture.md +287 -11
  10. package/docs/encode-run-state.md +2 -2
  11. package/docs/logs.md +97 -97
  12. package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
  13. package/openspec/changes/adaptive-bitrate/design.md +2 -2
  14. package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
  15. package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
  16. package/openspec/changes/chunked-request-bodies/design.md +1 -1
  17. package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
  18. package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
  19. package/openspec/changes/cold-start/design.md +1 -1
  20. package/openspec/changes/disk-cap/proposal.md +1 -1
  21. package/openspec/changes/proxy-observability/proposal.md +2 -2
  22. package/openspec/changes/track-selection/proposal.md +1 -1
  23. package/openspec/changes/transcode-quality/proposal.md +1 -1
  24. package/package.json +2 -2
  25. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  26. package/research/double-spawn-2026-09-10.md +171 -171
  27. package/routes/api/playback-plan/post.js +2 -2
  28. package/routes/api/sources/files/get.js +1 -1
  29. package/routes/api/sources/stats/get.js +1 -1
  30. package/routes/api/sources/warm/post.js +10 -3
  31. package/routes/api/subtitles/get.js +230 -230
  32. package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
  33. package/routes/api/transcode-sessions/net-report/post.js +129 -94
  34. package/routes/api/transcode-sessions/post.js +160 -139
  35. package/routes/api/transcode-sessions/progress/get.js +3 -3
  36. package/routes/api/transcode-sessions/release/post.js +3 -3
  37. package/routes/api/transcode-sessions/seek/post.js +12 -3
  38. package/routes/stream/get.js +2 -2
  39. package/routes/transcode/audio-file/get.js +15 -6
  40. package/routes/transcode/audio-warm/get.js +9 -6
  41. package/routes/transcode/session-file/get.js +52 -268
  42. package/routes/transcode/variant-file/get.js +46 -7
  43. package/routes/transcode/variant-warm/get.js +19 -6
  44. package/scripts/render-run-graph.js +1 -1
  45. package/server.js +720 -465
  46. package/services/encode/CoverageMap.js +428 -428
  47. package/services/encode/CushionReport.js +345 -0
  48. package/services/encode/EncodeAdmission.js +332 -0
  49. package/services/{orchestrators → encode}/EncodeOrchestrator.js +307 -29
  50. package/services/encode/EncodeRun.js +16 -9
  51. package/services/encode/EncodeRuns.js +1229 -0
  52. package/services/encode/Encoder.js +18 -2
  53. package/services/encode/EncoderSelection.js +38 -0
  54. package/services/encode/LocalObservations.js +359 -0
  55. package/services/encode/NvencEncoder.js +5 -0
  56. package/services/encode/OutputOpening.js +755 -0
  57. package/services/encode/OutputTimes.js +579 -0
  58. package/services/encode/Renditions.js +2162 -0
  59. package/services/encode/RunProgress.js +84 -0
  60. package/services/encode/SoftwareEncoder.js +12 -7
  61. package/services/encode/args.js +402 -186
  62. package/services/encode/calibration.js +361 -0
  63. package/services/encode/contention.js +128 -128
  64. package/services/encode/encode-run-state.js +16 -20
  65. package/services/encode/fingerprint.js +322 -0
  66. package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
  67. package/services/{learned-median.js → encode/learned-median.js} +11 -0
  68. package/services/encode/output/EncodedOutput.js +57 -0
  69. package/services/encode/output/Output.js +131 -0
  70. package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
  71. package/services/encode/output/OutputSpec.js +390 -0
  72. package/services/{output → encode/output}/Timeline.js +12 -37
  73. package/services/encode/output/index.js +20 -0
  74. package/services/encode/output/ladder.js +32 -0
  75. package/services/{output → encode/output}/playlists.js +3 -0
  76. package/services/encode/output-key-format.js +21 -0
  77. package/services/encode/quality/EncodeCost.js +1083 -0
  78. package/services/encode/quality/HostLoad.js +525 -0
  79. package/services/encode/quality/HostTimings.js +249 -0
  80. package/services/encode/quality/OutputQualityState.js +28 -0
  81. package/services/encode/quality/QualityController.js +935 -0
  82. package/services/encode/quality/QualityOffer.js +416 -0
  83. package/services/encode/quality/drain-threat.js +47 -0
  84. package/services/encode/quality/link-budget.js +254 -0
  85. package/services/encode/quality/output-format.js +266 -0
  86. package/services/encode/quality/serving-output.js +216 -0
  87. package/services/encode/quality/visible-rung.js +57 -0
  88. package/services/encode/run-command.js +23 -20
  89. package/services/encode/run-inputs.js +42 -0
  90. package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
  91. package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
  92. package/services/encode/segment-formats/init-compat.js +324 -0
  93. package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
  94. package/services/encode/throughput.js +97 -0
  95. package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
  96. package/services/media/KeyframeTables.js +316 -0
  97. package/services/{source → media}/SourceFile.js +7 -55
  98. package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
  99. package/services/media/SubtitleOrchestrator.js +167 -0
  100. package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
  101. package/services/{container → media/container}/AviContainer.js +266 -266
  102. package/services/{container → media/container}/Container.js +596 -601
  103. package/services/media/container/ContainerFactory.js +56 -0
  104. package/services/media/container/KeyframeTable.js +272 -0
  105. package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
  106. package/services/{container → media/container}/Mp4Container.js +1244 -1244
  107. package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
  108. package/services/media/keyframe-probe.js +157 -0
  109. package/services/media/media-info-probe.js +93 -0
  110. package/services/{playback-planner.js → media/playback-planner.js} +82 -39
  111. package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
  112. package/services/{tracks → media/tracks}/language-detect.js +174 -174
  113. package/services/server/OutputLifecycle.js +418 -0
  114. package/services/server/SegmentServing.js +1146 -0
  115. package/services/server/ViewerRequests.js +522 -0
  116. package/services/server/controllers/PlaybackController.js +36 -0
  117. package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
  118. package/services/server/http-responses.js +60 -0
  119. package/services/server/transcode-session-files.js +325 -0
  120. package/services/server/wire-outputs.js +983 -0
  121. package/services/storage/Diagnostics.js +202 -0
  122. package/services/storage/MachineBudget.js +235 -0
  123. package/services/{piece-store → storage}/allowance.js +107 -107
  124. package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
  125. package/services/{files → storage/files}/CompletedFiles.js +116 -1
  126. package/services/{disk → storage}/free.js +33 -0
  127. package/services/storage/machine-memory.js +56 -0
  128. package/services/{memory-report.js → storage/memory-report.js} +575 -596
  129. package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
  130. package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
  131. package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
  132. package/services/storage/wire.js +175 -0
  133. package/services/{download → torrent/download}/SwarmSelection.js +2 -2
  134. package/services/{download → torrent/download}/registry.js +2 -2
  135. package/services/torrent/files.js +1 -1
  136. package/services/torrent/piece-store-of.js +29 -0
  137. package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
  138. package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
  139. package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
  140. package/services/{torrent-worker → torrent/worker}/client.js +77 -58
  141. package/services/torrent/worker/held-bytes.js +155 -0
  142. package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
  143. package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
  144. package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
  145. package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
  146. package/services/torrent/worker/resume-warm.js +107 -0
  147. package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
  148. package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
  149. package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
  150. package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
  151. package/services/{health-collector.js → transport/health-collector.js} +6 -35
  152. package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
  153. package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
  154. package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
  155. package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
  156. package/services/viewer/Assignments.js +340 -0
  157. package/services/{priority → viewer}/PriorityMap.js +390 -390
  158. package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
  159. package/services/viewer/Viewer.js +657 -483
  160. package/services/viewer/Viewers.js +368 -257
  161. package/services/viewer/active-output.js +37 -54
  162. package/services/viewer/audio-start.js +4 -3
  163. package/services/viewer/buffer-trend.js +81 -0
  164. package/services/viewer/choices.js +647 -0
  165. package/services/viewer/positions.js +126 -120
  166. package/services/viewer/report-intake.js +46 -45
  167. package/test/asset-paths.test.js +28 -0
  168. package/test/audio-inventory.test.js +3 -3
  169. package/test/audio-track-bitrate.test.js +40 -0
  170. package/test/auto-quality-step.test.js +707 -515
  171. package/test/available-share.test.js +1 -1
  172. package/test/background-fill.test.js +1 -1
  173. package/test/client-log-file.test.js +110 -110
  174. package/test/completed-files.test.js +1 -1
  175. package/test/concurrent-cost.test.js +13 -3
  176. package/test/container-media-info.test.js +3 -3
  177. package/test/core-dumps.test.js +1 -1
  178. package/test/coverage-follows-the-disk.test.js +46 -9
  179. package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
  180. package/test/cut-times-timeline.test.js +2 -2
  181. package/test/cuts-follow-published-grid.test.js +9 -8
  182. package/test/data-channel-frame.test.js +1 -1
  183. package/test/dead-torrent-handle.test.js +1 -1
  184. package/test/declared-rates.test.js +2 -2
  185. package/test/declared-tracks.test.js +2 -2
  186. package/test/decode-cost-fit.test.js +1 -1
  187. package/test/decode-cost.test.js +34 -25
  188. package/test/delivery-probe.test.js +1 -1
  189. package/test/demand-register.test.js +4 -4
  190. package/test/dht-bootstrap.test.js +1 -1
  191. package/test/drain-threat.test.js +65 -0
  192. package/test/duplicate-add.test.js +1 -1
  193. package/test/encode-admission.test.js +146 -0
  194. package/test/encode-cost.test.js +199 -10
  195. package/test/encode-orchestrator.test.js +39 -1
  196. package/test/encode-plan-viewers.test.js +2 -2
  197. package/test/encode-run.test.js +2 -2
  198. package/test/encode-slope.test.js +1 -1
  199. package/test/encoded-output.test.js +39 -0
  200. package/test/encoder-readings.test.js +1 -1
  201. package/test/every-claimant-is-told-its-share.test.js +152 -0
  202. package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
  203. package/test/failed-start-breaker.test.js +81 -30
  204. package/test/fastest-wires.test.js +1 -1
  205. package/test/ffmpeg-stream-counts.test.js +1 -1
  206. package/test/file-edges.test.js +3 -3
  207. package/test/fmp4-self-contained.test.js +1 -1
  208. package/test/fmp4-timestamps.test.js +1 -1
  209. package/test/fmp4-track-completeness.test.js +1 -1
  210. package/test/format-keyed-outputs.test.js +78 -0
  211. package/test/health-metrics.test.js +3 -2
  212. package/test/held-request-width.test.js +22 -23
  213. package/test/helpers/encode-run.js +136 -136
  214. package/test/helpers/held-file.js +29 -0
  215. package/test/helpers/manager.js +57 -0
  216. package/test/helpers/output-spec.js +63 -0
  217. package/test/host-load.test.js +1 -1
  218. package/test/index-accuracy-is-about-the-file.test.js +128 -0
  219. package/test/init-compat.test.js +219 -0
  220. package/test/input-lost-quiets-the-plan.test.js +1 -1
  221. package/test/input-unavailable.test.js +1 -1
  222. package/test/keeping-period.test.js +4 -4
  223. package/test/keyframe-index-accuracy.test.js +97 -45
  224. package/test/keyframe-table-budget.test.js +28 -33
  225. package/test/keyframes-belong-to-the-file.test.js +174 -57
  226. package/test/learned-median.test.js +1 -1
  227. package/test/letting-go-asks-who-is-watching.test.js +191 -0
  228. package/test/limit-row-by-area.test.js +60 -0
  229. package/test/link-budget.test.js +111 -0
  230. package/test/link-probe.test.js +56 -56
  231. package/test/local-encoding-data.test.js +273 -0
  232. package/test/look-ahead-coverage.test.js +1 -1
  233. package/test/manager-surface.test.js +294 -0
  234. package/test/matroska-blocks.test.js +0 -0
  235. package/test/matroska-cues-track.test.js +1 -1
  236. package/test/memory-budget.test.js +2 -2
  237. package/test/mp4-composition-times.test.js +0 -0
  238. package/test/mp4-subtitles.test.js +1 -1
  239. package/test/one-authority.test.js +45 -41
  240. package/test/one-budget.test.js +149 -0
  241. package/test/one-output-one-session.test.js +204 -0
  242. package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
  243. package/test/one-serving-output-per-height.test.js +458 -0
  244. package/test/orchestrator-wired.test.js +46 -45
  245. package/test/output-catalog.test.js +39 -0
  246. package/test/output-shape.test.js +4 -51
  247. package/test/output-spec.test.js +251 -157
  248. package/test/output-speed.test.js +17 -15
  249. package/test/outputs-being-prepared.test.js +40 -0
  250. package/test/packet-witness-ring.test.js +2 -2
  251. package/test/packet-witness.test.js +1 -1
  252. package/test/piece-disk-store.test.js +2 -2
  253. package/test/piece-from-whole-file.test.js +1 -1
  254. package/test/piece-lru.test.js +1 -1
  255. package/test/piece-reader.test.js +2 -2
  256. package/test/piece-store-eviction.test.js +1 -1
  257. package/test/piece-store-never-refuses.test.js +1 -1
  258. package/test/piece-store-reservations.test.js +1 -1
  259. package/test/piece-store-slow-disk.test.js +1 -1
  260. package/test/piece-tail.test.js +1 -1
  261. package/test/plan-host-timings.test.js +1 -1
  262. package/test/plan-is-steady.test.js +225 -225
  263. package/test/priority-map-download.test.js +4 -4
  264. package/test/priority-map-emptied.test.js +204 -205
  265. package/test/priority-map-per-output.test.js +215 -209
  266. package/test/priority-map.test.js +1 -1
  267. package/test/probe-wedge-certainty.test.js +1 -1
  268. package/test/produced-copy-choice.test.js +15 -60
  269. package/test/quality-offer.test.js +162 -0
  270. package/test/quality-variants.test.js +1229 -1167
  271. package/test/rate-control-in-the-output.test.js +180 -0
  272. package/test/read-survives-withdrawal.test.js +2 -2
  273. package/test/read-window.test.js +10 -3
  274. package/test/resume-warm.test.js +1 -1
  275. package/test/run-inputs.test.js +80 -0
  276. package/test/run-position-follows-published-grid.test.js +13 -11
  277. package/test/run-progress.test.js +65 -0
  278. package/test/same-height-switch.test.js +664 -0
  279. package/test/seek-frees-the-viewers-requests.test.js +217 -0
  280. package/test/seek-landing.test.js +13 -13
  281. package/test/seek-target-not-superseded.test.js +28 -29
  282. package/test/segment-closed-on-both-branches.test.js +96 -0
  283. package/test/segment-cut-times.test.js +1 -1
  284. package/test/segment-hold-supersede.test.js +28 -35
  285. package/test/segment-judged-by-its-own-header.test.js +221 -0
  286. package/test/segment-serve-wiring.test.js +130 -39
  287. package/test/segment-store-eviction.test.js +22 -7
  288. package/test/segment-store.test.js +98 -14
  289. package/test/segment-true-start.test.js +1 -1
  290. package/test/segments-are-shared.test.js +17 -16
  291. package/test/send-chunk-bytes.test.js +1 -1
  292. package/test/serving-existing-output.test.js +102 -0
  293. package/test/serving-output.test.js +102 -0
  294. package/test/shared-piece-store.test.js +1 -1
  295. package/test/source-file.test.js +1 -1
  296. package/test/stale-request-after-seek.test.js +17 -21
  297. package/test/startup-readings.test.js +10 -5
  298. package/test/subtitle-cue-framing.test.js +5 -5
  299. package/test/subtitle-cue-source.test.js +36 -38
  300. package/test/subtitle-cue-walk.test.js +11 -10
  301. package/test/subtitle-defaults.test.js +1 -1
  302. package/test/subtitle-language.test.js +4 -4
  303. package/test/subtitle-track-numbering.test.js +5 -4
  304. package/test/supply-margin.test.js +1 -1
  305. package/test/swarm-follows-readers.test.js +3 -3
  306. package/test/swarm-reach.test.js +1 -1
  307. package/test/swarm-selection.test.js +3 -3
  308. package/test/tail-duplication.test.js +1 -1
  309. package/test/the-channel-has-both-ends.test.js +91 -0
  310. package/test/the-torrent-thread-serves-bytes.test.js +107 -0
  311. package/test/thread-pool.test.js +3 -3
  312. package/test/timeline.test.js +1 -12
  313. package/test/torrent-cost.test.js +1 -1
  314. package/test/tracks-begin-together.test.js +38 -24
  315. package/test/tunnel-renewal.test.js +1 -1
  316. package/test/two-viewers-one-picture.test.js +120 -87
  317. package/test/upload-hurry.test.js +3 -3
  318. package/test/usrsctp-state.test.js +1 -1
  319. package/test/video-facts.test.js +1 -1
  320. package/test/viewer-assignments.test.js +143 -0
  321. package/test/viewer-outputs.test.js +60 -52
  322. package/test/viewer-presence.test.js +22 -25
  323. package/test/viewer-requests-admission.test.js +89 -0
  324. package/test/viewer-states.test.js +189 -202
  325. package/test/viewer-subtitle-subscription.test.js +1 -1
  326. package/test/viewer.test.js +139 -136
  327. package/test/viewing-generations.test.js +390 -0
  328. package/test/visible-rung.test.js +37 -0
  329. package/test/wait-ledger.test.js +1 -1
  330. package/test/webrtc-shim.test.js +3 -3
  331. package/test/wedge-certainty.test.js +2 -2
  332. package/test/withdraw-piece-claim.test.js +2 -2
  333. package/test/worker-channel.test.js +1 -31
  334. package/utils/client-log-file.js +151 -151
  335. package/scripts/read-container-index.mjs +0 -108
  336. package/services/container/ContainerFactory.js +0 -122
  337. package/services/controllers/PlaybackController.js +0 -33
  338. package/services/disk/DiskSpace.js +0 -150
  339. package/services/disk/wire.js +0 -61
  340. package/services/hls-session-manager.js +0 -9608
  341. package/services/orchestrators/SubtitleOrchestrator.js +0 -160
  342. package/services/output/Output.js +0 -94
  343. package/services/output/OutputSpec.js +0 -202
  344. package/services/output/index.js +0 -1
  345. package/services/output/ladder.js +0 -26
  346. package/services/produced-index.js +0 -222
  347. package/services/quality/EncodeCost.js +0 -555
  348. package/services/torrent-worker/container-tracks.js +0 -414
  349. package/services/viewer/link-readings.js +0 -56
  350. package/test/produced-index.test.js +0 -142
  351. /package/{services → bin}/thread-pool.js +0 -0
  352. /package/services/{available-share.js → encode/available-share.js} +0 -0
  353. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  354. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  355. /package/services/{host-load.js → encode/host-load.js} +0 -0
  356. /package/services/{output → encode/output}/cut-grid.js +0 -0
  357. /package/services/{output → encode/output}/rates.js +0 -0
  358. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  359. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  360. /package/services/{container → media/container}/ebml-reader.js +0 -0
  361. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  362. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  364. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  365. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  366. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  367. /package/services/{tracks → media/tracks}/index.js +0 -0
  368. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  369. /package/services/{disk → storage}/keep.js +0 -0
  370. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  371. /package/services/{disk → storage}/returns.js +0 -0
  372. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  373. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  374. /package/services/{demand → torrent/demand}/Window.js +0 -0
  375. /package/services/{demand → torrent/demand}/index.js +0 -0
  376. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  377. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  378. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  379. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  380. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  381. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  382. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  383. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  384. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  385. /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
+ });