@torrent-tv/proxy 2.88.2 → 2.88.4

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 (475) 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 +2259 -2246
  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 +23 -4
  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 -29
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/post.js +36 -36
  87. package/routes/api/sources/stats/get.js +132 -132
  88. package/routes/api/sources/warm/post.js +217 -217
  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 +138 -138
  92. package/routes/api/transcode-sessions/post.js +178 -173
  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 +380 -380
  99. package/routes/transcode/audio-file/get.js +64 -64
  100. package/routes/transcode/audio-warm/get.js +95 -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 +725 -725
  106. package/services/encode/CoverageMap.js +428 -428
  107. package/services/encode/CushionReport.js +345 -345
  108. package/services/encode/EncodeAdmission.js +376 -376
  109. package/services/encode/EncodeOrchestrator.js +1217 -1217
  110. package/services/encode/EncodePlan.js +1366 -1366
  111. package/services/encode/EncodeRun.js +813 -813
  112. package/services/encode/EncodeRuns.js +1235 -1235
  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 +783 -758
  118. package/services/encode/OutputTimes.js +579 -579
  119. package/services/encode/QsvEncoder.js +58 -58
  120. package/services/encode/Renditions.js +2282 -2184
  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 +418 -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 +1150 -1141
  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 +349 -254
  158. package/services/encode/quality/output-format.js +319 -315
  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 +754 -754
  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 +456 -456
  166. package/services/encode/segment-formats/index.js +85 -85
  167. package/services/encode/segment-formats/init-compat.js +324 -324
  168. package/services/encode/segment-formats/mp4-boxes.js +21 -6
  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 +298 -245
  180. package/services/media/container/AviContainer.js +266 -266
  181. package/services/media/container/Container.js +609 -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 +1344 -1244
  186. package/services/media/container/ebml-reader.js +159 -159
  187. package/services/media/ffmpeg-banner.js +260 -260
  188. package/services/media/keyframe-probe.js +157 -157
  189. package/services/media/media-info-probe.js +93 -93
  190. package/services/media/playback-planner.js +931 -931
  191. package/services/media/tracks/AudioTrack.js +143 -131
  192. package/services/media/tracks/ContainerTrack.js +75 -75
  193. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  194. package/services/media/tracks/SubtitleTrack.js +38 -38
  195. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  196. package/services/media/tracks/VideoTrack.js +33 -33
  197. package/services/media/tracks/audio-bound.js +233 -0
  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 +1150 -1150
  202. package/services/server/ViewerRequests.js +760 -760
  203. package/services/server/WaitLedger.js +142 -142
  204. package/services/server/controllers/PlaybackController.js +40 -40
  205. package/services/server/controllers/SubtitleController.js +109 -109
  206. package/services/server/http-responses.js +60 -60
  207. package/services/server/playback-readiness.js +13 -8
  208. package/services/server/transcode-session-files.js +325 -325
  209. package/services/server/wire-outputs.js +995 -992
  210. package/services/storage/Diagnostics.js +202 -202
  211. package/services/storage/MachineBudget.js +235 -235
  212. package/services/storage/allowance.js +107 -107
  213. package/services/storage/core-dumps.js +106 -106
  214. package/services/storage/files/CompletedFiles.js +391 -391
  215. package/services/storage/files/piece-from-whole-file.js +118 -118
  216. package/services/storage/free.js +71 -71
  217. package/services/storage/keep.js +48 -48
  218. package/services/storage/machine-memory.js +56 -56
  219. package/services/storage/memory-report.js +575 -575
  220. package/services/storage/piece-store/piece-disk-store.js +539 -539
  221. package/services/storage/piece-store/piece-lru.js +495 -495
  222. package/services/storage/piece-store/shared-piece-store.js +1931 -1931
  223. package/services/storage/returns.js +103 -103
  224. package/services/storage/segment-store/SegmentStore.js +1240 -1240
  225. package/services/storage/wire.js +175 -175
  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/episode-naming.js +182 -182
  235. package/services/torrent/files.js +455 -455
  236. package/services/torrent/naming.js +619 -619
  237. package/services/torrent/piece-store-of.js +29 -29
  238. package/services/torrent/supply-margin.js +262 -262
  239. package/services/torrent/torrent-pool.js +2675 -2675
  240. package/services/torrent/worker/background-fill.js +213 -213
  241. package/services/torrent/worker/channel.js +271 -271
  242. package/services/torrent/worker/client.js +765 -765
  243. package/services/torrent/worker/fastest-wires.js +342 -342
  244. package/services/torrent/worker/handle-state.js +23 -23
  245. package/services/torrent/worker/held-bytes.js +155 -155
  246. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  247. package/services/torrent/worker/piece-reader.js +1078 -1078
  248. package/services/torrent/worker/pool-adapter.js +371 -371
  249. package/services/torrent/worker/protocol.js +184 -184
  250. package/services/torrent/worker/resume-warm.js +107 -107
  251. package/services/torrent/worker/webrtc-shim.js +219 -219
  252. package/services/torrent/worker/worker.js +1129 -1129
  253. package/services/transport/data-channel-handler.js +1369 -1369
  254. package/services/transport/delivery-probe.js +842 -842
  255. package/services/transport/health-collector.js +53 -53
  256. package/services/transport/nat-classifier.js +231 -231
  257. package/services/transport/packet-witness.js +808 -808
  258. package/services/transport/port-mapper.js +235 -235
  259. package/services/transport/registry-api.js +81 -81
  260. package/services/transport/tunnel-client.js +447 -447
  261. package/services/transport/usrsctp-state.js +175 -175
  262. package/services/transport/webrtc-manager.js +484 -484
  263. package/services/viewer/Assignments.js +340 -340
  264. package/services/viewer/PriorityMap.js +390 -390
  265. package/services/viewer/PriorityOrchestrator.js +325 -325
  266. package/services/viewer/Viewer.js +691 -691
  267. package/services/viewer/Viewers.js +368 -368
  268. package/services/viewer/active-output.js +37 -37
  269. package/services/viewer/audio-start.js +46 -46
  270. package/services/viewer/buffer-trend.js +81 -81
  271. package/services/viewer/choices.js +647 -647
  272. package/services/viewer/positions.js +126 -126
  273. package/services/viewer/report-intake.js +46 -46
  274. package/store/source-registry.js +71 -71
  275. package/test/asset-paths.test.js +28 -28
  276. package/test/audio-buffer-continuity.test.js +70 -0
  277. package/test/audio-inventory.test.js +212 -212
  278. package/test/audio-track-bitrate.test.js +40 -40
  279. package/test/auto-quality-step.test.js +707 -707
  280. package/test/available-share.test.js +76 -76
  281. package/test/background-fill.test.js +190 -190
  282. package/test/client-log-file.test.js +110 -110
  283. package/test/completed-files.test.js +115 -115
  284. package/test/concurrent-cost.test.js +148 -148
  285. package/test/container-media-info.test.js +228 -228
  286. package/test/contention.test.js +76 -76
  287. package/test/core-dumps.test.js +57 -57
  288. package/test/coverage-follows-the-disk.test.js +228 -228
  289. package/test/coverage-map.test.js +195 -195
  290. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  291. package/test/cut-times-timeline.test.js +65 -65
  292. package/test/cuts-follow-published-grid.test.js +109 -109
  293. package/test/data-channel-frame.test.js +63 -63
  294. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  295. package/test/dead-torrent-handle.test.js +52 -52
  296. package/test/declared-rates.test.js +116 -116
  297. package/test/declared-tracks.test.js +43 -43
  298. package/test/decode-cost-fit.test.js +202 -202
  299. package/test/decode-cost.test.js +462 -462
  300. package/test/delivery-probe.test.js +393 -393
  301. package/test/demand-register.test.js +206 -206
  302. package/test/dht-bootstrap.test.js +72 -72
  303. package/test/drain-threat.test.js +65 -65
  304. package/test/duplicate-add.test.js +48 -48
  305. package/test/encode-admission.test.js +146 -146
  306. package/test/encode-cost.test.js +283 -283
  307. package/test/encode-exit.test.js +111 -111
  308. package/test/encode-orchestrator.test.js +419 -419
  309. package/test/encode-plan-viewers.test.js +718 -718
  310. package/test/encode-plan.test.js +545 -545
  311. package/test/encode-run-state.test.js +314 -314
  312. package/test/encode-run.test.js +162 -162
  313. package/test/encode-slope.test.js +46 -46
  314. package/test/encoded-output.test.js +39 -39
  315. package/test/encoder-kinds.test.js +122 -122
  316. package/test/encoder-readings.test.js +46 -46
  317. package/test/episode-naming.test.js +131 -131
  318. package/test/every-claimant-is-told-its-share.test.js +152 -152
  319. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  320. package/test/failed-start-breaker.test.js +202 -202
  321. package/test/fastest-wires.test.js +143 -143
  322. package/test/ffmpeg-stream-counts.test.js +78 -78
  323. package/test/file-edges.test.js +191 -191
  324. package/test/fixtures/audio-frame-boundary.json +1 -0
  325. package/test/fmp4-presentation-clock.test.js +147 -147
  326. package/test/fmp4-self-contained.test.js +80 -80
  327. package/test/fmp4-timestamps.test.js +140 -140
  328. package/test/fmp4-track-completeness.test.js +41 -41
  329. package/test/format-keyed-outputs.test.js +78 -78
  330. package/test/health-metrics.test.js +38 -38
  331. package/test/held-request-width.test.js +157 -157
  332. package/test/helpers/encode-run.js +136 -136
  333. package/test/helpers/held-file.js +29 -29
  334. package/test/helpers/manager.js +57 -57
  335. package/test/helpers/output-spec.js +63 -63
  336. package/test/host-load.test.js +68 -68
  337. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  338. package/test/init-compat.test.js +219 -219
  339. package/test/input-lost-quiets-the-plan.test.js +261 -261
  340. package/test/input-unavailable.test.js +48 -48
  341. package/test/installation-id.test.js +39 -39
  342. package/test/keeping-period.test.js +83 -83
  343. package/test/keyframe-index-accuracy.test.js +188 -188
  344. package/test/keyframe-table-budget.test.js +102 -102
  345. package/test/keyframes-belong-to-the-file.test.js +249 -249
  346. package/test/learned-median.test.js +37 -37
  347. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  348. package/test/limit-row-by-area.test.js +60 -60
  349. package/test/link-budget.test.js +112 -111
  350. package/test/link-probe.test.js +56 -56
  351. package/test/local-encoding-data.test.js +273 -273
  352. package/test/logger-repeats.test.js +134 -134
  353. package/test/look-ahead-coverage.test.js +56 -56
  354. package/test/manager-surface.test.js +292 -292
  355. package/test/matroska-cues-track.test.js +192 -192
  356. package/test/memory-budget.test.js +390 -390
  357. package/test/move-cost.test.js +216 -216
  358. package/test/mp4-subtitles.test.js +173 -173
  359. package/test/one-authority.test.js +285 -285
  360. package/test/one-budget.test.js +149 -149
  361. package/test/one-output-one-session.test.js +270 -204
  362. package/test/one-resource-divided.test.js +185 -185
  363. package/test/one-serving-output-per-height.test.js +458 -458
  364. package/test/orchestrator-wired.test.js +206 -206
  365. package/test/output-catalog.test.js +39 -39
  366. package/test/output-shape.test.js +21 -21
  367. package/test/output-spec.test.js +251 -251
  368. package/test/output-speed.test.js +88 -88
  369. package/test/outputs-being-prepared.test.js +40 -40
  370. package/test/packet-witness-ring.test.js +236 -236
  371. package/test/packet-witness.test.js +148 -148
  372. package/test/piece-disk-store.test.js +401 -401
  373. package/test/piece-from-whole-file.test.js +129 -129
  374. package/test/piece-lru.test.js +319 -319
  375. package/test/piece-reader.test.js +162 -162
  376. package/test/piece-selection-offset.test.js +85 -85
  377. package/test/piece-store-eviction.test.js +595 -595
  378. package/test/piece-store-never-refuses.test.js +153 -153
  379. package/test/piece-store-reservations.test.js +403 -403
  380. package/test/piece-store-slow-disk.test.js +159 -159
  381. package/test/piece-tail.test.js +97 -97
  382. package/test/plan-host-timings.test.js +68 -68
  383. package/test/plan-is-steady.test.js +225 -225
  384. package/test/playback-readiness.test.js +453 -453
  385. package/test/playback-start-regressions.test.js +99 -99
  386. package/test/priority-map-download.test.js +174 -174
  387. package/test/priority-map-emptied.test.js +204 -204
  388. package/test/priority-map-per-output.test.js +215 -215
  389. package/test/priority-map.test.js +163 -163
  390. package/test/probe-wedge-certainty.test.js +60 -60
  391. package/test/produced-copy-choice.test.js +213 -213
  392. package/test/published-timeline.test.js +83 -83
  393. package/test/quality-offer.test.js +162 -162
  394. package/test/quality-variants.test.js +1257 -1229
  395. package/test/rate-control-in-the-output.test.js +180 -180
  396. package/test/read-survives-withdrawal.test.js +164 -164
  397. package/test/read-window.test.js +365 -365
  398. package/test/resume-warm.test.js +39 -39
  399. package/test/run-graph-drift.test.js +37 -37
  400. package/test/run-inputs.test.js +80 -80
  401. package/test/run-intervals.test.js +100 -100
  402. package/test/run-position-follows-published-grid.test.js +89 -89
  403. package/test/run-progress.test.js +65 -65
  404. package/test/same-height-switch.test.js +664 -664
  405. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  406. package/test/seek-landing.test.js +109 -109
  407. package/test/seek-target-not-superseded.test.js +195 -195
  408. package/test/segment-closed-on-both-branches.test.js +96 -96
  409. package/test/segment-cut-times.test.js +63 -63
  410. package/test/segment-demand.test.js +80 -80
  411. package/test/segment-hold-supersede.test.js +91 -91
  412. package/test/segment-judged-by-its-own-header.test.js +221 -221
  413. package/test/segment-serve-wiring.test.js +475 -475
  414. package/test/segment-store-eviction.test.js +247 -247
  415. package/test/segment-store.test.js +323 -323
  416. package/test/segment-true-start.test.js +86 -86
  417. package/test/segments-are-shared.test.js +176 -176
  418. package/test/send-chunk-bytes.test.js +86 -86
  419. package/test/serving-existing-output.test.js +105 -102
  420. package/test/serving-output.test.js +102 -102
  421. package/test/shared-piece-store.test.js +310 -310
  422. package/test/sidecar-naming.test.js +142 -142
  423. package/test/soundtrack-rate-bound.test.js +289 -0
  424. package/test/source-file.test.js +133 -133
  425. package/test/source-files-route.test.js +113 -113
  426. package/test/source-registry.test.js +18 -18
  427. package/test/split-a-stretch.test.js +139 -139
  428. package/test/stale-request-after-seek.test.js +158 -158
  429. package/test/startup-readings.test.js +124 -124
  430. package/test/stream-route.test.js +209 -209
  431. package/test/subtitle-cue-framing.test.js +200 -200
  432. package/test/subtitle-cue-source.test.js +102 -102
  433. package/test/subtitle-cue-walk.test.js +370 -370
  434. package/test/subtitle-cursor.test.js +63 -63
  435. package/test/subtitle-defaults.test.js +118 -118
  436. package/test/subtitle-language.test.js +252 -252
  437. package/test/subtitle-track-numbering.test.js +386 -386
  438. package/test/supply-margin.test.js +177 -177
  439. package/test/swarm-follows-readers.test.js +174 -174
  440. package/test/swarm-reach.test.js +130 -130
  441. package/test/swarm-selection.test.js +220 -220
  442. package/test/tail-duplication.test.js +167 -167
  443. package/test/the-channel-has-both-ends.test.js +91 -91
  444. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  445. package/test/thread-pool.test.js +53 -53
  446. package/test/timeline.test.js +84 -84
  447. package/test/torrent-contents.test.js +235 -235
  448. package/test/torrent-cost.test.js +71 -71
  449. package/test/torrent-files.test.js +221 -221
  450. package/test/torrent-naming.test.js +255 -255
  451. package/test/tracks-begin-together.test.js +209 -209
  452. package/test/tunnel-renewal.test.js +167 -167
  453. package/test/two-viewers-one-picture.test.js +504 -407
  454. package/test/upload-hurry.test.js +186 -186
  455. package/test/usrsctp-state.test.js +131 -131
  456. package/test/video-facts.test.js +102 -102
  457. package/test/viewer-assignments.test.js +143 -143
  458. package/test/viewer-outputs.test.js +285 -285
  459. package/test/viewer-presence.test.js +116 -116
  460. package/test/viewer-requests-admission.test.js +89 -89
  461. package/test/viewer-states.test.js +189 -189
  462. package/test/viewer-subtitle-subscription.test.js +96 -96
  463. package/test/viewer.test.js +139 -139
  464. package/test/viewing-generations.test.js +390 -390
  465. package/test/visible-rung.test.js +37 -37
  466. package/test/wait-ledger.test.js +100 -100
  467. package/test/webrtc-shim.test.js +149 -149
  468. package/test/wedge-certainty.test.js +131 -131
  469. package/test/withdraw-piece-claim.test.js +212 -212
  470. package/test/worker-channel.test.js +158 -158
  471. package/utils/client-log-file.js +151 -151
  472. package/utils/logger.js +312 -312
  473. package/utils/parse-range.js +25 -25
  474. package/utils/perf.js +59 -59
  475. package/utils/torrent-source-key.js +38 -38
