@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
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file Who takes what of this machine, and how each of them is told its share.
3
+ *
4
+ * Kept apart from the owner because the owner knows nothing about this proxy —
5
+ * it reads what is free, divides it and hands out shares — and kept out of the
6
+ * session manager because a list of claimants is not a fact about a session.
7
+ *
8
+ * A CLAIMANT IS WHOEVER HOLDS BYTES, and every one of them is here. Four do:
9
+ * the torrent's pieces in memory, the pieces spilled from there to disk, the
10
+ * segments an encoder produced, the files downloaded whole — and the evidence,
11
+ * which is a claimant with a rule of its own (`Diagnostics.js`). Three of the
12
+ * five live on the torrent thread, so they arrive as pairs of closures over its
13
+ * channel rather than as the objects themselves.
14
+ *
15
+ * **Which RESOURCE each takes is read, not assumed.** Memory is one. Disk is
16
+ * one per device, because two filesystems cannot pay for each other: measured
17
+ * on the addon host 2026-09-05, `/tmp` — the segments and the spill — is the
18
+ * overlay, and `/data` — the evidence — is ext4 on the nvme. One figure divided
19
+ * between claimants on both gave each a share of a disk it does not write to.
20
+ *
21
+ * What is NOT a claimant is anything that merely counts somebody else's bytes.
22
+ * The torrent pool held a "disk cap" of 10 GB over `torrentDownloadedBytes` — a
23
+ * sum over WebTorrent's bitfield, so a piece held purely in MEMORY told against
24
+ * a ceiling called disk, while the bytes it was meant to bound belong to the
25
+ * spill and to the whole files, which have owners of their own. It owned
26
+ * nothing and is gone.
27
+ */
28
+
29
+ import { deviceOf } from "./free.js";
30
+ import { MachineBudget } from "./MachineBudget.js";
31
+ import { availableMemoryBytes } from "./machine-memory.js";
32
+
33
+ /**
34
+ * Build the owner of this machine's room and register everything that takes any.
35
+ *
36
+ * @param {object} params
37
+ * @param {{ root: string, stats: () => { bytes: number } }} params.segmentStore
38
+ * @param {{ held: () => number, allow: (bytes: number) => unknown }} [params.spill] -
39
+ * The pieces the memory store spills. They live on the torrent thread, so
40
+ * this is a pair of closures over the channel rather than the pool itself.
41
+ * @param {{ held: () => number, wanted: () => number, allow: (bytes: number) => unknown }} [params.memory] -
42
+ * The pieces held IN memory, on the torrent thread as well. Told their share
43
+ * by the same owner that divides the disk, because the two trade: what does
44
+ * not fit in memory is spilled, so this share decides how much disk is needed.
45
+ * @param {{ held: () => number, allow: (bytes: number) => unknown, root?: string }} [params.wholeFiles] -
46
+ * Files downloaded whole and kept as files, on the torrent thread as well.
47
+ * @param {import("./Diagnostics.js").Diagnostics} [params.diagnostics] - The
48
+ * evidence. Built where the collectors are, so one object is both registered
49
+ * here and consulted by them.
50
+ * @param {string} [params.diagnosticsRoot] - Where the evidence is written,
51
+ * which on the addon host is a different device from everything else.
52
+ * @param {(directory: string) => Promise<number | null>} params.readFree
53
+ * @param {import("./MachineBudget.js").BudgetPolicy} [params.policy] - What the
54
+ * operator has said this proxy may take. Absent, the measured default.
55
+ * @param {{ info: Function, warn?: Function }} [params.logger]
56
+ * @returns {{ revise: () => Promise<unknown>, segmentBytes: () => number, diagnostics: object | null, describe: () => string }}
57
+ * What the segments may hold is asked for rather than pushed: zero until the
58
+ * first revision, and zero stops growth rather than licensing it.
59
+ */
60
+ export function wireMachineBudget({
61
+ segmentStore,
62
+ spill,
63
+ memory,
64
+ wholeFiles,
65
+ diagnostics,
66
+ diagnosticsRoot = "",
67
+ readFree,
68
+ policy,
69
+ logger
70
+ }) {
71
+ // THE OPERATOR NAMES ONE FIGURE FOR DISK, and disk is one resource per
72
+ // device — so the floor they named is given to every disk resource as it is
73
+ // defined, rather than asking them to name a device they have never heard of.
74
+ const floors = { ...(policy?.floors ?? {}) };
75
+ const diskFloorBytes = Math.max(0, Number(policy?.diskFloorBytes) || 0);
76
+ const effective = { ...(policy ?? {}), floors };
77
+ const budget = new MachineBudget({ policy: effective, logger });
78
+ /**
79
+ * @param {string} name
80
+ * @param {string} directory
81
+ */
82
+ const defineDisk = (name, directory) => {
83
+ if (diskFloorBytes > 0 && !(name in floors)) {
84
+ floors[name] = diskFloorBytes;
85
+ }
86
+ budget.defineResource({ name, readFree: () => readFree(directory) });
87
+ };
88
+
89
+ budget.defineResource({ name: "memory", readFree: () => availableMemoryBytes() });
90
+
91
+ const segmentsOn = deviceOf(segmentStore.root);
92
+ defineDisk(segmentsOn, segmentStore.root);
93
+
94
+ let segmentBytes = 0;
95
+ budget.register({
96
+ name: "segments",
97
+ resource: segmentsOn,
98
+ held: () => segmentStore.stats().bytes,
99
+ // The whole of every film anybody is watching. There is no smaller honest
100
+ // answer, so it asks for everything and is cut in proportion like the rest.
101
+ wanted: () => Number.MAX_SAFE_INTEGER,
102
+ allow: (bytes) => {
103
+ segmentBytes = bytes;
104
+ }
105
+ });
106
+
107
+ if (typeof memory?.allow === "function") {
108
+ budget.register({
109
+ name: "pieces in memory",
110
+ resource: "memory",
111
+ held: () => memory.held?.() ?? 0,
112
+ wanted: () => memory.wanted?.() ?? 0,
113
+ allow: (bytes) => {
114
+ void memory.allow(bytes);
115
+ }
116
+ });
117
+ }
118
+
119
+ if (typeof spill?.allow === "function") {
120
+ // The share travels the channel that already carries everything else, and
121
+ // the reply says what they hold — one exchange, both directions.
122
+ budget.register({
123
+ name: "spilled pieces",
124
+ resource: segmentsOn,
125
+ held: () => spill.held?.() ?? 0,
126
+ wanted: () => Number.MAX_SAFE_INTEGER,
127
+ allow: (bytes) => {
128
+ void spill.allow(bytes);
129
+ }
130
+ });
131
+ }
132
+
133
+ if (typeof wholeFiles?.allow === "function") {
134
+ const filesOn = wholeFiles.root ? deviceOf(wholeFiles.root) : segmentsOn;
135
+ defineDisk(filesOn, wholeFiles.root || segmentStore.root);
136
+ budget.register({
137
+ name: "whole files",
138
+ resource: filesOn,
139
+ held: () => wholeFiles.held?.() ?? 0,
140
+ // A whole file is worth keeping while anybody may read it again, and
141
+ // there is no smaller honest answer than "all of them" — like the rest,
142
+ // it is cut in proportion.
143
+ wanted: () => Number.MAX_SAFE_INTEGER,
144
+ allow: (bytes) => {
145
+ void wholeFiles.allow(bytes);
146
+ }
147
+ });
148
+ }
149
+
150
+ if (diagnostics) {
151
+ const evidenceOn = diagnosticsRoot ? deviceOf(diagnosticsRoot) : segmentsOn;
152
+ defineDisk(evidenceOn, diagnosticsRoot || segmentStore.root);
153
+ budget.register({
154
+ name: "diagnostics",
155
+ resource: evidenceOn,
156
+ held: () => diagnostics.held(),
157
+ // What it holds plus one more of the largest kind seen — measured, and
158
+ // nothing at all before a fault has happened.
159
+ wanted: () => diagnostics.wanted(),
160
+ allow: (bytes) => diagnostics.allow(bytes)
161
+ });
162
+ }
163
+
164
+ return {
165
+ revise: async () => {
166
+ // Re-read the evidence before dividing: it is the one claimant whose
167
+ // bytes are written by something that never reports them.
168
+ await diagnostics?.measure();
169
+ return budget.revise();
170
+ },
171
+ segmentBytes: () => segmentBytes,
172
+ diagnostics: diagnostics ?? null,
173
+ describe: () => budget.describe()
174
+ };
175
+ }
@@ -29,7 +29,7 @@ import {
29
29
  Urgency,
30
30
  urgencyName
31
31
  } from "../demand/index.js";
