@torrent-tv/proxy 2.87.5 → 2.87.7

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 (463) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +8 -0
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/browser-proxy-contract.md +101 -94
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +1 -1
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/audio-tracks/post.js +29 -0
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/files/get.js +100 -100
  87. package/routes/api/sources/post.js +36 -36
  88. package/routes/api/sources/stats/get.js +132 -132
  89. package/routes/api/subtitles/get.js +230 -230
  90. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  91. package/routes/api/transcode-sessions/net-report/post.js +129 -129
  92. package/routes/api/transcode-sessions/post.js +160 -160
  93. package/routes/api/transcode-sessions/progress/get.js +27 -27
  94. package/routes/api/transcode-sessions/release/post.js +41 -41
  95. package/routes/api/transcode-sessions/seek/post.js +63 -63
  96. package/routes/health/get.js +13 -13
  97. package/routes/healthz/get.js +13 -13
  98. package/routes/stream/get.js +361 -361
  99. package/routes/transcode/audio-file/get.js +64 -64
  100. package/routes/transcode/audio-warm/get.js +84 -84
  101. package/routes/transcode/session-file/get.js +52 -52
  102. package/routes/transcode/variant-file/get.js +95 -95
  103. package/routes/transcode/variant-warm/get.js +84 -84
  104. package/scripts/render-run-graph.js +173 -173
  105. package/server.js +4 -0
  106. package/services/encode/CoverageMap.js +428 -428
  107. package/services/encode/CushionReport.js +345 -345
  108. package/services/encode/EncodeAdmission.js +332 -332
  109. package/services/encode/EncodeOrchestrator.js +1217 -1217
  110. package/services/encode/EncodePlan.js +1366 -1366
  111. package/services/encode/EncodeRun.js +776 -776
  112. package/services/encode/EncodeRuns.js +1229 -1229
  113. package/services/encode/Encoder.js +142 -142
  114. package/services/encode/EncoderSelection.js +38 -38
  115. package/services/encode/LocalObservations.js +359 -359
  116. package/services/encode/NvencEncoder.js +50 -50
  117. package/services/encode/OutputOpening.js +755 -755
  118. package/services/encode/OutputTimes.js +579 -579
  119. package/services/encode/QsvEncoder.js +58 -58
  120. package/services/encode/Renditions.js +2162 -2162
  121. package/services/encode/RunProgress.js +84 -84
  122. package/services/encode/SegmentDemand.js +95 -95
  123. package/services/encode/SoftwareEncoder.js +121 -121
  124. package/services/encode/V4l2m2mEncoder.js +53 -53
  125. package/services/encode/VaapiEncoder.js +66 -66
  126. package/services/encode/args.js +416 -416
  127. package/services/encode/available-share.js +89 -89
  128. package/services/encode/calibration.js +361 -361
  129. package/services/encode/contention.js +128 -128
  130. package/services/encode/decode-cost-fit.js +363 -363
  131. package/services/encode/encode-exit.js +114 -114
  132. package/services/encode/encode-run-state.js +551 -551
  133. package/services/encode/encoder-readings.js +45 -45
  134. package/services/encode/fingerprint.js +322 -322
  135. package/services/encode/host-load.js +174 -174
  136. package/services/encode/hwaccel.js +1894 -1894
  137. package/services/encode/index.js +9 -9
  138. package/services/encode/learned-median.js +90 -90
  139. package/services/encode/output/EncodedOutput.js +57 -57
  140. package/services/encode/output/Output.js +131 -131
  141. package/services/encode/output/OutputCatalog.js +414 -414
  142. package/services/encode/output/OutputSpec.js +390 -390
  143. package/services/encode/output/Timeline.js +308 -308
  144. package/services/encode/output/cut-grid.js +107 -107
  145. package/services/encode/output/index.js +20 -20
  146. package/services/encode/output/ladder.js +32 -32
  147. package/services/encode/output/playlists.js +284 -284
  148. package/services/encode/output/rates.js +125 -125
  149. package/services/encode/output-key-format.js +21 -21
  150. package/services/encode/quality/EncodeCost.js +1083 -1083
  151. package/services/encode/quality/HostLoad.js +525 -525
  152. package/services/encode/quality/HostTimings.js +249 -249
  153. package/services/encode/quality/OutputQualityState.js +28 -28
  154. package/services/encode/quality/QualityController.js +935 -935
  155. package/services/encode/quality/QualityOffer.js +416 -416
  156. package/services/encode/quality/drain-threat.js +47 -47
  157. package/services/encode/quality/link-budget.js +254 -254
  158. package/services/encode/quality/output-format.js +266 -266
  159. package/services/encode/quality/serving-output.js +216 -216
  160. package/services/encode/quality/visible-rung.js +57 -57
  161. package/services/encode/run-budget.js +97 -97
  162. package/services/encode/run-command.js +747 -747
  163. package/services/encode/run-costs.js +155 -155
  164. package/services/encode/run-inputs.js +42 -42
  165. package/services/encode/segment-formats/fmp4.js +437 -437
  166. package/services/encode/segment-formats/index.js +77 -77
  167. package/services/encode/segment-formats/init-compat.js +324 -324
  168. package/services/encode/segment-formats/mp4-boxes.js +311 -311
  169. package/services/encode/segment-formats/mpegts.js +134 -134
  170. package/services/encode/start-stop-cost.js +178 -178
  171. package/services/encode/throughput.js +97 -97
  172. package/services/encode/torrent-cost.js +88 -88
  173. package/services/identity/installation-id.js +38 -38
  174. package/services/media/ContainerOrchestrator.js +149 -149
  175. package/services/media/KeyframeTables.js +316 -316
  176. package/services/media/SourceFile.js +298 -298
  177. package/services/media/SubtitleCues.js +520 -520
  178. package/services/media/SubtitleOrchestrator.js +167 -167
  179. package/services/media/audio-inventory.js +245 -212
  180. package/services/media/container/AviContainer.js +266 -266
  181. package/services/media/container/Container.js +603 -603
  182. package/services/media/container/ContainerFactory.js +56 -56
  183. package/services/media/container/KeyframeTable.js +272 -272
  184. package/services/media/container/MatroskaContainer.js +1494 -1494
  185. package/services/media/container/Mp4Container.js +1244 -1244
  186. package/services/media/container/SubtitleFileContainer.js +322 -322
  187. package/services/media/container/ebml-reader.js +159 -159
  188. package/services/media/ffmpeg-banner.js +260 -260
  189. package/services/media/keyframe-probe.js +157 -157
  190. package/services/media/media-info-probe.js +93 -93
  191. package/services/media/playback-planner.js +131 -59
  192. package/services/media/tracks/AudioTrack.js +131 -131
  193. package/services/media/tracks/ContainerTrack.js +75 -75
  194. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  195. package/services/media/tracks/SubtitleTrack.js +38 -38
  196. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  197. package/services/media/tracks/VideoTrack.js +33 -33
  198. package/services/media/tracks/index.js +15 -15
  199. package/services/media/tracks/language-detect.js +174 -174
  200. package/services/server/OutputLifecycle.js +418 -418
  201. package/services/server/SegmentServing.js +1146 -1146
  202. package/services/server/ViewerRequests.js +522 -522
  203. package/services/server/WaitLedger.js +142 -142
  204. package/services/server/controllers/PlaybackController.js +40 -36
  205. package/services/server/controllers/SubtitleController.js +109 -109
  206. package/services/server/http-responses.js +60 -60
  207. package/services/server/transcode-session-files.js +325 -325
  208. package/services/server/wire-outputs.js +983 -983
  209. package/services/storage/Diagnostics.js +202 -202
  210. package/services/storage/MachineBudget.js +235 -235
  211. package/services/storage/allowance.js +107 -107
  212. package/services/storage/core-dumps.js +106 -106
  213. package/services/storage/files/CompletedFiles.js +391 -391
  214. package/services/storage/files/piece-from-whole-file.js +118 -118
  215. package/services/storage/free.js +71 -71
  216. package/services/storage/keep.js +48 -48
  217. package/services/storage/machine-memory.js +56 -56
  218. package/services/storage/memory-report.js +575 -575
  219. package/services/storage/piece-store/piece-disk-store.js +516 -516
  220. package/services/storage/piece-store/piece-lru.js +495 -495
  221. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  222. package/services/storage/returns.js +103 -103
  223. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  224. package/services/storage/wire.js +175 -175
  225. package/services/torrent/Contents.js +324 -324
  226. package/services/torrent/demand/DemandRegister.js +202 -202
  227. package/services/torrent/demand/Urgency.js +137 -137
  228. package/services/torrent/demand/Window.js +118 -118
  229. package/services/torrent/demand/index.js +8 -8
  230. package/services/torrent/demand/pieces.js +140 -140
  231. package/services/torrent/download/SwarmSelection.js +386 -386
  232. package/services/torrent/download/registry.js +102 -102
  233. package/services/torrent/download/withdraw-claim.js +80 -80
  234. package/services/torrent/files.js +455 -455
  235. package/services/torrent/naming.js +619 -619
  236. package/services/torrent/piece-store-of.js +29 -29
  237. package/services/torrent/supply-margin.js +262 -262
  238. package/services/torrent/torrent-pool.js +2 -2
  239. package/services/torrent/worker/background-fill.js +213 -213
  240. package/services/torrent/worker/channel.js +271 -271
  241. package/services/torrent/worker/fastest-wires.js +342 -342
  242. package/services/torrent/worker/handle-state.js +23 -23
  243. package/services/torrent/worker/held-bytes.js +155 -155
  244. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  245. package/services/torrent/worker/piece-reader.js +1078 -1078
  246. package/services/torrent/worker/pool-adapter.js +1 -1
  247. package/services/torrent/worker/resume-warm.js +107 -107
  248. package/services/torrent/worker/webrtc-shim.js +219 -219
  249. package/services/transport/data-channel-handler.js +1356 -1356
  250. package/services/transport/delivery-probe.js +842 -842
  251. package/services/transport/health-collector.js +53 -53
  252. package/services/transport/nat-classifier.js +231 -231
  253. package/services/transport/packet-witness.js +808 -808
  254. package/services/transport/port-mapper.js +235 -235
  255. package/services/transport/registry-api.js +81 -81
  256. package/services/transport/tunnel-client.js +447 -447
  257. package/services/transport/usrsctp-state.js +175 -175
  258. package/services/transport/webrtc-manager.js +484 -484
  259. package/services/viewer/Assignments.js +340 -340
  260. package/services/viewer/PriorityMap.js +390 -390
  261. package/services/viewer/PriorityOrchestrator.js +325 -325
  262. package/services/viewer/Viewer.js +657 -657
  263. package/services/viewer/Viewers.js +368 -368
  264. package/services/viewer/active-output.js +37 -37
  265. package/services/viewer/audio-start.js +46 -46
  266. package/services/viewer/buffer-trend.js +81 -81
  267. package/services/viewer/choices.js +647 -647
  268. package/services/viewer/positions.js +126 -126
  269. package/services/viewer/report-intake.js +46 -46
  270. package/store/source-registry.js +71 -71
  271. package/test/asset-paths.test.js +28 -28
  272. package/test/audio-inventory.test.js +212 -176
  273. package/test/audio-track-bitrate.test.js +40 -40
  274. package/test/auto-quality-step.test.js +707 -707
  275. package/test/available-share.test.js +76 -76
  276. package/test/background-fill.test.js +190 -190
  277. package/test/client-log-file.test.js +110 -110
  278. package/test/completed-files.test.js +115 -115
  279. package/test/concurrent-cost.test.js +148 -148
  280. package/test/container-media-info.test.js +228 -228
  281. package/test/contention.test.js +76 -76
  282. package/test/core-dumps.test.js +57 -57
  283. package/test/coverage-follows-the-disk.test.js +228 -228
  284. package/test/coverage-map.test.js +195 -195
  285. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  286. package/test/cut-times-timeline.test.js +65 -65
  287. package/test/cuts-follow-published-grid.test.js +109 -109
  288. package/test/data-channel-frame.test.js +63 -63
  289. package/test/dead-torrent-handle.test.js +52 -52
  290. package/test/declared-rates.test.js +116 -116
  291. package/test/declared-tracks.test.js +35 -35
  292. package/test/decode-cost-fit.test.js +202 -202
  293. package/test/decode-cost.test.js +462 -462
  294. package/test/delivery-probe.test.js +393 -393
  295. package/test/demand-register.test.js +206 -206
  296. package/test/dht-bootstrap.test.js +72 -72
  297. package/test/drain-threat.test.js +65 -65
  298. package/test/duplicate-add.test.js +48 -48
  299. package/test/encode-admission.test.js +146 -146
  300. package/test/encode-cost.test.js +283 -283
  301. package/test/encode-exit.test.js +111 -111
  302. package/test/encode-orchestrator.test.js +419 -419
  303. package/test/encode-plan-viewers.test.js +718 -718
  304. package/test/encode-plan.test.js +545 -545
  305. package/test/encode-run-state.test.js +314 -314
  306. package/test/encode-run.test.js +161 -161
  307. package/test/encode-slope.test.js +46 -46
  308. package/test/encoded-output.test.js +39 -39
  309. package/test/encoder-kinds.test.js +122 -122
  310. package/test/encoder-readings.test.js +46 -46
  311. package/test/every-claimant-is-told-its-share.test.js +152 -152
  312. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  313. package/test/failed-start-breaker.test.js +202 -202
  314. package/test/fastest-wires.test.js +143 -143
  315. package/test/ffmpeg-stream-counts.test.js +78 -78
  316. package/test/file-edges.test.js +191 -191
  317. package/test/fmp4-self-contained.test.js +80 -80
  318. package/test/fmp4-timestamps.test.js +140 -140
  319. package/test/fmp4-track-completeness.test.js +41 -41
  320. package/test/format-keyed-outputs.test.js +78 -78
  321. package/test/health-metrics.test.js +38 -38
  322. package/test/held-request-width.test.js +157 -157
  323. package/test/helpers/encode-run.js +136 -136
  324. package/test/helpers/held-file.js +29 -29
  325. package/test/helpers/manager.js +57 -57
  326. package/test/helpers/output-spec.js +63 -63
  327. package/test/host-load.test.js +68 -68
  328. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  329. package/test/init-compat.test.js +219 -219
  330. package/test/input-lost-quiets-the-plan.test.js +261 -261
  331. package/test/input-unavailable.test.js +48 -48
  332. package/test/installation-id.test.js +39 -39
  333. package/test/keeping-period.test.js +83 -83
  334. package/test/keyframe-index-accuracy.test.js +188 -188
  335. package/test/keyframe-table-budget.test.js +102 -102
  336. package/test/keyframes-belong-to-the-file.test.js +249 -249
  337. package/test/learned-median.test.js +37 -37
  338. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  339. package/test/limit-row-by-area.test.js +60 -60
  340. package/test/link-budget.test.js +111 -111
  341. package/test/link-probe.test.js +56 -56
  342. package/test/local-encoding-data.test.js +273 -273
  343. package/test/logger-repeats.test.js +134 -134
  344. package/test/look-ahead-coverage.test.js +56 -56
  345. package/test/manager-surface.test.js +292 -292
  346. package/test/matroska-cues-track.test.js +192 -192
  347. package/test/memory-budget.test.js +390 -390
  348. package/test/move-cost.test.js +216 -216
  349. package/test/mp4-subtitles.test.js +173 -173
  350. package/test/one-authority.test.js +285 -285
  351. package/test/one-budget.test.js +149 -149
  352. package/test/one-output-one-session.test.js +204 -204
  353. package/test/one-resource-divided.test.js +185 -185
  354. package/test/one-serving-output-per-height.test.js +458 -458
  355. package/test/orchestrator-wired.test.js +206 -206
  356. package/test/output-catalog.test.js +39 -39
  357. package/test/output-shape.test.js +21 -21
  358. package/test/output-spec.test.js +251 -251
  359. package/test/output-speed.test.js +88 -88
  360. package/test/outputs-being-prepared.test.js +40 -40
  361. package/test/packet-witness-ring.test.js +236 -236
  362. package/test/packet-witness.test.js +148 -148
  363. package/test/piece-disk-store.test.js +381 -381
  364. package/test/piece-from-whole-file.test.js +129 -129
  365. package/test/piece-lru.test.js +319 -319
  366. package/test/piece-reader.test.js +162 -162
  367. package/test/piece-selection-offset.test.js +85 -85
  368. package/test/piece-store-eviction.test.js +560 -560
  369. package/test/piece-store-never-refuses.test.js +153 -153
  370. package/test/piece-store-reservations.test.js +318 -318
  371. package/test/piece-store-slow-disk.test.js +159 -159
  372. package/test/piece-tail.test.js +97 -97
  373. package/test/plan-host-timings.test.js +68 -68
  374. package/test/plan-is-steady.test.js +225 -225
  375. package/test/priority-map-download.test.js +174 -174
  376. package/test/priority-map-emptied.test.js +204 -204
  377. package/test/priority-map-per-output.test.js +215 -215
  378. package/test/priority-map.test.js +163 -163
  379. package/test/probe-wedge-certainty.test.js +60 -60
  380. package/test/produced-copy-choice.test.js +213 -213
  381. package/test/published-timeline.test.js +83 -83
  382. package/test/quality-offer.test.js +162 -162
  383. package/test/quality-variants.test.js +1229 -1229
  384. package/test/rate-control-in-the-output.test.js +180 -180
  385. package/test/read-survives-withdrawal.test.js +164 -164
  386. package/test/read-window.test.js +365 -365
  387. package/test/resume-warm.test.js +39 -39
  388. package/test/run-graph-drift.test.js +37 -37
  389. package/test/run-inputs.test.js +80 -80
  390. package/test/run-intervals.test.js +100 -100
  391. package/test/run-position-follows-published-grid.test.js +89 -89
  392. package/test/run-progress.test.js +65 -65
  393. package/test/same-height-switch.test.js +664 -664
  394. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  395. package/test/seek-landing.test.js +109 -109
  396. package/test/seek-target-not-superseded.test.js +195 -195
  397. package/test/segment-closed-on-both-branches.test.js +96 -96
  398. package/test/segment-cut-times.test.js +63 -63
  399. package/test/segment-demand.test.js +80 -80
  400. package/test/segment-hold-supersede.test.js +91 -91
  401. package/test/segment-judged-by-its-own-header.test.js +221 -221
  402. package/test/segment-serve-wiring.test.js +475 -475
  403. package/test/segment-store-eviction.test.js +247 -247
  404. package/test/segment-store.test.js +323 -323
  405. package/test/segment-true-start.test.js +86 -86
  406. package/test/segments-are-shared.test.js +176 -176
  407. package/test/send-chunk-bytes.test.js +86 -86
  408. package/test/serving-existing-output.test.js +102 -102
  409. package/test/serving-output.test.js +102 -102
  410. package/test/shared-piece-store.test.js +310 -310
  411. package/test/sidecar-naming.test.js +142 -142
  412. package/test/source-file.test.js +133 -133
  413. package/test/source-files-route.test.js +103 -103
  414. package/test/source-registry.test.js +18 -18
  415. package/test/split-a-stretch.test.js +139 -139
  416. package/test/stale-request-after-seek.test.js +158 -158
  417. package/test/startup-readings.test.js +124 -124
  418. package/test/stream-route.test.js +182 -182
  419. package/test/subtitle-cue-framing.test.js +200 -200
  420. package/test/subtitle-cue-source.test.js +102 -102
  421. package/test/subtitle-cue-walk.test.js +370 -370
  422. package/test/subtitle-cursor.test.js +63 -63
  423. package/test/subtitle-defaults.test.js +118 -118
  424. package/test/subtitle-language.test.js +252 -252
  425. package/test/subtitle-track-numbering.test.js +386 -386
  426. package/test/supply-margin.test.js +177 -177
  427. package/test/swarm-follows-readers.test.js +174 -174
  428. package/test/swarm-reach.test.js +130 -130
  429. package/test/swarm-selection.test.js +220 -220
  430. package/test/tail-duplication.test.js +167 -167
  431. package/test/the-channel-has-both-ends.test.js +91 -91
  432. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  433. package/test/thread-pool.test.js +53 -53
  434. package/test/timeline.test.js +84 -84
  435. package/test/torrent-contents.test.js +235 -235
  436. package/test/torrent-cost.test.js +71 -71
  437. package/test/torrent-files.test.js +221 -221
  438. package/test/torrent-naming.test.js +255 -255
  439. package/test/tracks-begin-together.test.js +209 -209
  440. package/test/tunnel-renewal.test.js +167 -167
  441. package/test/two-viewers-one-picture.test.js +407 -407
  442. package/test/upload-hurry.test.js +186 -186
  443. package/test/usrsctp-state.test.js +131 -131
  444. package/test/video-facts.test.js +102 -102
  445. package/test/viewer-assignments.test.js +143 -143
  446. package/test/viewer-outputs.test.js +285 -285
  447. package/test/viewer-presence.test.js +116 -116
  448. package/test/viewer-requests-admission.test.js +89 -89
  449. package/test/viewer-states.test.js +189 -189
  450. package/test/viewer-subtitle-subscription.test.js +96 -96
  451. package/test/viewer.test.js +139 -139
  452. package/test/viewing-generations.test.js +390 -390
  453. package/test/visible-rung.test.js +37 -37
  454. package/test/wait-ledger.test.js +100 -100
  455. package/test/webrtc-shim.test.js +149 -149
  456. package/test/wedge-certainty.test.js +131 -131
  457. package/test/withdraw-piece-claim.test.js +212 -212
  458. package/test/worker-channel.test.js +158 -158
  459. package/utils/client-log-file.js +151 -151
  460. package/utils/logger.js +312 -312
  461. package/utils/parse-range.js +25 -25
  462. package/utils/perf.js +59 -59
  463. package/{services/torrent → utils}/torrent-source-key.js +38 -38
