@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,250 +1,271 @@
1
- /**
2
- * @file The transport both sides of the torrent worker share.
3
- *
4
- * One place holds the request/reply bookkeeping and the streaming rules, so the
5
- * worker and its main-thread client cannot drift apart on the details that
6
- * matter: which side transfers, which side acknowledges, and when a stream is
7
- * allowed to keep going.
8
- *
9
- * See `protocol.js` for why the design is what it is — every number in it came
10
- * from a measurement, not a preference.
11
- */
12
-
13
- import { Event, STREAM_HIGH_WATER_CHUNKS } from "./protocol.js";
14
-
15
- /**
16
- * Issue request ids that stay unique for the life of a thread.
17
- *
18
- * @returns {() => number}
19
- */
20
- export function createRequestIds() {
21
- let next = 0;
22
- return () => (next += 1);
23
- }
24
-
25
- /**
26
- * Wrap a port's messages into request/reply calls.
27
- *
28
- * Callers get promises; the plumbing of matching replies to requests, and of
29
- * turning a worker-side failure back into a rejection, lives here. `Error`
30
- * objects do not survive a thread boundary, so failures cross as messages and
31
- * are rebuilt into `Error`s on arrival — a caller sees an ordinary rejection.
32
- *
33
- * @param {import("node:worker_threads").MessagePort | import("node:worker_threads").Worker} port
34
- * @returns {{
35
- * call: (command: string, params?: object) => Promise<unknown>,
36
- * handleReply: (message: object) => boolean,
37
- * rejectAll: (reason: Error) => void
38
- * }}
39
- */
40
- export function createCaller(port) {
41
- const nextId = createRequestIds();
42
- const pending = new Map();
43
-
44
- return {
45
- /**
46
- * The one source of request ids on this side of the channel.
47
- *
48
- * Reads used to number themselves from a second counter, which put two
49
- * unrelated sequences in one namespace: a read and a command could both be
50
- * in flight as id 5, and the worker's reply to the read then resolved the
51
- * command — with the read's result, silently, while the command's real
52
- * answer arrived later and was dropped as unknown. Handing out every id
53
- * from here makes that collision impossible rather than unlikely.
54
- */
55
- nextId,
56
-
57
- call(command, params = {}) {
58
- return new Promise((resolve, reject) => {
59
- const id = nextId();
60
- pending.set(id, { resolve, reject });
61
- port.postMessage({ command, id, params });
62
- });
63
- },
64
-
65
- /**
66
- * Feed a message in; returns true when it was a reply this caller owned.
67
- */
68
- handleReply(message) {
69
- const entry = pending.get(message?.id);
70
- if (!entry) {
71
- return false;
72
- }
73
- if (message.type === Event.RESULT) {
74
- pending.delete(message.id);
75
- entry.resolve(message.result);
76
- return true;
77
- }
78
- if (message.type === Event.ERROR) {
79
- pending.delete(message.id);
80
- entry.reject(new Error(message.error ?? "Torrent worker request failed."));
81
- return true;
82
- }
83
- return false;
84
- },
85
-
86
- /**
87
- * Fail everything outstanding — the worker died, or is being shut down.
88
- */
89
- rejectAll(reason) {
90
- for (const [, entry] of pending) {
91
- entry.reject(reason);
92
- }
93
- pending.clear();
94
- }
95
- };
96
- }
97
-
98
- /**
99
- * Receive a chunked body as an ordinary `ReadableStream`.
100
- *
101
- * This is the half that makes the design pay off: chunks arrive as transferred
102
- * buffers (no copying), and are handed on through a standard stream, so callers
103
- * treat it exactly like any other body. Each chunk is acknowledged as it is
104
- * enqueued, which is what lets the worker keep only
105
- * {@link STREAM_HIGH_WATER_CHUNKS} in flight.
106
- *
107
- * Cancelling the stream — a viewer navigating away, a superseded seek — sends
108
- * the cancel command, so the worker stops reading rather than filling a queue
109
- * nobody will drain.
110
- *
111
- * @param {object} params
112
- * @param {import("node:worker_threads").Worker} params.port
113
- * @param {number} params.requestId
114
- * @param {() => void} params.onCancel - Sends CANCEL_READ for this request.
115
- * @returns {{ stream: ReadableStream<Uint8Array>, push: (bytes: Uint8Array) => void, close: () => void, fail: (error: Error) => void }}
116
- */
117
- export function createReceiveStream({ port, requestId, onCancel }) {
118
- let controller = null;
119
- let finished = false;
120
-
121
- const stream = new ReadableStream({
122
- start(streamController) {
123
- controller = streamController;
124
- },
125
- cancel() {
126
- if (!finished) {
127
- finished = true;
128
- onCancel();
129
- }
130
- }
131
- });
132
-
133
- return {
134
- stream,
135
-
136
- push(bytes) {
137
- if (finished || !controller) {
138
- return;
139
- }
140
- controller.enqueue(bytes);
141
- // Acknowledge only once the data is in the stream's own queue, so the
142
- // worker's in-flight count reflects what has actually been taken up.
143
- port.postMessage({ type: Event.CHUNK_ACK, id: requestId });
144
- },
145
-
146
- close() {
147
- if (finished || !controller) {
148
- return;
149
- }
150
- finished = true;
151
- controller.close();
152
- },
153
-
154
- fail(error) {
155
- if (finished || !controller) {
156
- return;
157
- }
158
- finished = true;
159
- controller.error(error);
160
- }
161
- };
162
- }
163
-
164
- /**
165
- * Send a body as chunks, pausing when too many are unacknowledged.
166
- *
167
- * The worker side of the same arrangement. `waitForCapacity` resolves when the
168
- * main thread has taken up enough of what was sent; without it a fast disk
169
- * would outrun the channel and rebuild in the message queue exactly the memory
170
- * the transfers were saving.
171
- *
172
- * @param {object} params
173
- * @param {import("node:worker_threads").MessagePort} params.port
174
- * @param {number} params.requestId
175
- * @returns {{ send: (bytes: Buffer) => Promise<void>, end: () => void, ack: () => void, cancel: () => void, isCancelled: () => boolean }}
176
- */
177
- export function createSendStream({ port, requestId }) {
178
- let inFlight = 0;
179
- let cancelled = false;
180
- let wake = null;
181
-
182
- const waitForCapacity = () => {
183
- if (cancelled || inFlight < STREAM_HIGH_WATER_CHUNKS) {
184
- return Promise.resolve();
185
- }
186
- return new Promise((resolve) => {
187
- wake = resolve;
188
- });
189
- };
190
-
191
- return {
192
- async send(bytes) {
193
- await waitForCapacity();
194
- if (cancelled) {
195
- return;
196
- }
197
- inFlight += 1;
198
- // Copy into memory this transport allocated, then transfer THAT.
199
- //
200
- // Transferring the caller's buffer is faster and was what shipped, but it
201
- // is only correct if the caller owns the memory outright — and no test at
202
- // this boundary can establish that. 2.9.73 tried to decide it by
203
- // inspection (`byteOffset === 0 && byteLength === buffer.byteLength`),
204
- // which answers "does this view cover its region", not "did we allocate
205
- // it". WebTorrent's piece cache returns a buffer covering its whole
206
- // region and keeps using it, so the check passed and the transfer
207
- // detached the cache: every later read failed with a detached
208
- // ArrayBuffer, and because the error never reached the reader it looked
209
- // like an empty file (`Stream ends prematurely at 0`).
210
- //
211
- // The copy costs 3.64 ms per 8 MB on the field host, against 37 ms for a
212
- // structured clone. It disappears entirely for pieces read through the
213
- // shared piece store, which the main thread reads by offset without any
214
- // hand-over at all.
215
- const payload = new Uint8Array(bytes);
216
- port.postMessage(
217
- { type: Event.CHUNK, id: requestId, bytes: payload },
218
- [payload.buffer]
219
- );
220
- },
221
-
222
- end() {
223
- if (!cancelled) {
224
- port.postMessage({ type: Event.READ_END, id: requestId });
225
- }
226
- },
227
-
228
- ack() {
229
- inFlight = Math.max(0, inFlight - 1);
230
- if (wake) {
231
- const resume = wake;
232
- wake = null;
233
- resume();
234
- }
235
- },
236
-
237
- cancel() {
238
- cancelled = true;
239
- if (wake) {
240
- const resume = wake;
241
- wake = null;
242
- resume();
243
- }
244
- },
245
-
246
- isCancelled() {
247
- return cancelled;
248
- }
249
- };
250
- }
1
+ /**
2
+ * @file The transport both sides of the torrent worker share.
3
+ *
4
+ * One place holds the request/reply bookkeeping and the streaming rules, so the
5
+ * worker and its main-thread client cannot drift apart on the details that
6
+ * matter: which side transfers, which side acknowledges, and when a stream is
7
+ * allowed to keep going.
8
+ *
9
+ * See `protocol.js` for why the design is what it is — every number in it came
10
+ * from a measurement, not a preference.
11
+ */
12
+
13
+ import { Event, STREAM_HIGH_WATER_CHUNKS } from "./protocol.js";
14
+
15
+ /**
16
+ * Issue request ids that stay unique for the life of a thread.
17
+ *
18
+ * @returns {() => number}
19
+ */
20
+ export function createRequestIds() {
21
+ let next = 0;
22
+ return () => (next += 1);
23
+ }
24
+
25
+ /**
26
+ * Wrap a port's messages into request/reply calls.
27
+ *
28
+ * Callers get promises; the plumbing of matching replies to requests, and of
29
+ * turning a worker-side failure back into a rejection, lives here. `Error`
30
+ * objects do not survive a thread boundary, so failures cross as messages and
31
+ * are rebuilt into `Error`s on arrival — a caller sees an ordinary rejection.
32
+ *
33
+ * @param {import("node:worker_threads").MessagePort | import("node:worker_threads").Worker} port
34
+ * @returns {{
35
+ * call: (command: string, params?: object) => Promise<unknown>,
36
+ * handleReply: (message: object) => boolean,
37
+ * rejectAll: (reason: Error) => void
38
+ * }}
39
+ */
40
+ export function createCaller(port) {
41
+ const nextId = createRequestIds();
42
+ const pending = new Map();
43
+
44
+ return {
45
+ /**
46
+ * The one source of request ids on this side of the channel.
47
+ *
48
+ * Reads used to number themselves from a second counter, which put two
49
+ * unrelated sequences in one namespace: a read and a command could both be
50
+ * in flight as id 5, and the worker's reply to the read then resolved the
51
+ * command — with the read's result, silently, while the command's real
52
+ * answer arrived later and was dropped as unknown. Handing out every id
53
+ * from here makes that collision impossible rather than unlikely.
54
+ */
55
+ nextId,
56
+
57
+ call(command, params = {}) {
58
+ return new Promise((resolve, reject) => {
59
+ const id = nextId();
60
+ pending.set(id, { resolve, reject });
61
+ port.postMessage({ command, id, params });
62
+ });
63
+ },
64
+
65
+ /**
66
+ * Feed a message in; returns true when it was a reply this caller owned.
67
+ */
68
+ handleReply(message) {
69
+ const entry = pending.get(message?.id);
70
+ if (!entry) {
71
+ return false;
72
+ }
73
+ if (message.type === Event.RESULT) {
74
+ pending.delete(message.id);
75
+ entry.resolve(message.result);
76
+ return true;
77
+ }
78
+ if (message.type === Event.ERROR) {
79
+ pending.delete(message.id);
80
+ entry.reject(new Error(message.error ?? "Torrent worker request failed."));
81
+ return true;
82
+ }
83
+ return false;
84
+ },
85
+
86
+ /**
87
+ * Fail everything outstanding — the worker died, or is being shut down.
88
+ */
89
+ rejectAll(reason) {
90
+ for (const [, entry] of pending) {
91
+ entry.reject(reason);
92
+ }
93
+ pending.clear();
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Receive a chunked body as an ordinary `ReadableStream`.
100
+ *
101
+ * This is the half that makes the design pay off: chunks arrive as transferred
102
+ * buffers (no copying), and are handed on through a standard stream, so callers
103
+ * treat it exactly like any other body. Each chunk is acknowledged as it is
104
+ * enqueued, which is what lets the worker keep only
105
+ * {@link STREAM_HIGH_WATER_CHUNKS} in flight.
106
+ *
107
+ * Cancelling the stream — a viewer navigating away, a superseded seek — sends
108
+ * the cancel command, so the worker stops reading rather than filling a queue
109
+ * nobody will drain.
110
+ *
111
+ * @param {object} params
112
+ * @param {import("node:worker_threads").Worker} params.port
113
+ * @param {number} params.requestId
114
+ * @param {() => void} params.onCancel - Sends CANCEL_READ for this request.
115
+ * @returns {{ stream: ReadableStream<Uint8Array>, push: (bytes: Uint8Array) => void, close: () => void, fail: (error: Error) => void }}
116
+ */
117
+ export function createReceiveStream({ port, requestId, onCancel }) {
118
+ let controller = null;
119
+ let finished = false;
120
+
121
+ const stream = new ReadableStream({
122
+ start(streamController) {
123
+ controller = streamController;
124
+ },
125
+ cancel() {
126
+ if (!finished) {
127
+ finished = true;
128
+ onCancel();
129
+ }
130
+ }
131
+ });
132
+
133
+ return {
134
+ stream,
135
+
136
+ push(bytes) {
137
+ if (finished || !controller) {
138
+ return;
139
+ }
140
+ controller.enqueue(bytes);
141
+ // Acknowledge only once the data is in the stream's own queue, so the
142
+ // worker's in-flight count reflects what has actually been taken up.
143
+ port.postMessage({ type: Event.CHUNK_ACK, id: requestId });
144
+ },
145
+
146
+ close() {
147
+ if (finished || !controller) {
148
+ return;
149
+ }
150
+ finished = true;
151
+ controller.close();
152
+ },
153
+
154
+ fail(error) {
155
+ if (finished || !controller) {
156
+ return;
157
+ }
158
+ finished = true;
159
+ controller.error(error);
160
+ }
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Send a body as chunks, pausing when too many are unacknowledged.
166
+ *
167
+ * The worker side of the same arrangement. `waitForCapacity` resolves when the
168
+ * main thread has taken up enough of what was sent; without it a fast disk
169
+ * would outrun the channel and rebuild in the message queue exactly the memory
170
+ * the transfers were saving.
171
+ *
172
+ * @param {object} params
173
+ * @param {import("node:worker_threads").MessagePort} params.port
174
+ * @param {number} params.requestId
175
+ * @returns {{ send: (bytes: Buffer) => Promise<void>, end: () => void, ack: () => void, cancel: () => void, isCancelled: () => boolean, onCancel: (listener: () => void) => () => void }}
176
+ */
177
+ export function createSendStream({ port, requestId }) {
178
+ let inFlight = 0;
179
+ let cancelled = false;
180
+ let wake = null;
181
+ /** Told the moment the read is cancelled. @type {Set<() => void>} */
182
+ const cancelListeners = new Set();
183
+
184
+ const waitForCapacity = () => {
185
+ if (cancelled || inFlight < STREAM_HIGH_WATER_CHUNKS) {
186
+ return Promise.resolve();
187
+ }
188
+ return new Promise((resolve) => {
189
+ wake = resolve;
190
+ });
191
+ };
192
+
193
+ return {
194
+ async send(bytes) {
195
+ await waitForCapacity();
196
+ if (cancelled) {
197
+ return;
198
+ }
199
+ inFlight += 1;
200
+ // Copy into memory this transport allocated, then transfer THAT.
201
+ //
202
+ // Transferring the caller's buffer is faster and was what shipped, but it
203
+ // is only correct if the caller owns the memory outright — and no test at
204
+ // this boundary can establish that. 2.9.73 tried to decide it by
205
+ // inspection (`byteOffset === 0 && byteLength === buffer.byteLength`),
206
+ // which answers "does this view cover its region", not "did we allocate
207
+ // it". WebTorrent's piece cache returns a buffer covering its whole
208
+ // region and keeps using it, so the check passed and the transfer
209
+ // detached the cache: every later read failed with a detached
210
+ // ArrayBuffer, and because the error never reached the reader it looked
211
+ // like an empty file (`Stream ends prematurely at 0`).
212
+ //
213
+ // The copy costs 3.64 ms per 8 MB on the field host, against 37 ms for a
214
+ // structured clone. It disappears entirely for pieces read through the
215
+ // shared piece store, which the main thread reads by offset without any
216
+ // hand-over at all.
217
+ const payload = new Uint8Array(bytes);
218
+ port.postMessage(
219
+ { type: Event.CHUNK, id: requestId, bytes: payload },
220
+ [payload.buffer]
221
+ );
222
+ },
223
+
224
+ end() {
225
+ if (!cancelled) {
226
+ port.postMessage({ type: Event.READ_END, id: requestId });
227
+ }
228
+ },
229
+
230
+ ack() {
231
+ inFlight = Math.max(0, inFlight - 1);
232
+ if (wake) {
233
+ const resume = wake;
234
+ wake = null;
235
+ resume();
236
+ }
237
+ },
238
+
239
+ cancel() {
240
+ cancelled = true;
241
+ if (wake) {
242
+ const resume = wake;
243
+ wake = null;
244
+ resume();
245
+ }
246
+ for (const listener of [...cancelListeners]) {
247
+ listener();
248
+ }
249
+ cancelListeners.clear();
250
+ },
251
+
252
+ isCancelled() {
253
+ return cancelled;
254
+ },
255
+
256
+ /**
257
+ * Be told when this read is cancelled — at once if it already has been.
258
+ *
259
+ * @param {() => void} listener
260
+ * @returns {() => void} Stop being told.
261
+ */
262
+ onCancel(listener) {
263
+ if (cancelled) {
264
+ listener();
265
+ return () => {};
266
+ }
267
+ cancelListeners.add(listener);
268
+ return () => cancelListeners.delete(listener);
269
+ }
270
+ };
271
+ }