32
- import { findSharedStore } from "../piece-store/shared-piece-store.js";
32
+ import { pieceStoreOf } from "../piece-store-of.js";
33
33
 
34
34
  export class SwarmSelection {
35
35
  #torrent;
@@ -50,7 +50,7 @@ export class SwarmSelection {
50
50
  * piece store is reached. Injectable so a test can drive the memory
51
51
  * projection without constructing a real store.
52
52
  */
53
- constructor({ torrent, register, findStore = findSharedStore }) {
53
+ constructor({ torrent, register, findStore = pieceStoreOf }) {
54
54
  this.#torrent = torrent;
55
55
  this.#register = register;
56
56
  this.#findStore = findStore;
@@ -2,11 +2,11 @@
2
2
  * @file One demand register and one swarm selection per torrent, found from the
3
3
  * torrent itself.
4
4
  *
5
- * The same shape the piece store already uses — `findSharedStore(torrent)`
5
+ * The same shape the piece store already uses — `pieceStoreOf(torrent)`
6
6
  * reaches the store without it being threaded through every call — and for the
7
7
  * same reason: the reader, the pool and the background fill all need the same
8
8
  * instance, and passing it through six layers of arguments would make the
9
- * plumbing bigger than the thing.
9
+ * argument lists bigger than the thing they carry.
10
10
  *
11
11
  * Kept in a live set as well as a weak map, because one question cannot be
12
12
  * answered per torrent: whether ANYTHING anywhere is still missing something
@@ -6,7 +6,7 @@
6
6
  * picture — `Rus Sound/<name>.mka`, `Sub/[group]/<name>.ass` — and a viewer who
7
7
  * cannot reach them is watching the release without half of what it carries.
8
8
  * This module answers only which file goes with which; what is inside a file is
9
- * the container layer's business (`services/container/`), and what a viewer is
9
+ * the container layer's business (`services/media/container/`), and what a viewer is
10
10
  * shown is composed in the browser, where the locale is known.
11
11
  *
12
12
  * **Why it lives in `torrent/`.** A container knows only itself, and a track
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file The piece store behind a torrent, reached through what it does.
3
+ *
4
+ * WebTorrent wraps the store it was given in stores of its own, each holding
5
+ * the next one as `store`, so the store the torrent component talks to is found
6
+ * by walking that chain. It is recognised by the interface this component uses
7
+ * — which pieces the readers have asked to keep (`protectedRanges`) — and not by
8
+ * its class: the torrent component does not import the storage component's
9
+ * implementation, it is handed it where the thread is assembled
10
+ * (`worker/worker.js`).
11
+ */
12
+
13
+ /** How deep WebTorrent's own wrappers are allowed to go before the walk gives up. */
14
+ const WRAPPER_DEPTH = 8;
15
+
16
+ /**
17
+ * @param {{ store?: object } | null | undefined} torrent
18
+ * @returns {object | null}
19
+ */
20
+ export function pieceStoreOf(torrent) {
21
+ let candidate = torrent?.store;
22
+ for (let depth = 0; candidate && depth < WRAPPER_DEPTH; depth += 1) {
23
+ if (typeof candidate.protectedRanges === "function") {
24
+ return candidate;
25
+ }
26
+ candidate = candidate.store;
27
+ }
28
+ return null;
29
+ }
@@ -2,24 +2,24 @@
2
2
  * @file WebTorrent client pool.
3
3
  *
4
4
  * Manages a shared WebTorrent client instance and a map of active torrents
5
- * keyed by their own infohash (see `torrent-source-key.js`), so a magnet and
5
+ * keyed by their own infohash (see `torrent/torrent-source-key.js`), so a magnet and
6
6
  * a `.torrent` file for the same content share one entry. Tracks file-level
7
7
  * usage so that only the pieces needed by active streams are selected for
8
8
  * download.
9
9
  */
10
10
 
11
- import { IDLE_KEEP_MS } from "./disk/keep.js";
11
+ import { IDLE_KEEP_MS } from "../storage/keep.js";
12
12
  import dns from "node:dns/promises";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
15
- import { rmSync, statfsSync } from "node:fs";
15
+ import { rmSync } from "node:fs";
16
16
  import WebTorrent from "webtorrent";
17
- import { logger } from "../utils/logger.js";
18
- import { SharedPieceStore, findSharedStore } from "./piece-store/shared-piece-store.js";
17
+ import { logger } from "../../utils/logger.js";
18
+ import { pieceStoreOf } from "./piece-store-of.js";
19
19
  import { Urgency, urgencyName } from "./demand/index.js";
20
20
  import { demandFor, forgetTorrent, reconcileAll, hasUnmetDemand } from "./download/registry.js";
21
21
  import { withdrawClaim } from "./download/withdraw-claim.js";
22
- import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "./priority/PriorityMap.js";
22
+ import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "../viewer/PriorityMap.js";
23
23
  import { deriveSourceKey } from "./torrent-source-key.js";
24
24
 
25
25
  /** How a window stated from the priority map names itself. */
@@ -89,15 +89,6 @@ const PRIORITY_WINDOW_BYTES = 16 * 1024 * 1024;
89
89
  const HEADER_HEAD_BYTES = 256 * 1024;
90
90
  const HEADER_TAIL_BYTES = 2 * 1024 * 1024;
91
91
 
92
- // Global disk cap. Downloaded torrent data is removed on idle TTL and at
93
- // shutdown, but under pressure (several large files within the TTL window)
94
- // it can still fill a small HA host's disk (SD/eMMC), which can take down
95
- // Home Assistant itself. When the total exceeds the cap, whole torrents with
96
- // no active reader are evicted least-recently-used first. Active torrents are
97
- // never evicted (we cannot delete what is playing).
98
- const DISK_CAP_ABSOLUTE_MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GB
99
- const DISK_CAP_SWEEP_INTERVAL_MS = 30_000;
100
-
101
92
  // Adaptive upload. Seeding to the BitTorrent swarm does not help our viewer (we
102
93
  // deliver over our own WebRTC/HTTPS channel) — it is pure uplink cost and the
103
94
  // riskiest legal act (active distribution). So the default is minimal: a
@@ -528,23 +519,6 @@ function formatWarning(warning) {
528
519
  return stack || warning.message;
529
520
  }
530
521
 
531
- /**
532
- * @param {string} storePath
533
- * @returns {number}
534
- */
535
- function computeDefaultDiskCap(storePath) {
536
- try {
537
- const stat = statfsSync(storePath);
538
- const freeBytes = stat.bavail * stat.bsize;
539
- if (Number.isFinite(freeBytes) && freeBytes > 0) {
540
- return Math.min(DISK_CAP_ABSOLUTE_MAX_BYTES, Math.floor(freeBytes / 2));
541
- }
542
- } catch {
543
- // statfs unavailable (old Node / odd FS) — fall back to the fixed max.
544
- }
545
- return DISK_CAP_ABSOLUTE_MAX_BYTES;
546
- }
547
-
548
522
  /**
549
523
  * Decode a raw torrent source value into the format expected by WebTorrent.
550
524
  *
@@ -1009,15 +983,9 @@ export class TorrentPool {
1009
983
  */
1010
984
  #edgePrefetches = new Map();
1011
985
 
1012
- /** Global disk cap in bytes (0 = disabled). */
1013
- #maxDiskBytes = 0;
1014
-
1015
986
  /** Memory budget per torrent for resident pieces; undefined = store default. */
1016
987
  #memoryBytes;
1017
988
 
1018
- /** Periodic disk-cap enforcement timer. */
1019
- #diskSweepTimer = null;
1020
-
1021
989
  /** Current client-wide upload limit in bytes/sec (adaptive). -1 = not yet set. */
1022
990
  #uploadLimit = -1;
1023
991
 
@@ -1047,12 +1015,6 @@ export class TorrentPool {
1047
1015
  */
1048
1016
  #swarmTimingByTorrent = new WeakMap();
1049
1017
 
1050
- /**
1051
- * @param {{ maxDiskBytes?: number }} [options]
1052
- * `maxDiskBytes` caps total downloaded torrent data; when omitted a
1053
- * default is computed from free disk (min(10 GB, half free)). Pass 0 to
1054
- * disable the cap.
1055
- */
1056
1018
  /**
1057
1019
  * Sources every file of which this proxy holds whole.
1058
1020
  *
@@ -1168,11 +1130,26 @@ export class TorrentPool {
1168
1130
  }
1169
1131
  }
1170
1132
 
1133
+ /** The chunk store class every torrent is built with. @type {Function} */
1134
+ #pieceStore;
1135
+
1171
1136
  get claimsWithdrawn() {
1172
1137
  return this.#claimsWithdrawn;
1173
1138
  }
1174
1139
 
1175
- constructor({ maxDiskBytes, memoryBytes, dhtBootstrap } = {}) {
1140
+ /**
1141
+ * @param {object} params
1142
+ * @param {number} [params.memoryBytes]
1143
+ * @param {string[]} [params.dhtBootstrap]
1144
+ * @param {Function} params.pieceStore - The chunk store WebTorrent builds per
1145
+ * torrent: the storage component's implementation, handed in where the
1146
+ * thread is assembled.
1147
+ */
1148
+ constructor({ memoryBytes, dhtBootstrap, pieceStore } = {}) {
1149
+ if (typeof pieceStore !== "function") {
1150
+ throw new TypeError("TorrentPool needs the piece store to build torrents with");
1151
+ }
1152
+ this.#pieceStore = pieceStore;
1176
1153
  this.#memoryBytes = Number.isFinite(memoryBytes) && memoryBytes > 0 ? memoryBytes : undefined;
1177
1154
 
1178
1155
  // Sweep orphaned torrent data left by a previous hard kill (no graceful
@@ -1235,16 +1212,6 @@ export class TorrentPool {
1235
1212
  logger.warn(`torrent-pool: client warning: ${formatWarning(warning)}`);
1236
1213
  });
1237
1214
 
1238
- this.#maxDiskBytes = Number.isFinite(maxDiskBytes) && maxDiskBytes >= 0
1239
- ? maxDiskBytes
1240
- : computeDefaultDiskCap(os.tmpdir());
1241
- if (this.#maxDiskBytes > 0) {
1242
- const gigabytes = (this.#maxDiskBytes / (1024 * 1024 * 1024)).toFixed(1);
1243
- logger.info(`torrent-pool: disk cap ${gigabytes} GB (LRU eviction of idle torrents above it)`);
1244
- this.#diskSweepTimer = setInterval(() => this.#enforceDiskCap(), DISK_CAP_SWEEP_INTERVAL_MS);
1245
- this.#diskSweepTimer.unref?.();
1246
- }
1247
-
1248
1215
  // Adaptive upload: start with seeding OFF (nothing is being watched yet),
1249
1216
  // then let the periodic adjuster raise it to the floor while a reader is
1250
1217
  // active and to the boost when download is choke-starved. WebTorrent's
@@ -1321,7 +1288,7 @@ export class TorrentPool {
1321
1288
  * @returns {{ ranges: Array<{ from: number, to: number }>, missing: boolean }}
1322
1289
  */
1323
1290
  #readerDemand(torrent) {
1324
- const store = findSharedStore(torrent);
1291
+ const store = pieceStoreOf(torrent);
1325
1292
  const ranges = typeof store?.protectedRanges === "function" ? store.protectedRanges() : [];
1326
1293
  let missing = false;
1327
1294
  for (const range of ranges) {
@@ -1610,61 +1577,6 @@ export class TorrentPool {
1610
1577
  logger.info(`torrent-pool: upload limit -> ${Math.round(bytesPerSec / 1024)} KB/s (${reason})`);
1611
1578
  }
1612
1579
 
1613
- /**
1614
- * Sum of downloaded bytes across pooled torrents — a cheap proxy for the
1615
- * on-disk footprint (the FS store writes downloaded pieces).
1616
- *
1617
- * @returns {number}
1618
- */
1619
- #currentDiskBytes() {
1620
- let total = 0;
1621
- for (const torrent of this.torrents.values()) {
1622
- total += Math.max(0, torrentDownloadedBytes(torrent));
1623
- }
1624
- return total;
1625
- }
1626
-
1627
- /**
1628
- * Evict whole torrents, least-recently-used first, while the total on-disk
1629
- * footprint exceeds the cap. Only torrents with NO active file reader are
1630
- * evictable — a playing torrent cannot be deleted. Best-effort.
1631
- *
1632
- * @returns {void}
1633
- */
1634
- #enforceDiskCap() {
1635
- if (this.#maxDiskBytes <= 0) {
1636
- return;
1637
- }
1638
- let used = this.#currentDiskBytes();
1639
- if (used <= this.#maxDiskBytes) {
1640
- return;
1641
- }
1642
- // Candidates: pooled torrents nothing is wanted of, the longest unwanted
1643
- // first.
1644
- const candidates = [...this.torrents.values()]
1645
- .filter((torrent) => !isWanted(torrent))
1646
- .sort(
1647
- (earlier, later) =>
1648
- (this.#lastAccess.get(earlier) ?? 0) - (this.#lastAccess.get(later) ?? 0)
1649
- );
1650
-
1651
- for (const torrent of candidates) {
1652
- if (used <= this.#maxDiskBytes) {
1653
- break;
1654
- }
1655
- const freed = Math.max(0, torrentDownloadedBytes(torrent));
1656
- const name = typeof torrent?.name === "string" ? torrent.name : "(unknown)";
1657
- const gigabytes = (this.#maxDiskBytes / (1024 * 1024 * 1024)).toFixed(1);
1658
- logger.info(
1659
- `torrent-pool: disk cap ${gigabytes} GB exceeded — evicting idle torrent "${name}" ` +
1660
- `(~${(freed / (1024 * 1024)).toFixed(0)} MB)`
1661
- );
1662
- this.#cancelIdleRemoval(torrent);
1663
- this.#removeTorrent(torrent, "disk-cap");
1664
- used -= freed;
1665
- }
1666
- }
1667
-
1668
1580
  /**
1669
1581
  * Attach peer-discovery diagnostics to a freshly added torrent: tracker
1670
1582
  * announce results (seeders/leechers per announce) and torrent-level
@@ -1914,7 +1826,7 @@ export class TorrentPool {
1914
1826
  this.#readPositionByTorrent.delete(existing);
1915
1827
  this.client.remove(existing, { destroyStore: true }, () => {
1916
1828
  const addedReplacement = this.client.add(torrentId, {
1917
- store: SharedPieceStore,
1829
+ store: this.#pieceStore,
1918
1830
  storeCacheSlots: 0,
1919
1831
  storeOpts: this.#storeOptions(),
1920
1832
  deselect: true
@@ -1947,7 +1859,7 @@ export class TorrentPool {
1947
1859
  // hands out, holds pieces in shared memory the main thread can read
1948
1860
  // directly, and spills to disk instead of losing them.
1949
1861
  const added = this.client.add(torrentId, {
1950
- store: SharedPieceStore,
1862
+ store: this.#pieceStore,
1951
1863
  storeCacheSlots: 0,
1952
1864
  storeOpts: this.#storeOptions(),
1953
1865
  // Nothing is fetched until somebody says they want it. WebTorrent's own
@@ -2561,7 +2473,7 @@ export class TorrentPool {
2561
2473
  * whole-file selection, a window around the read head, and the reader itself
2562
2474
  * — and they overwrote each other on every request. Every need is now stated
2563
2475
  * in one register (`services/demand/`) and one class turns the register into
2564
- * requests to the swarm (`services/download/SwarmSelection.js`); several
2476
+ * requests to the swarm (`services/torrent/download/SwarmSelection.js`); several
2565
2477
  * readers on one file therefore produce the union of their windows instead of
2566
2478
  * the last caller's opinion.
2567
2479
  *
@@ -2667,11 +2579,6 @@ export class TorrentPool {
2667
2579
  * @returns {Promise<void>}
2668
2580
  */
2669
2581
  async destroyAll() {
2670
- // Stop periodic disk-cap enforcement.
2671
- if (this.#diskSweepTimer) {
2672
- clearInterval(this.#diskSweepTimer);
2673
- this.#diskSweepTimer = null;
2674
- }
2675
2582
  // Stop periodic adaptive-upload adjustment.
2676
2583
  if (this.#uploadAdjustTimer) {
2677
2584
  clearInterval(this.#uploadAdjustTimer);
@@ -19,7 +19,7 @@
19
19
  * this must not be asking the swarm for anything.
20
20
  *
21
21
  * WHY IT IS A READ AND NOT A SELECTION. `file.select()` claims every piece of a
22
- * file at once. `#syncSelections` in `torrent-pool.js` records what that cost
22
+ * file at once. `#syncSelections` in `torrent/torrent-pool.js` records what that cost
23
23
  * when it was done alongside the readers' own moving windows: a claim covering
24
24
  * everything always outranked the window, and a seek to 89.1 % of a 4.7 GB film
25
25
  * waited 93 s while the swarm fetched 2.47 GB in file order. So this walks the
@@ -27,7 +27,7 @@
27
27
  * is reading and gives it back.
28
28
  */
29
29
 
30
- import { logger } from "../../utils/logger.js";
30
+ import { logger } from "../../../utils/logger.js";
31
31
  import { readersAreBlockedOn, stallsSeenOn } from "./piece-reader.js";
32
32
 
33
33
  /**