@@ -1,62 +1,62 @@
1
- # Proposal: Cold-start reduction (proxy side)
2
-
3
- ## Why
4
-
5
- Field recordings (mobile tester, 2026-07-08) show ~90 seconds from picking an
6
- episode to a picture. The proxy owns two chunks of that:
7
-
8
- 1. **A redundant ffmpeg input scan.** The playback planner probes the input
9
- (`probeStreamCodecs`) and CACHES the plan — then `createSession` in
10
- hls-session-manager immediately probes the SAME input again
11
- (`probeInputMediaInfo`) for duration/resolution/fps/startTime/HDR. Each
12
- probe is a full ffmpeg startup + banner scan over the torrent-backed HTTP
13
- stream — seconds on the HA host, on the critical path, twice.
14
- 2. **First-segment piece latency.** The planner prefetches only the file
15
- EDGES (256 KB head + 2 MB tail — what the codec probe needs). The first
16
- segment's encode then needs the first ~10–20 MB of the file BODY, whose
17
- pieces start downloading only when ffmpeg asks for them — while the swarm
18
- sat mostly idle during the plan poll.
19
-
20
- There is also no per-stage timing in the logs, so field regressions in
21
- session startup are invisible.
22
-
23
- ## What Changes
24
-
25
- - **Reuse the planner's probe in session create.** The planner parses and
26
- caches the full media info (duration, width/height, fps, startTime, isHdr)
27
- from the probe it already runs; hls-session-manager consults that cache
28
- and skips its own `probeInputMediaInfo` on a hit (probe stays as the
29
- fallback). One full ffmpeg scan disappears from the critical path.
30
- - **Prefetch the file-body start once the plan is ready.** After a successful
31
- probe, fire-and-forget prefetch of the first 16 MB of the file, so the
32
- session's ffmpeg reads hit already-downloaded data instead of paying piece
33
- latency at encode time.
34
- - **Stage timings in the log.** Session create logs probe ms (and
35
- cache-hit/probed), keyframe-probe ms, and the time from create to the
36
- first servable segment — so cold-start is measurable per stage in the
37
- field, next to the client-side summary line (server `cold-start` change).
38
-
39
- Client-side counterparts (phase instrumentation, earlier prebuffer start)
40
- live in the server repo's `cold-start` change. The two changes are
41
- independent — no wire-format coupling, either releases alone.
42
-
43
- NOT in this change (candidates for later, kept out deliberately):
44
- next-episode speculative prefetch (bandwidth cost needs field data first);
45
- encoder-side first-segment speed-ups (faster warm-up preset would need a
46
- mid-stream switch — parked with transcode-quality 2.2b).
47
-
48
- ## Capabilities
49
-
50
- ### New Capabilities
51
-
52
- - `cold-start`: proxy-side session startup latency behaviour.
53
-
54
- ## Impact
55
-
56
- - `services/playback-planner.js` — parse + cache full media info; expose
57
- `getCachedMediaInfo`; body-start prefetch after a successful probe.
58
- - `services/hls-session-manager.js` — consult the cache in `createSession`;
59
- stage-timing log lines.
60
- - `server.js` — wire `getCachedMediaInfo` into the HlsSessionManager deps.
61
- - Release: proxy `npm run patch` + ha-addon version bump (standard order:
62
- proxy first, then addon).
1
+ # Proposal: Cold-start reduction (proxy side)
2
+
3
+ ## Why
4
+
5
+ Field recordings (mobile tester, 2026-07-08) show ~90 seconds from picking an
6
+ episode to a picture. The proxy owns two chunks of that:
7
+
8
+ 1. **A redundant ffmpeg input scan.** The playback planner probes the input
9
+ (`probeStreamCodecs`) and CACHES the plan — then `createSession` in
10
+ hls-session-manager immediately probes the SAME input again
11
+ (`probeInputMediaInfo`) for duration/resolution/fps/startTime/HDR. Each
12
+ probe is a full ffmpeg startup + banner scan over the torrent-backed HTTP
13
+ stream — seconds on the HA host, on the critical path, twice.
14
+ 2. **First-segment piece latency.** The planner prefetches only the file
15
+ EDGES (256 KB head + 2 MB tail — what the codec probe needs). The first
16
+ segment's encode then needs the first ~10–20 MB of the file BODY, whose
17
+ pieces start downloading only when ffmpeg asks for them — while the swarm
18
+ sat mostly idle during the plan poll.
19
+
20
+ There is also no per-stage timing in the logs, so field regressions in
21
+ session startup are invisible.
22
+
23
+ ## What Changes
24
+
25
+ - **Reuse the planner's probe in session create.** The planner parses and
26
+ caches the full media info (duration, width/height, fps, startTime, isHdr)
27
+ from the probe it already runs; hls-session-manager consults that cache
28
+ and skips its own `probeInputMediaInfo` on a hit (probe stays as the
29
+ fallback). One full ffmpeg scan disappears from the critical path.
30
+ - **Prefetch the file-body start once the plan is ready.** After a successful
31
+ probe, fire-and-forget prefetch of the first 16 MB of the file, so the
32
+ session's ffmpeg reads hit already-downloaded data instead of paying piece
33
+ latency at encode time.
34
+ - **Stage timings in the log.** Session create logs probe ms (and
35
+ cache-hit/probed), keyframe-probe ms, and the time from create to the
36
+ first servable segment — so cold-start is measurable per stage in the
37
+ field, next to the client-side summary line (server `cold-start` change).
38
+
39
+ Client-side counterparts (phase instrumentation, earlier prebuffer start)
40
+ live in the server repo's `cold-start` change. The two changes are
41
+ independent — no wire-format coupling, either releases alone.
42
+
43
+ NOT in this change (candidates for later, kept out deliberately):
44
+ next-episode speculative prefetch (bandwidth cost needs field data first);
45
+ encoder-side first-segment speed-ups (faster warm-up preset would need a
46
+ mid-stream switch — parked with transcode-quality 2.2b).
47
+
48
+ ## Capabilities
49
+
50
+ ### New Capabilities
51
+
52
+ - `cold-start`: proxy-side session startup latency behaviour.
53
+
54
+ ## Impact
55
+
56
+ - `services/playback-planner.js` — parse + cache full media info; expose
57
+ `getCachedMediaInfo`; body-start prefetch after a successful probe.
58
+ - `services/hls-session-manager.js` — consult the cache in `createSession`;
59
+ stage-timing log lines.
60
+ - `server.js` — wire `getCachedMediaInfo` into the HlsSessionManager deps.
61
+ - Release: proxy `npm run patch` + ha-addon version bump (standard order:
62
+ proxy first, then addon).
@@ -1,47 +1,47 @@
1
- # cold-start — delta spec (proxy)
2
-
3
- ## ADDED Requirements
4
-
5
- ### Requirement: Session create reuses the planner's probe
6
-
7
- Creating an HLS transcode session SHALL NOT re-probe an input whose media
8
- info the playback planner already probed and cached for the same source and
9
- file. The cached info is used only when its critical fields (duration,
10
- width, height) are present and valid; otherwise the session SHALL probe as
11
- before. Cache lifetime and invalidation follow the plan cache exactly.
12
-
13
- #### Scenario: Warm plan, immediate session
14
- - **WHEN** the browser requests a transcode session right after receiving a
15
- playback plan for the same file
16
- - **THEN** the session starts without a second ffmpeg input scan and its
17
- playlist/ladder decisions are identical to what the probe would have
18
- produced
19
-
20
- #### Scenario: Cache cannot serve
21
- - **WHEN** no cached media info exists (e.g. proxy restarted between plan
22
- and session) or a critical field is missing
23
- - **THEN** the session probes the input itself, exactly as before this
24
- change
25
-
26
- ### Requirement: The file-body start is warm before the encoder needs it
27
-
28
- Once a playback plan probe succeeds, the proxy SHALL prefetch the beginning
29
- of the file body (bounded, ~16 MB) in the background, without delaying the
30
- plan response and without competing with a still-running header probe.
31
-
32
- #### Scenario: First segment does not wait for pieces
33
- - **WHEN** the viewer confirms playback within the normal flow (seconds
34
- after the plan)
35
- - **THEN** the encoder's initial reads are served from already-downloaded
36
- data and the first segment's production is not blocked on piece download
37
-
38
- ### Requirement: Session startup is measurable per stage
39
-
40
- Session creation SHALL log the media-info acquisition time (and whether it
41
- was cached or probed), the keyframe-probe time when it runs, and — once per
42
- session — the time from session-create entry to the first servable segment.
43
-
44
- #### Scenario: Field regression triage
45
- - **WHEN** a tester reports a slow start
46
- - **THEN** the proxy log shows, for that session, where the time went:
47
- media info, keyframes, or first-segment production
1
+ # cold-start — delta spec (proxy)
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Session create reuses the planner's probe
6
+
7
+ Creating an HLS transcode session SHALL NOT re-probe an input whose media
8
+ info the playback planner already probed and cached for the same source and
9
+ file. The cached info is used only when its critical fields (duration,
10
+ width, height) are present and valid; otherwise the session SHALL probe as
11
+ before. Cache lifetime and invalidation follow the plan cache exactly.
12
+
13
+ #### Scenario: Warm plan, immediate session
14
+ - **WHEN** the browser requests a transcode session right after receiving a
15
+ playback plan for the same file
16
+ - **THEN** the session starts without a second ffmpeg input scan and its
17
+ playlist/ladder decisions are identical to what the probe would have
18
+ produced
19
+
20
+ #### Scenario: Cache cannot serve
21
+ - **WHEN** no cached media info exists (e.g. proxy restarted between plan
22
+ and session) or a critical field is missing
23
+ - **THEN** the session probes the input itself, exactly as before this
24
+ change
25
+
26
+ ### Requirement: The file-body start is warm before the encoder needs it
27
+
28
+ Once a playback plan probe succeeds, the proxy SHALL prefetch the beginning
29
+ of the file body (bounded, ~16 MB) in the background, without delaying the
30
+ plan response and without competing with a still-running header probe.
31
+
32
+ #### Scenario: First segment does not wait for pieces
33
+ - **WHEN** the viewer confirms playback within the normal flow (seconds
34
+ after the plan)
35
+ - **THEN** the encoder's initial reads are served from already-downloaded
36
+ data and the first segment's production is not blocked on piece download
37
+
38
+ ### Requirement: Session startup is measurable per stage
39
+
40
+ Session creation SHALL log the media-info acquisition time (and whether it
41
+ was cached or probed), the keyframe-probe time when it runs, and — once per
42
+ session — the time from session-create entry to the first servable segment.
43
+
44
+ #### Scenario: Field regression triage
45
+ - **WHEN** a tester reports a slow start
46
+ - **THEN** the proxy log shows, for that session, where the time went:
47
+ media info, keyframes, or first-segment production
@@ -1,48 +1,48 @@
1
- # Tasks: Cold-start reduction (proxy side)
2
-
3
- Execute in order; design.md is normative. Read the code regions listed at
4
- the top of design.md first.
5
-
6
- ## 1. Media-info reuse
7
-
8
- - [ ] 1.1 Make the banner-parse helpers of `probeInputMediaInfo` reusable by
9
- the planner (export or shared module — no duplication). `node --check`
10
- both files.
11
- - [ ] 1.2 playback-planner: build + cache `mediaInfo` alongside the plan on
12
- a codecs-detected probe; expose `getCachedMediaInfo({ sourceKey,
13
- fileIndex })`. Pending probes cache nothing.
14
- - [ ] 1.3 server.js: pass `getCachedMediaInfo` into HlsSessionManager;
15
- hls-session-manager: consult it in `createSession` with the
16
- critical-fields gate (duration/width/height), probe as fallback.
17
- - [ ] 1.4 Verify equivalence: for one file, run plan → session with the
18
- cache and (by temporarily disabling the wiring) without it — the
19
- session log line (duration, segments, ladder/rung) must be identical.
20
-
21
- ## 2. Body-start prefetch
22
-
23
- - [ ] 2.1 playback-planner: fire-and-forget 16 MB head prefetch
24
- (`prefetchFileEdges` with `tailBytes: 0`) at the probe-success point;
25
- never on pending polls; never awaited.
26
- - [ ] 2.2 Verify: on a fresh well-seeded magnet, proxy log/order shows the
27
- prefetch starting right after the plan while the session that follows
28
- produces its first segment without a piece-wait stall (compare
29
- first-segment ms with task 3 timings before/after).
30
-
31
- ## 3. Stage timings
32
-
33
- - [ ] 3.1 `createSession`: one `cold-start …: media-info=<ms> (cached|probed)
34
- keyframes=<ms|skipped> create-total=<ms>` line.
35
- - [ ] 3.2 First-segment-ready line (`+<ms>` since create entry), once per
36
- session (timestamp on the session object + boolean guard).
37
-
38
- ## 4. Verification and release
39
-
40
- - [ ] 4.1 Regression: normal HLS transcode start, video-copy start (keyframe
41
- probe still runs), seek-restart, quality switch — behaviour unchanged;
42
- timings visible in the log.
43
- - [ ] 4.2 Before/after numbers on the dev HA proxy for one cold magnet:
44
- media-info ms (probed → cached) and first-segment ms. Record them in
45
- this file.
46
- - [ ] 4.3 CHANGELOG entry (current version + 1 patch); `npm run patch`; bump
47
- ha-addon `config.yaml` + its CHANGELOG; push; update the addon in HA
48
- (proxy FIRST, then addon — release order per root CLAUDE.md).
1
+ # Tasks: Cold-start reduction (proxy side)
2
+
3
+ Execute in order; design.md is normative. Read the code regions listed at
4
+ the top of design.md first.
5
+
6
+ ## 1. Media-info reuse
7
+
8
+ - [ ] 1.1 Make the banner-parse helpers of `probeInputMediaInfo` reusable by
9
+ the planner (export or shared module — no duplication). `node --check`
10
+ both files.
11
+ - [ ] 1.2 playback-planner: build + cache `mediaInfo` alongside the plan on
12
+ a codecs-detected probe; expose `getCachedMediaInfo({ sourceKey,
13
+ fileIndex })`. Pending probes cache nothing.
14
+ - [ ] 1.3 server.js: pass `getCachedMediaInfo` into HlsSessionManager;
15
+ hls-session-manager: consult it in `createSession` with the
16
+ critical-fields gate (duration/width/height), probe as fallback.
17
+ - [ ] 1.4 Verify equivalence: for one file, run plan → session with the
18
+ cache and (by temporarily disabling the wiring) without it — the
19
+ session log line (duration, segments, ladder/rung) must be identical.
20
+
21
+ ## 2. Body-start prefetch
22
+
23
+ - [ ] 2.1 playback-planner: fire-and-forget 16 MB head prefetch
24
+ (`prefetchFileEdges` with `tailBytes: 0`) at the probe-success point;
25
+ never on pending polls; never awaited.
26
+ - [ ] 2.2 Verify: on a fresh well-seeded magnet, proxy log/order shows the
27
+ prefetch starting right after the plan while the session that follows
28
+ produces its first segment without a piece-wait stall (compare
29
+ first-segment ms with task 3 timings before/after).
30
+
31
+ ## 3. Stage timings
32
+
33
+ - [ ] 3.1 `createSession`: one `cold-start …: media-info=<ms> (cached|probed)
34
+ keyframes=<ms|skipped> create-total=<ms>` line.
35
+ - [ ] 3.2 First-segment-ready line (`+<ms>` since create entry), once per
36
+ session (timestamp on the session object + boolean guard).
37
+
38
+ ## 4. Verification and release
39
+
40
+ - [ ] 4.1 Regression: normal HLS transcode start, video-copy start (keyframe
41
+ probe still runs), seek-restart, quality switch — behaviour unchanged;
42
+ timings visible in the log.
43
+ - [ ] 4.2 Before/after numbers on the dev HA proxy for one cold magnet:
44
+ media-info ms (probed → cached) and first-segment ms. Record them in
45
+ this file.
46
+ - [ ] 4.3 CHANGELOG entry (current version + 1 patch); `npm run patch`; bump
47
+ ha-addon `config.yaml` + its CHANGELOG; push; update the addon in HA
48
+ (proxy FIRST, then addon — release order per root CLAUDE.md).
@@ -1,2 +1,2 @@
1
- schema: spec-driven
2
- created: 2026-07-07
1
+ schema: spec-driven
2
+ created: 2026-07-07
@@ -1,37 +1,37 @@
1
- # Proposal: Global disk cap with LRU eviction (Disk hygiene Level 1, final)
2
-
3
- ## Why
4
-
5
- Downloaded torrent data is already removed on a 300 s idle TTL and at
6
- shutdown, and orphans are swept at startup — but under pressure (several
7
- large files opened within the TTL window, or a fast fill) the total can
8
- still grow unbounded and fill a small Home Assistant host's disk
9
- (SD/eMMC on a Yellow/Pi is often 16–32 GB). A full disk can take down Home
10
- Assistant itself. This adds the last missing Level 1 piece: a global cap.
11
-
12
- ## What Changes
13
-
14
- - The pool tracks total downloaded bytes and, when it exceeds a cap, evicts
15
- whole torrents with NO active reader, least-recently-used first, until
16
- back under the cap (checked every 30 s and reused via the existing
17
- remove-with-store path). A torrent that is currently playing is never
18
- evicted — we cannot delete what is in use.
19
- - The cap defaults to `min(10 GB, half of free disk)` (measured via
20
- `statfs` on the store filesystem), and is overridable with
21
- `--max-disk-bytes` (0 disables).
22
-
23
- ## Capabilities
24
-
25
- ### New Capabilities
26
-
27
- - `disk-cap`: bounded total on-disk footprint via LRU eviction.
28
-
29
- ### Modified Capabilities
30
-
31
- <!-- none -->
32
-
33
- ## Impact
34
-
35
- - `services/torrent/torrent-pool.js` (access tracking, cap enforcement),
36
- `bin/cli.js` (`--max-disk-bytes`), `server.js` (option pass-through);
37
- ha-addon bump. Part of the proxy transcode/hygiene batch (2.9.29).
1
+ # Proposal: Global disk cap with LRU eviction (Disk hygiene Level 1, final)
2
+
3
+ ## Why
4
+
5
+ Downloaded torrent data is already removed on a 300 s idle TTL and at
6
+ shutdown, and orphans are swept at startup — but under pressure (several
7
+ large files opened within the TTL window, or a fast fill) the total can
8
+ still grow unbounded and fill a small Home Assistant host's disk
9
+ (SD/eMMC on a Yellow/Pi is often 16–32 GB). A full disk can take down Home
10
+ Assistant itself. This adds the last missing Level 1 piece: a global cap.
11
+
12
+ ## What Changes
13
+
14
+ - The pool tracks total downloaded bytes and, when it exceeds a cap, evicts
15
+ whole torrents with NO active reader, least-recently-used first, until
16
+ back under the cap (checked every 30 s and reused via the existing
17
+ remove-with-store path). A torrent that is currently playing is never
18
+ evicted — we cannot delete what is in use.
19
+ - The cap defaults to `min(10 GB, half of free disk)` (measured via
20
+ `statfs` on the store filesystem), and is overridable with
21
+ `--max-disk-bytes` (0 disables).
22
+
23
+ ## Capabilities
24
+
25
+ ### New Capabilities
26
+
27
+ - `disk-cap`: bounded total on-disk footprint via LRU eviction.
28
+
29
+ ### Modified Capabilities
30
+
31
+ <!-- none -->
32
+
33
+ ## Impact
34
+
35
+ - `services/torrent/torrent-pool.js` (access tracking, cap enforcement),
36
+ `bin/cli.js` (`--max-disk-bytes`), `server.js` (option pass-through);
37
+ ha-addon bump. Part of the proxy transcode/hygiene batch (2.9.29).
@@ -1,25 +1,25 @@
1
- # disk-cap — delta spec
2
-
3
- ## ADDED Requirements
4
-
5
- ### Requirement: Total torrent data is bounded by a disk cap
6
- The pool SHALL keep the total downloaded torrent footprint under a cap. When
7
- the total exceeds the cap it SHALL evict whole torrents that have no active
8
- file reader, least-recently-used first, removing each with its on-disk store,
9
- until back under the cap or no evictable torrent remains. A torrent with an
10
- active reader SHALL NEVER be evicted. The cap SHALL default to the smaller of
11
- 10 GB and half the free disk, and be overridable (0 disables).
12
-
13
- #### Scenario: Idle torrents evicted under pressure
14
- - **WHEN** the total downloaded data exceeds the cap and some torrents have no
15
- active reader
16
- - **THEN** the least-recently-used idle torrents are removed with their stores
17
- until the total is back under the cap
18
-
19
- #### Scenario: Active torrent protected
20
- - **WHEN** the cap is exceeded but the only large torrent is currently playing
21
- - **THEN** it is not evicted (the cap cannot delete in-use data)
22
-
23
- #### Scenario: Cap disabled
24
- - **WHEN** the cap is set to 0
25
- - **THEN** no eviction occurs
1
+ # disk-cap — delta spec
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Total torrent data is bounded by a disk cap
6
+ The pool SHALL keep the total downloaded torrent footprint under a cap. When
7
+ the total exceeds the cap it SHALL evict whole torrents that have no active
8
+ file reader, least-recently-used first, removing each with its on-disk store,
9
+ until back under the cap or no evictable torrent remains. A torrent with an
10
+ active reader SHALL NEVER be evicted. The cap SHALL default to the smaller of
11
+ 10 GB and half the free disk, and be overridable (0 disables).
12
+
13
+ #### Scenario: Idle torrents evicted under pressure
14
+ - **WHEN** the total downloaded data exceeds the cap and some torrents have no
15
+ active reader
16
+ - **THEN** the least-recently-used idle torrents are removed with their stores
17
+ until the total is back under the cap
18
+
19
+ #### Scenario: Active torrent protected
20
+ - **WHEN** the cap is exceeded but the only large torrent is currently playing
21
+ - **THEN** it is not evicted (the cap cannot delete in-use data)
22
+
23
+ #### Scenario: Cap disabled
24
+ - **WHEN** the cap is set to 0
25
+ - **THEN** no eviction occurs
@@ -1,19 +1,19 @@
1
- # Tasks: Global disk cap with LRU eviction
2
-
3
- ## 1. Implementation
4
-
5
- - [x] 1.1 Access tracking (`#lastAccess`) updated on getTorrent (incl. the
6
- duplicate-infoHash path) and acquireFile
7
- - [x] 1.2 Cap computed at construction (`min(10GB, half free)` via statfs) or
8
- taken from the `maxDiskBytes` option; 0 disables
9
- - [x] 1.3 Periodic (30 s) `#enforceDiskCap`: evict zero-reader torrents
10
- LRU-first via the existing remove-with-store path; clear timer on
11
- destroyAll; drop `#lastAccess` on removal
12
- - [x] 1.4 `--max-disk-bytes` CLI flag → server.js → pool
13
- - [x] 1.5 Syntax checks + unit-test the eviction ordering/active-skip
14
-
15
- ## 2. Release
16
-
17
- - [ ] 2.1 Ship in the proxy batch (2.9.29) + ha-addon bump
18
- - [ ] 2.2 Field-check on the host: open several large files, confirm idle
19
- ones are evicted and playback is never interrupted
1
+ # Tasks: Global disk cap with LRU eviction
2
+
3
+ ## 1. Implementation
4
+
5
+ - [x] 1.1 Access tracking (`#lastAccess`) updated on getTorrent (incl. the
6
+ duplicate-infoHash path) and acquireFile
7
+ - [x] 1.2 Cap computed at construction (`min(10GB, half free)` via statfs) or
8
+ taken from the `maxDiskBytes` option; 0 disables
9
+ - [x] 1.3 Periodic (30 s) `#enforceDiskCap`: evict zero-reader torrents
10
+ LRU-first via the existing remove-with-store path; clear timer on
11
+ destroyAll; drop `#lastAccess` on removal
12
+ - [x] 1.4 `--max-disk-bytes` CLI flag → server.js → pool
13
+ - [x] 1.5 Syntax checks + unit-test the eviction ordering/active-skip
14
+
15
+ ## 2. Release
16
+
17
+ - [ ] 2.1 Ship in the proxy batch (2.9.29) + ha-addon bump
18
+ - [ ] 2.2 Field-check on the host: open several large files, confirm idle
19
+ ones are evicted and playback is never interrupted
@@ -1,2 +1,2 @@
1
- schema: spec-driven
2
- created: 2026-07-06
1
+ schema: spec-driven
2
+ created: 2026-07-06
@@ -1,38 +1,38 @@
1
- # Design: Proxy observability
2
-
3
- ## Context
4
-
5
- Diagnosing the 2026-07-06 failures required docker exec into the addon
6
- container (version) and offered no explanation for zero-peer torrents.
7
- See proposal for the three gaps.
8
-
9
- ## Goals / Non-Goals
10
-
11
- **Goals:** remote version visibility; explainable zero-peer torrents; clean
12
- logs.
13
-
14
- **Non-Goals:** peer-origin tags in `[stats]` (tracker/DHT/PEX attribution is
15
- not cheaply available on WebTorrent's public API — dropped from the original
16
- proposal scope); structured/queryable log output.
17
-
18
- ## Decisions
19
-
20
- 1. **Version via createRequire of package.json** (same pattern as the server
21
- repo), passed as a route dep — no version literal to forget on release.
22
- 2. **Announce results from `torrent.discovery.tracker` ("update" event)** —
23
- internal WebTorrent API, therefore guarded with optional chaining and a
24
- fallback log line when absent; a breaking WebTorrent upgrade degrades to
25
- "not logged", never to a crash. Torrent-level `warning` events carry
26
- tracker rejections/errors and are a public API.
27
- 3. **SSDP fix = setMaxListeners(0) on the one emitter** right after the
28
- first successful map() (the UPnP client is created lazily by it).
29
- Alternatives rejected: raising EventEmitter.defaultMaxListeners (global,
30
- hides real leaks elsewhere); one shared NatAPI for both mappers (couples
31
- TCP/UDP lifecycles for no gain).
32
-
33
- ## Risks / Trade-offs
34
-
35
- - [Announce logging depends on WebTorrent internals] → defensive access +
36
- explicit fallback line; covered by the spec's best-effort clause.
37
- - [Announce lines add log volume] → one line per announce interval
38
- (minutes), negligible next to the existing [stats] cadence.
1
+ # Design: Proxy observability
2
+
3
+ ## Context
4
+
5
+ Diagnosing the 2026-07-06 failures required docker exec into the addon
6
+ container (version) and offered no explanation for zero-peer torrents.
7
+ See proposal for the three gaps.
8
+
9
+ ## Goals / Non-Goals
10
+
11
+ **Goals:** remote version visibility; explainable zero-peer torrents; clean
12
+ logs.
13
+
14
+ **Non-Goals:** peer-origin tags in `[stats]` (tracker/DHT/PEX attribution is
15
+ not cheaply available on WebTorrent's public API — dropped from the original
16
+ proposal scope); structured/queryable log output.
17
+
18
+ ## Decisions
19
+
20
+ 1. **Version via createRequire of package.json** (same pattern as the server
21
+ repo), passed as a route dep — no version literal to forget on release.
22
+ 2. **Announce results from `torrent.discovery.tracker` ("update" event)** —
23
+ internal WebTorrent API, therefore guarded with optional chaining and a
24
+ fallback log line when absent; a breaking WebTorrent upgrade degrades to
25
+ "not logged", never to a crash. Torrent-level `warning` events carry
26
+ tracker rejections/errors and are a public API.
27
+ 3. **SSDP fix = setMaxListeners(0) on the one emitter** right after the
28
+ first successful map() (the UPnP client is created lazily by it).
29
+ Alternatives rejected: raising EventEmitter.defaultMaxListeners (global,
30
+ hides real leaks elsewhere); one shared NatAPI for both mappers (couples
31
+ TCP/UDP lifecycles for no gain).
32
+
33
+ ## Risks / Trade-offs
34
+
35
+ - [Announce logging depends on WebTorrent internals] → defensive access +
36
+ explicit fallback line; covered by the spec's best-effort clause.
37
+ - [Announce lines add log volume] → one line per announce interval
38
+ (minutes), negligible next to the existing [stats] cadence.