@@ -1,174 +1,174 @@
1
- /**
2
- * @file A proxy that needs nothing from a swarm is not in that swarm.
3
- *
4
- * WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
5
- * `_addIncomingPeer` checks that the torrent is neither destroyed nor paused
6
- * and registers the peer. A proxy with its port mapped is reachable, so on a
7
- * popular torrent connections arrive and are never turned away — field
8
- * 2026-09-11: 249 connected at the start of one viewing, 596 at the end, 15 862
9
- * more queued, on a file complete for three quarters of an hour.
10
- *
11
- * The rule is not a limit on connections. While anybody is reading, every one is
12
- * worth keeping: the one that has delivered nothing yet may deliver next.
13
- *
14
- * AND IT IS ASKED OF WHAT HAS BEEN STATED, never of a count of readers. The
15
- * first version asked every five seconds whether anybody was READING, and a
16
- * torrent added three seconds earlier answered no — its edges still being read,
17
- * its plan still being built. It left the swarm with 741 connections let go,
18
- * and nothing rejoined it: rejoining waited for a reader, and the reader was
19
- * waiting for the header the swarm had been fetching. Playback did not start at
20
- * all.
21
- *
22
- * The question now is whether anything is WANTED of it, which the priority map,
23
- * the ends of an open file and a stopped read all answer — and which is true
24
- * from the moment a torrent is opened.
25
- */
26
-
27
- import test from "node:test";
28
- import assert from "node:assert/strict";
29
- import {
30
- isWanted,
31
- leaveSwarm,
32
- rejoinSwarm,
33
- stateFileEdges,
34
- swarmDecisionFor
35
- } from "../services/torrent/torrent-pool.js";
36
- import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
37
- import { Urgency } from "../services/torrent/demand/index.js";
38
-
39
- /**
40
- * A torrent that records what was done to it. Only the surface the rule
41
- * touches: `paused` is the library's own flag, and the peers are what the pause
42
- * does not close by itself.
43
- *
44
- * @param {number} peerCount
45
- * @returns {object}
46
- */
47
- function torrentWith(peerCount) {
48
- const peers = new Map();
49
- for (let index = 0; index < peerCount; index += 1) {
50
- peers.set(String(index), {
51
- wire: { downloaded: 0 },
52
- destroy() {
53
- peers.delete(String(index));
54
- }
55
- });
56
- }
57
- return {
58
- infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
59
- files: [{ length: 1024 }, { length: 2048 }],
60
- paused: false,
61
- wires: [...peers.values()].map((peer) => peer.wire),
62
- _peers: peers,
63
- pause() {
64
- this.paused = true;
65
- },
66
- resume() {
67
- this.paused = false;
68
- }
69
- };
70
- }
71
-
72
- test("letting a swarm go pauses the torrent and closes what the pause does not", () => {
73
- const torrent = torrentWith(6);
74
-
75
- const closed = leaveSwarm(torrent);
76
-
77
- assert.equal(torrent.paused, true, "the library's own word for this was not used");
78
- assert.equal(closed, 6);
79
- assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
80
- });
81
-
82
- test("a swarm already let go is not let go twice", () => {
83
- const torrent = torrentWith(4);
84
- leaveSwarm(torrent);
85
- assert.equal(leaveSwarm(torrent), 0);
86
- });
87
-
88
- test("the reader that comes back takes the swarm with it", () => {
89
- const torrent = torrentWith(3);
90
- leaveSwarm(torrent);
91
-
92
- assert.equal(rejoinSwarm(torrent), true);
93
- assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
94
- assert.equal(rejoinSwarm(torrent), false, "a torrent already in its swarm was resumed again");
95
- });
96
-
97
- test("a torrent whose metadata has not arrived is wanted, whatever is stated", () => {
98
- // THE FIELD FAILURE OF 2026-09-11, as a question rather than as a mechanism.
99
- // A torrent being added has no list of files, so nothing can name a byte of
100
- // it — and it is being fetched precisely because somebody asked for it.
101
- const torrent = torrentWith(103);
102
- torrent.files = [];
103
- assert.equal(isWanted(torrent), true);
104
- });
105
-
106
- test("a torrent with files and nothing stated for them is wanted by nobody", () => {
107
- const torrent = torrentWith(4);
108
- try {
109
- assert.equal(isWanted(torrent), false);
110
- } finally {
111
- forgetTorrent(torrent);
112
- }
113
- });
114
-
115
- test("anything stated makes it wanted, and the last withdrawal ends that", () => {
116
- const torrent = torrentWith(4);
117
- try {
118
- // The ends of a file being opened, which is the first thing said about a
119
- // torrent anybody has picked and the reason it stays in its swarm through
120
- // the seconds when nothing else can say anything about it.
121
- stateFileEdges(torrent, 0, Urgency.TAIL);
122
- assert.equal(isWanted(torrent), true);
123
-
124
- const { register } = demandFor(torrent);
125
- register.withdraw("file-edges:0:head");
126
- assert.equal(isWanted(torrent), true, "one end of it is still wanted");
127
- register.withdraw("file-edges:0:tail");
128
- assert.equal(isWanted(torrent), false);
129
- } finally {
130
- forgetTorrent(torrent);
131
- }
132
- });
133
-
134
- test("a torrent that has been destroyed is wanted by nobody", () => {
135
- const torrent = torrentWith(1);
136
- torrent.destroyed = true;
137
- assert.equal(isWanted(torrent), false);
138
- });
139
-
140
- test("a torrent something is wanted of keeps its swarm and is off the clock", () => {
141
- assert.deepEqual(
142
- swarmDecisionFor({ wanted: true, everWanted: true }),
143
- { swarm: "take", onTheClock: false }
144
- );
145
- assert.deepEqual(
146
- swarmDecisionFor({ wanted: true, everWanted: false }),
147
- { swarm: "take", onTheClock: false }
148
- );
149
- });
150
-
151
- test("a swarm is let go on a DEPARTURE, never on a beginning", () => {
152
- // The whole of the 2.83.1 failure in one line: a torrent that has never been
153
- // wanted is one being opened — its metadata has just landed, the file list is
154
- // on its way to the person choosing, and nothing has had a chance to state
155
- // anything about it yet. Taking its swarm away there destroyed 741
156
- // connections it needed a minute later.
157
- assert.deepEqual(
158
- swarmDecisionFor({ wanted: false, everWanted: false }),
159
- { swarm: "leave alone", onTheClock: true }
160
- );
161
- assert.deepEqual(
162
- swarmDecisionFor({ wanted: false, everWanted: true }),
163
- { swarm: "let go", onTheClock: true }
164
- );
165
- });
166
-
167
- test("whatever happens to the swarm, an unwanted torrent is on the idle clock", () => {
168
- // Including the one nobody has ever wanted: a file list fetched and never
169
- // played used to be held for the life of the process, because the only thing
170
- // that started the clock was a reader letting go.
171
- for (const everWanted of [true, false]) {
172
- assert.equal(swarmDecisionFor({ wanted: false, everWanted }).onTheClock, true);
173
- }
174
- });
1
+ /**
2
+ * @file A proxy that needs nothing from a swarm is not in that swarm.
3
+ *
4
+ * WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
5
+ * `_addIncomingPeer` checks that the torrent is neither destroyed nor paused
6
+ * and registers the peer. A proxy with its port mapped is reachable, so on a
7
+ * popular torrent connections arrive and are never turned away — field
8
+ * 2026-09-11: 249 connected at the start of one viewing, 596 at the end, 15 862
9
+ * more queued, on a file complete for three quarters of an hour.
10
+ *
11
+ * The rule is not a limit on connections. While anybody is reading, every one is
12
+ * worth keeping: the one that has delivered nothing yet may deliver next.
13
+ *
14
+ * AND IT IS ASKED OF WHAT HAS BEEN STATED, never of a count of readers. The
15
+ * first version asked every five seconds whether anybody was READING, and a
16
+ * torrent added three seconds earlier answered no — its edges still being read,
17
+ * its plan still being built. It left the swarm with 741 connections let go,
18
+ * and nothing rejoined it: rejoining waited for a reader, and the reader was
19
+ * waiting for the header the swarm had been fetching. Playback did not start at
20
+ * all.
21
+ *
22
+ * The question now is whether anything is WANTED of it, which the priority map,
23
+ * the ends of an open file and a stopped read all answer — and which is true
24
+ * from the moment a torrent is opened.
25
+ */
26
+
27
+ import test from "node:test";
28
+ import assert from "node:assert/strict";
29
+ import {
30
+ isWanted,
31
+ leaveSwarm,
32
+ rejoinSwarm,
33
+ stateFileEdges,
34
+ swarmDecisionFor
35
+ } from "../services/torrent/torrent-pool.js";
36
+ import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
37
+ import { Urgency } from "../services/torrent/demand/index.js";
38
+
39
+ /**
40
+ * A torrent that records what was done to it. Only the surface the rule
41
+ * touches: `paused` is the library's own flag, and the peers are what the pause
42
+ * does not close by itself.
43
+ *
44
+ * @param {number} peerCount
45
+ * @returns {object}
46
+ */
47
+ function torrentWith(peerCount) {
48
+ const peers = new Map();
49
+ for (let index = 0; index < peerCount; index += 1) {
50
+ peers.set(String(index), {
51
+ wire: { downloaded: 0 },
52
+ destroy() {
53
+ peers.delete(String(index));
54
+ }
55
+ });
56
+ }
57
+ return {
58
+ infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
59
+ files: [{ length: 1024 }, { length: 2048 }],
60
+ paused: false,
61
+ wires: [...peers.values()].map((peer) => peer.wire),
62
+ _peers: peers,
63
+ pause() {
64
+ this.paused = true;
65
+ },
66
+ resume() {
67
+ this.paused = false;
68
+ }
69
+ };
70
+ }
71
+
72
+ test("letting a swarm go pauses the torrent and closes what the pause does not", () => {
73
+ const torrent = torrentWith(6);
74
+
75
+ const closed = leaveSwarm(torrent);
76
+
77
+ assert.equal(torrent.paused, true, "the library's own word for this was not used");
78
+ assert.equal(closed, 6);
79
+ assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
80
+ });
81
+
82
+ test("a swarm already let go is not let go twice", () => {
83
+ const torrent = torrentWith(4);
84
+ leaveSwarm(torrent);
85
+ assert.equal(leaveSwarm(torrent), 0);
86
+ });
87
+
88
+ test("the reader that comes back takes the swarm with it", () => {
89
+ const torrent = torrentWith(3);
90
+ leaveSwarm(torrent);
91
+
92
+ assert.equal(rejoinSwarm(torrent), true);
93
+ assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
94
+ assert.equal(rejoinSwarm(torrent), false, "a torrent already in its swarm was resumed again");
95
+ });
96
+
97
+ test("a torrent whose metadata has not arrived is wanted, whatever is stated", () => {
98
+ // THE FIELD FAILURE OF 2026-09-11, as a question rather than as a mechanism.
99
+ // A torrent being added has no list of files, so nothing can name a byte of
100
+ // it — and it is being fetched precisely because somebody asked for it.
101
+ const torrent = torrentWith(103);
102
+ torrent.files = [];
103
+ assert.equal(isWanted(torrent), true);
104
+ });
105
+
106
+ test("a torrent with files and nothing stated for them is wanted by nobody", () => {
107
+ const torrent = torrentWith(4);
108
+ try {
109
+ assert.equal(isWanted(torrent), false);
110
+ } finally {
111
+ forgetTorrent(torrent);
112
+ }
113
+ });
114
+
115
+ test("anything stated makes it wanted, and the last withdrawal ends that", () => {
116
+ const torrent = torrentWith(4);
117
+ try {
118
+ // The ends of a file being opened, which is the first thing said about a
119
+ // torrent anybody has picked and the reason it stays in its swarm through
120
+ // the seconds when nothing else can say anything about it.
121
+ stateFileEdges(torrent, 0, Urgency.TAIL);
122
+ assert.equal(isWanted(torrent), true);
123
+
124
+ const { register } = demandFor(torrent);
125
+ register.withdraw("file-edges:0:head");
126
+ assert.equal(isWanted(torrent), true, "one end of it is still wanted");
127
+ register.withdraw("file-edges:0:tail");
128
+ assert.equal(isWanted(torrent), false);
129
+ } finally {
130
+ forgetTorrent(torrent);
131
+ }
132
+ });
133
+
134
+ test("a torrent that has been destroyed is wanted by nobody", () => {
135
+ const torrent = torrentWith(1);
136
+ torrent.destroyed = true;
137
+ assert.equal(isWanted(torrent), false);
138
+ });
139
+
140
+ test("a torrent something is wanted of keeps its swarm and is off the clock", () => {
141
+ assert.deepEqual(
142
+ swarmDecisionFor({ wanted: true, everWanted: true }),
143
+ { swarm: "take", onTheClock: false }
144
+ );
145
+ assert.deepEqual(
146
+ swarmDecisionFor({ wanted: true, everWanted: false }),
147
+ { swarm: "take", onTheClock: false }
148
+ );
149
+ });
150
+
151
+ test("a swarm is let go on a DEPARTURE, never on a beginning", () => {
152
+ // The whole of the 2.83.1 failure in one line: a torrent that has never been
153
+ // wanted is one being opened — its metadata has just landed, the file list is
154
+ // on its way to the person choosing, and nothing has had a chance to state
155
+ // anything about it yet. Taking its swarm away there destroyed 741
156
+ // connections it needed a minute later.
157
+ assert.deepEqual(
158
+ swarmDecisionFor({ wanted: false, everWanted: false }),
159
+ { swarm: "leave alone", onTheClock: true }
160
+ );
161
+ assert.deepEqual(
162
+ swarmDecisionFor({ wanted: false, everWanted: true }),
163
+ { swarm: "let go", onTheClock: true }
164
+ );
165
+ });
166
+
167
+ test("whatever happens to the swarm, an unwanted torrent is on the idle clock", () => {
168
+ // Including the one nobody has ever wanted: a file list fetched and never
169
+ // played used to be held for the life of the process, because the only thing
170
+ // that started the clock was a reader letting go.
171
+ for (const everWanted of [true, false]) {
172
+ assert.equal(swarmDecisionFor({ wanted: false, everWanted }).onTheClock, true);
173
+ }
174
+ });
@@ -1,130 +1,130 @@
1
- /**
2
- * @file The line describing a swarm answers the question it is asked.
3
- *
4
- * It could not. `routes/api/sources/stats/get.js` printed `peers=N` beside
5
- * `wires=?`, which reads as two quantities of which one is unknown — while
6
- * WebTorrent's `numPeers` IS `wires.length` (`lib/torrent.js:254`, the same in
7
- * 2.8.5 and 3.0.21), so the first was the connection count and the second was a
8
- * field that has never printed anything, because the torrent lives on a worker
9
- * thread where that property does not exist.
10
- *
11
- * What was missing is the other half: how many peers the client HOLDS but is
12
- * not connected to, and what the tracker said the swarm has. Measured
13
- * 2026-08-21 on `JUFD665.mp4`, the tracker answered `seeders=5` at 13:40:30 and
14
- * the first wire arrived at 13:44:47 — 4 min 17 s in which the stats line
15
- * repeated unchanged every two seconds while the answer ("offered, not
16
- * connected") was already in the process.
17
- */
18
-
19
- import assert from "node:assert/strict";
20
- import test from "node:test";
21
-
22
- import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
23
-
24
- test("connected and known are separate numbers", () => {
25
- const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
26
- assert.deepEqual(describeSwarmReach(torrent), {
27
- connectedPeers: 2,
28
- deliveringPeers: 0,
29
- knownPeers: 7,
30
- queuedPeers: 4
31
- });
32
- });
33
-
34
- test("offered but not connected is distinguishable from nobody offered", () => {
35
- // The two shapes the field produced. They need opposite investigations, and
36
- // one line has to tell them apart.
37
- const offeredNotConnected = describeSwarmReach({ wires: [], _peersLength: 5, _numQueued: 5 });
38
- const nobodyOffered = describeSwarmReach({ wires: [], _peersLength: 0, _numQueued: 0 });
39
- assert.equal(offeredNotConnected.connectedPeers, 0);
40
- assert.equal(nobodyOffered.connectedPeers, 0);
41
- assert.notEqual(offeredNotConnected.knownPeers, nobodyOffered.knownPeers);
42
- });
43
-
44
- test("internals that are gone say nothing rather than breaking the poll", () => {
45
- // `_peersLength` and `_numQueued` are not WebTorrent's published interface.
46
- // The browser polls this every two seconds, so a version that drops them must
47
- // cost the field and not the answer.
48
- assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
49
- connectedPeers: 1,
50
- deliveringPeers: 0,
51
- knownPeers: null,
52
- queuedPeers: null
53
- });
54
- assert.deepEqual(describeSwarmReach({}), {
55
- connectedPeers: 0,
56
- deliveringPeers: 0,
57
- knownPeers: null,
58
- queuedPeers: null
59
- });
60
- assert.deepEqual(describeSwarmReach(null), {
61
- connectedPeers: 0,
62
- deliveringPeers: 0,
63
- knownPeers: null,
64
- queuedPeers: null
65
- });
66
- const throwing = {
67
- wires: [],
68
- get _peersLength() {
69
- throw new Error("destroyed");
70
- }
71
- };
72
- assert.deepEqual(describeSwarmReach(throwing), {
73
- connectedPeers: 0,
74
- deliveringPeers: 0,
75
- knownPeers: null,
76
- queuedPeers: null
77
- });
78
- });
79
-
80
- test("the wait for a first peer is a measured quantity", () => {
81
- // The field case: added 13:40:30.357, first wire 13:44:47.123.
82
- assert.equal(secondsToFirstPeer(1000, 258_766), 257.766);
83
- assert.equal(secondsToFirstPeer(1000, 1000), 0);
84
- });
85
-
86
- test("no peer yet is not a duration", () => {
87
- assert.equal(secondsToFirstPeer(1000, null), null);
88
- assert.equal(secondsToFirstPeer(null, 2000), null);
89
- // A clock that went backwards is not a negative wait; it is no reading.
90
- assert.equal(secondsToFirstPeer(2000, 1000), null);
91
- });
92
-
93
- test("the best tracker answer wins, not the most recent", () => {
94
- // A live tracker says 500, a dead one answers 0 two seconds later. Keeping
95
- // the last would print "nobody offered" about a swarm of five hundred, which
96
- // inverts the one distinction this figure is carried for.
97
- const answers = [
98
- { seeders: 500, leechers: 40 },
99
- { seeders: 0, leechers: 0 }
100
- ];
101
- assert.deepEqual(bestAnnounce(answers), { seeders: 500, leechers: 40, trackers: 2 });
102
- });
103
-
104
- test("trackers that answered are counted even when none knew anything", () => {
105
- assert.deepEqual(bestAnnounce([{ seeders: null, leechers: null }]), {
106
- seeders: null,
107
- leechers: null,
108
- trackers: 1
109
- });
110
- // No tracker has answered at all — a different state from "answered, knows
111
- // nobody", and the line says so.
112
- assert.deepEqual(bestAnnounce([]), { seeders: null, leechers: null, trackers: 0 });
113
- assert.deepEqual(bestAnnounce(null), { seeders: null, leechers: null, trackers: 0 });
114
- });
115
-
116
- test("leechers travel with the seeder count they were reported beside", () => {
117
- const answers = [
118
- { seeders: 2, leechers: 99 },
119
- { seeders: 7, leechers: 3 }
120
- ];
121
- assert.equal(bestAnnounce(answers).leechers, 3);
122
- // Including when the winning answer gave no leecher count: carrying the
123
- // previous tracker's figure forward would present two trackers' numbers as
124
- // one reading.
125
- assert.deepEqual(bestAnnounce([{ seeders: 2, leechers: 99 }, { seeders: 7, leechers: null }]), {
126
- seeders: 7,
127
- leechers: null,
128
- trackers: 2
129
- });
130
- });
1
+ /**
2
+ * @file The line describing a swarm answers the question it is asked.
3
+ *
4
+ * It could not. `routes/api/sources/stats/get.js` printed `peers=N` beside
5
+ * `wires=?`, which reads as two quantities of which one is unknown — while
6
+ * WebTorrent's `numPeers` IS `wires.length` (`lib/torrent.js:254`, the same in
7
+ * 2.8.5 and 3.0.21), so the first was the connection count and the second was a
8
+ * field that has never printed anything, because the torrent lives on a worker
9
+ * thread where that property does not exist.
10
+ *
11
+ * What was missing is the other half: how many peers the client HOLDS but is
12
+ * not connected to, and what the tracker said the swarm has. Measured
13
+ * 2026-08-21 on `JUFD665.mp4`, the tracker answered `seeders=5` at 13:40:30 and
14
+ * the first wire arrived at 13:44:47 — 4 min 17 s in which the stats line
15
+ * repeated unchanged every two seconds while the answer ("offered, not
16
+ * connected") was already in the process.
17
+ */
18
+
19
+ import assert from "node:assert/strict";
20
+ import test from "node:test";
21
+
22
+ import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
23
+
24
+ test("connected and known are separate numbers", () => {
25
+ const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
26
+ assert.deepEqual(describeSwarmReach(torrent), {
27
+ connectedPeers: 2,
28
+ deliveringPeers: 0,
29
+ knownPeers: 7,
30
+ queuedPeers: 4
31
+ });
32
+ });
33
+
34
+ test("offered but not connected is distinguishable from nobody offered", () => {
35
+ // The two shapes the field produced. They need opposite investigations, and
36
+ // one line has to tell them apart.
37
+ const offeredNotConnected = describeSwarmReach({ wires: [], _peersLength: 5, _numQueued: 5 });
38
+ const nobodyOffered = describeSwarmReach({ wires: [], _peersLength: 0, _numQueued: 0 });
39
+ assert.equal(offeredNotConnected.connectedPeers, 0);
40
+ assert.equal(nobodyOffered.connectedPeers, 0);
41
+ assert.notEqual(offeredNotConnected.knownPeers, nobodyOffered.knownPeers);
42
+ });
43
+
44
+ test("internals that are gone say nothing rather than breaking the poll", () => {
45
+ // `_peersLength` and `_numQueued` are not WebTorrent's published interface.
46
+ // The browser polls this every two seconds, so a version that drops them must
47
+ // cost the field and not the answer.
48
+ assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
49
+ connectedPeers: 1,
50
+ deliveringPeers: 0,
51
+ knownPeers: null,
52
+ queuedPeers: null
53
+ });
54
+ assert.deepEqual(describeSwarmReach({}), {
55
+ connectedPeers: 0,
56
+ deliveringPeers: 0,
57
+ knownPeers: null,
58
+ queuedPeers: null
59
+ });
60
+ assert.deepEqual(describeSwarmReach(null), {
61
+ connectedPeers: 0,
62
+ deliveringPeers: 0,
63
+ knownPeers: null,
64
+ queuedPeers: null
65
+ });
66
+ const throwing = {
67
+ wires: [],
68
+ get _peersLength() {
69
+ throw new Error("destroyed");
70
+ }
71
+ };
72
+ assert.deepEqual(describeSwarmReach(throwing), {
73
+ connectedPeers: 0,
74
+ deliveringPeers: 0,
75
+ knownPeers: null,
76
+ queuedPeers: null
77
+ });
78
+ });
79
+
80
+ test("the wait for a first peer is a measured quantity", () => {
81
+ // The field case: added 13:40:30.357, first wire 13:44:47.123.
82
+ assert.equal(secondsToFirstPeer(1000, 258_766), 257.766);
83
+ assert.equal(secondsToFirstPeer(1000, 1000), 0);
84
+ });
85
+
86
+ test("no peer yet is not a duration", () => {
87
+ assert.equal(secondsToFirstPeer(1000, null), null);
88
+ assert.equal(secondsToFirstPeer(null, 2000), null);
89
+ // A clock that went backwards is not a negative wait; it is no reading.
90
+ assert.equal(secondsToFirstPeer(2000, 1000), null);
91
+ });
92
+
93
+ test("the best tracker answer wins, not the most recent", () => {
94
+ // A live tracker says 500, a dead one answers 0 two seconds later. Keeping
95
+ // the last would print "nobody offered" about a swarm of five hundred, which
96
+ // inverts the one distinction this figure is carried for.
97
+ const answers = [
98
+ { seeders: 500, leechers: 40 },
99
+ { seeders: 0, leechers: 0 }
100
+ ];
101
+ assert.deepEqual(bestAnnounce(answers), { seeders: 500, leechers: 40, trackers: 2 });
102
+ });
103
+
104
+ test("trackers that answered are counted even when none knew anything", () => {
105
+ assert.deepEqual(bestAnnounce([{ seeders: null, leechers: null }]), {
106
+ seeders: null,
107
+ leechers: null,
108
+ trackers: 1
109
+ });
110
+ // No tracker has answered at all — a different state from "answered, knows
111
+ // nobody", and the line says so.
112
+ assert.deepEqual(bestAnnounce([]), { seeders: null, leechers: null, trackers: 0 });
113
+ assert.deepEqual(bestAnnounce(null), { seeders: null, leechers: null, trackers: 0 });
114
+ });
115
+
116
+ test("leechers travel with the seeder count they were reported beside", () => {
117
+ const answers = [
118
+ { seeders: 2, leechers: 99 },
119
+ { seeders: 7, leechers: 3 }
120
+ ];
121
+ assert.equal(bestAnnounce(answers).leechers, 3);
122
+ // Including when the winning answer gave no leecher count: carrying the
123
+ // previous tracker's figure forward would present two trackers' numbers as
124
+ // one reading.
125
+ assert.deepEqual(bestAnnounce([{ seeders: 2, leechers: 99 }, { seeders: 7, leechers: null }]), {
126
+ seeders: 7,
127
+ leechers: null,
128
+ trackers: 2
129
+ });
130
+ });