@torrent-tv/proxy 2.89.17 → 2.89.19

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 (300) hide show
  1. package/README.md +14 -3
  2. package/package.json +10 -2
  3. package/server.js +0 -11
  4. package/utils/client-log-file.js +147 -42
  5. package/.claude/commands/opsx/apply.md +0 -155
  6. package/.claude/commands/opsx/archive.md +0 -160
  7. package/.claude/commands/opsx/explore.md +0 -174
  8. package/.claude/commands/opsx/propose.md +0 -109
  9. package/.claude/commands/opsx/sync.md +0 -143
  10. package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
  11. package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
  12. package/.claude/skills/openspec-explore/SKILL.md +0 -289
  13. package/.claude/skills/openspec-propose/SKILL.md +0 -113
  14. package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
  15. package/.dockerignore +0 -5
  16. package/AGENTS.md +0 -363
  17. package/CHANGELOG.md +0 -2377
  18. package/Dockerfile +0 -30
  19. package/biome.json +0 -412
  20. package/corpus.tmp.json +0 -1
  21. package/docs/browser-proxy-contract.md +0 -155
  22. package/docs/container-architecture.md +0 -273
  23. package/docs/download-architecture.md +0 -261
  24. package/docs/encode-architecture.md +0 -765
  25. package/docs/encode-run-state.md +0 -96
  26. package/docs/logs.md +0 -151
  27. package/docs/playback-start-forecast.md +0 -113
  28. package/docs/socks5-egress-research.md +0 -114
  29. package/docs/storage-architecture.md +0 -273
  30. package/knip.json +0 -14
  31. package/main2.cpuprofile +0 -1
  32. package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
  33. package/openspec/changes/adaptive-bitrate/design.md +0 -92
  34. package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
  35. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
  36. package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
  37. package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
  38. package/openspec/changes/binary-distribution/proposal.md +0 -53
  39. package/openspec/changes/chunked-request-bodies/design.md +0 -81
  40. package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
  41. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
  42. package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
  43. package/openspec/changes/cold-start/design.md +0 -98
  44. package/openspec/changes/cold-start/proposal.md +0 -62
  45. package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
  46. package/openspec/changes/cold-start/tasks.md +0 -48
  47. package/openspec/changes/disk-cap/.openspec.yaml +0 -2
  48. package/openspec/changes/disk-cap/proposal.md +0 -37
  49. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
  50. package/openspec/changes/disk-cap/tasks.md +0 -19
  51. package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
  52. package/openspec/changes/proxy-observability/design.md +0 -38
  53. package/openspec/changes/proxy-observability/proposal.md +0 -49
  54. package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
  55. package/openspec/changes/proxy-observability/tasks.md +0 -21
  56. package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
  57. package/openspec/changes/seek-debounce/design.md +0 -118
  58. package/openspec/changes/seek-debounce/proposal.md +0 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
  60. package/openspec/changes/seek-debounce/tasks.md +0 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
  62. package/openspec/changes/subtitle-language/proposal.md +0 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
  64. package/openspec/changes/subtitle-language/tasks.md +0 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +0 -2
  66. package/openspec/changes/track-selection/design.md +0 -47
  67. package/openspec/changes/track-selection/proposal.md +0 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
  69. package/openspec/changes/track-selection/tasks.md +0 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
  71. package/openspec/changes/transcode-quality/proposal.md +0 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
  73. package/openspec/changes/transcode-quality/tasks.md +0 -63
  74. package/openspec/config.yaml +0 -38
  75. package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
  76. package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
  77. package/research/double-spawn-2026-09-10.md +0 -171
  78. package/research/handover-2026-09-13.md +0 -209
  79. package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
  80. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
  81. package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
  82. package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
  83. package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
  84. package/scripts/build-audio-samples.mjs +0 -31
  85. package/scripts/render-run-graph.js +0 -173
  86. package/test/asset-paths.test.js +0 -28
  87. package/test/audio-buffer-continuity.test.js +0 -77
  88. package/test/audio-calibration.test.js +0 -23
  89. package/test/audio-inventory.test.js +0 -212
  90. package/test/audio-track-bitrate.test.js +0 -40
  91. package/test/audio-warm-recorded.test.js +0 -63
  92. package/test/auto-quality-step.test.js +0 -762
  93. package/test/available-share.test.js +0 -76
  94. package/test/background-fill.test.js +0 -190
  95. package/test/client-log-file.test.js +0 -126
  96. package/test/client-logs-route.test.js +0 -34
  97. package/test/coalesce.test.js +0 -39
  98. package/test/completed-files.test.js +0 -115
  99. package/test/concurrent-cost.test.js +0 -148
  100. package/test/container-media-info.test.js +0 -228
  101. package/test/contention.test.js +0 -76
  102. package/test/core-dumps.test.js +0 -57
  103. package/test/coverage-follows-the-disk.test.js +0 -233
  104. package/test/coverage-map.test.js +0 -195
  105. package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
  106. package/test/cut-times-timeline.test.js +0 -65
  107. package/test/cuts-follow-published-grid.test.js +0 -109
  108. package/test/data-channel-frame.test.js +0 -63
  109. package/test/data-channel-viewer-lifecycle.test.js +0 -193
  110. package/test/dead-torrent-handle.test.js +0 -52
  111. package/test/declared-rates.test.js +0 -116
  112. package/test/declared-tracks.test.js +0 -43
  113. package/test/decode-cost-fit.test.js +0 -202
  114. package/test/decode-cost.test.js +0 -475
  115. package/test/delivery-probe-field.test.js +0 -173
  116. package/test/delivery-probe.test.js +0 -382
  117. package/test/demand-register.test.js +0 -206
  118. package/test/dht-bootstrap.test.js +0 -72
  119. package/test/drain-threat.test.js +0 -65
  120. package/test/duplicate-add.test.js +0 -48
  121. package/test/encode-admission.test.js +0 -170
  122. package/test/encode-cost.test.js +0 -275
  123. package/test/encode-exit.test.js +0 -111
  124. package/test/encode-orchestrator.test.js +0 -426
  125. package/test/encode-plan-viewers.test.js +0 -725
  126. package/test/encode-plan.test.js +0 -545
  127. package/test/encode-run-state.test.js +0 -314
  128. package/test/encode-run.test.js +0 -248
  129. package/test/encode-slope.test.js +0 -46
  130. package/test/encoded-output.test.js +0 -39
  131. package/test/encoder-kinds.test.js +0 -122
  132. package/test/encoder-readings.test.js +0 -68
  133. package/test/episode-naming.test.js +0 -131
  134. package/test/every-claimant-is-told-its-share.test.js +0 -152
  135. package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
  136. package/test/failed-start-breaker.test.js +0 -202
  137. package/test/fastest-wires.test.js +0 -143
  138. package/test/ffmpeg-stream-counts.test.js +0 -101
  139. package/test/file-edges.test.js +0 -191
  140. package/test/files-in-use.test.js +0 -34
  141. package/test/fixtures/audio-frame-boundary.json +0 -1
  142. package/test/fixtures/refused-audio-20261002.json +0 -10
  143. package/test/fmp4-presentation-clock.test.js +0 -148
  144. package/test/fmp4-self-contained.test.js +0 -80
  145. package/test/fmp4-timestamps.test.js +0 -140
  146. package/test/fmp4-track-completeness.test.js +0 -41
  147. package/test/format-keyed-outputs.test.js +0 -78
  148. package/test/health-metrics.test.js +0 -36
  149. package/test/held-read-ownership.test.js +0 -77
  150. package/test/held-request-width.test.js +0 -157
  151. package/test/helpers/encode-run.js +0 -248
  152. package/test/helpers/held-file.js +0 -77
  153. package/test/helpers/manager.js +0 -62
  154. package/test/helpers/matroska-file.js +0 -296
  155. package/test/helpers/output-spec.js +0 -63
  156. package/test/host-load.test.js +0 -68
  157. package/test/index-accuracy-is-about-the-file.test.js +0 -128
  158. package/test/init-compat.test.js +0 -219
  159. package/test/input-lost-quiets-the-plan.test.js +0 -261
  160. package/test/input-unavailable.test.js +0 -48
  161. package/test/installation-id.test.js +0 -39
  162. package/test/keeping-period.test.js +0 -83
  163. package/test/keyframe-index-accuracy.test.js +0 -188
  164. package/test/keyframe-table-budget.test.js +0 -123
  165. package/test/keyframes-belong-to-the-file.test.js +0 -249
  166. package/test/learned-median.test.js +0 -37
  167. package/test/letting-go-asks-who-is-watching.test.js +0 -191
  168. package/test/limit-row-by-area.test.js +0 -60
  169. package/test/link-budget.test.js +0 -111
  170. package/test/link-probe.test.js +0 -56
  171. package/test/local-encoding-data.test.js +0 -273
  172. package/test/logger-repeats.test.js +0 -134
  173. package/test/look-ahead-coverage.test.js +0 -56
  174. package/test/manager-surface.test.js +0 -291
  175. package/test/matroska-blocks.test.js +0 -0
  176. package/test/matroska-cluster-discovery.test.js +0 -323
  177. package/test/matroska-cues-track.test.js +0 -192
  178. package/test/media-time.test.js +0 -62
  179. package/test/memory-budget.test.js +0 -390
  180. package/test/move-cost.test.js +0 -216
  181. package/test/mp4-composition-times.test.js +0 -0
  182. package/test/mp4-subtitles.test.js +0 -173
  183. package/test/one-authority.test.js +0 -285
  184. package/test/one-budget.test.js +0 -149
  185. package/test/one-output-one-session.test.js +0 -273
  186. package/test/one-resource-divided.test.js +0 -185
  187. package/test/one-serving-output-per-height.test.js +0 -458
  188. package/test/orchestrator-wired.test.js +0 -207
  189. package/test/output-catalog.test.js +0 -39
  190. package/test/output-shape.test.js +0 -21
  191. package/test/output-spec.test.js +0 -251
  192. package/test/output-speed.test.js +0 -92
  193. package/test/outputs-being-prepared.test.js +0 -40
  194. package/test/packet-witness-ring.test.js +0 -236
  195. package/test/packet-witness.test.js +0 -148
  196. package/test/piece-completeness.test.js +0 -78
  197. package/test/piece-disk-store.test.js +0 -401
  198. package/test/piece-from-whole-file.test.js +0 -129
  199. package/test/piece-lru.test.js +0 -319
  200. package/test/piece-reader.test.js +0 -162
  201. package/test/piece-selection-offset.test.js +0 -85
  202. package/test/piece-store-eviction.test.js +0 -595
  203. package/test/piece-store-never-refuses.test.js +0 -153
  204. package/test/piece-store-reservations.test.js +0 -403
  205. package/test/piece-store-slow-disk.test.js +0 -159
  206. package/test/piece-tail.test.js +0 -97
  207. package/test/plan-host-timings.test.js +0 -68
  208. package/test/plan-is-steady.test.js +0 -225
  209. package/test/playback-readiness.test.js +0 -518
  210. package/test/playback-start-regressions.test.js +0 -101
  211. package/test/priority-map-download.test.js +0 -174
  212. package/test/priority-map-emptied.test.js +0 -204
  213. package/test/priority-map-per-output.test.js +0 -215
  214. package/test/priority-map.test.js +0 -163
  215. package/test/probe-wedge-certainty.test.js +0 -60
  216. package/test/produced-copy-choice.test.js +0 -213
  217. package/test/published-timeline.test.js +0 -83
  218. package/test/quality-offer.test.js +0 -160
  219. package/test/quality-variants.test.js +0 -1274
  220. package/test/rate-control-in-the-output.test.js +0 -181
  221. package/test/read-survives-withdrawal.test.js +0 -164
  222. package/test/read-window.test.js +0 -365
  223. package/test/reads-not-yet-arrived.test.js +0 -218
  224. package/test/resume-warm.test.js +0 -39
  225. package/test/run-graph-drift.test.js +0 -37
  226. package/test/run-inputs.test.js +0 -80
  227. package/test/run-intervals.test.js +0 -100
  228. package/test/run-position-follows-published-grid.test.js +0 -89
  229. package/test/run-progress.test.js +0 -64
  230. package/test/same-height-switch.test.js +0 -663
  231. package/test/seek-frees-the-viewers-requests.test.js +0 -217
  232. package/test/seek-landing.test.js +0 -109
  233. package/test/seek-target-not-superseded.test.js +0 -195
  234. package/test/segment-closed-on-both-branches.test.js +0 -114
  235. package/test/segment-cut-times.test.js +0 -63
  236. package/test/segment-demand.test.js +0 -80
  237. package/test/segment-hold-supersede.test.js +0 -91
  238. package/test/segment-judged-by-its-own-header.test.js +0 -222
  239. package/test/segment-serve-wiring.test.js +0 -481
  240. package/test/segment-store-eviction.test.js +0 -247
  241. package/test/segment-store.test.js +0 -352
  242. package/test/segment-true-start.test.js +0 -86
  243. package/test/segments-are-shared.test.js +0 -176
  244. package/test/send-chunk-bytes.test.js +0 -86
  245. package/test/serving-existing-output.test.js +0 -111
  246. package/test/serving-output.test.js +0 -102
  247. package/test/shared-piece-store.test.js +0 -310
  248. package/test/sidecar-naming.test.js +0 -142
  249. package/test/soundtrack-rate-bound.test.js +0 -289
  250. package/test/source-file.test.js +0 -133
  251. package/test/source-files-route.test.js +0 -113
  252. package/test/source-registry.test.js +0 -18
  253. package/test/split-a-stretch.test.js +0 -139
  254. package/test/stale-request-after-seek.test.js +0 -158
  255. package/test/startup-readings.test.js +0 -124
  256. package/test/stream-route.test.js +0 -237
  257. package/test/subtitle-cue-framing.test.js +0 -200
  258. package/test/subtitle-cue-source.test.js +0 -102
  259. package/test/subtitle-cue-walk.test.js +0 -385
  260. package/test/subtitle-cursor.test.js +0 -63
  261. package/test/subtitle-defaults.test.js +0 -118
  262. package/test/subtitle-language.test.js +0 -252
  263. package/test/subtitle-metadata.test.js +0 -15
  264. package/test/subtitle-plan-waits.test.js +0 -125
  265. package/test/subtitle-track-numbering.test.js +0 -391
  266. package/test/supply-margin.test.js +0 -190
  267. package/test/swarm-follows-readers.test.js +0 -174
  268. package/test/swarm-reach.test.js +0 -130
  269. package/test/swarm-selection.test.js +0 -220
  270. package/test/tail-duplication.test.js +0 -167
  271. package/test/the-channel-has-both-ends.test.js +0 -91
  272. package/test/the-torrent-thread-serves-bytes.test.js +0 -107
  273. package/test/thread-pool.test.js +0 -53
  274. package/test/timeline.test.js +0 -84
  275. package/test/torrent-contents.test.js +0 -235
  276. package/test/torrent-cost.test.js +0 -71
  277. package/test/torrent-files.test.js +0 -221
  278. package/test/torrent-naming.test.js +0 -255
  279. package/test/tracks-begin-together.test.js +0 -209
  280. package/test/tunnel-renewal.test.js +0 -167
  281. package/test/two-viewers-one-picture.test.js +0 -508
  282. package/test/upload-hurry.test.js +0 -186
  283. package/test/usrsctp-state.test.js +0 -131
  284. package/test/video-facts.test.js +0 -102
  285. package/test/viewer-assignments.test.js +0 -143
  286. package/test/viewer-outputs.test.js +0 -285
  287. package/test/viewer-presence.test.js +0 -116
  288. package/test/viewer-requests-admission.test.js +0 -92
  289. package/test/viewer-starts-on-map.test.js +0 -27
  290. package/test/viewer-states.test.js +0 -192
  291. package/test/viewer-subtitle-subscription.test.js +0 -96
  292. package/test/viewer.test.js +0 -139
  293. package/test/viewing-generations.test.js +0 -390
  294. package/test/visible-rung.test.js +0 -37
  295. package/test/wait-ledger.test.js +0 -100
  296. package/test/webrtc-shim.test.js +0 -149
  297. package/test/wedge-certainty.test.js +0 -131
  298. package/test/whole-file-held-read.test.js +0 -30
  299. package/test/withdraw-piece-claim.test.js +0 -212
  300. package/test/worker-channel.test.js +0 -158
@@ -1,76 +0,0 @@
1
- /**
2
- * @file What the machine can spare for a new encoder — and what must not be
3
- * charged twice.
4
- *
5
- * The numbers are the addon host's own, measured 2026-08-17 while a 240p step
6
- * ran at 1.01-1.12x against a prediction of 1.83x.
7
- */
8
-
9
- import assert from "node:assert/strict";
10
- import test from "node:test";
11
-
12
- import { availableShareFrom, correctForAvailability } from "../services/encode/available-share.js";
13
-
14
- test("only the work nobody has been charged for is subtracted", () => {
15
- // The field reading: the box is saturated, our encoder has more than half of
16
- // it, and the proxy's own work a fifth.
17
- const availability = availableShareFrom({ systemBusy: 0.99, encoderShare: 0.56, proxyShare: 0.2 });
18
-
19
- assert.equal(availability.known, true);
20
- // 0.99 − 0.76 = 0.23 belongs to the kernel, the container and whatever else
21
- // the owner runs. Our own encoders are priced by the concurrency arithmetic
22
- // and the proxy's work per megabyte moved; charging them here as well is the
23
- // double counting that emptied the quality menu in 2.21.0.
24
- assert.ok(Math.abs(availability.unattributed - 0.23) < 1e-9, `${availability.unattributed}`);
25
- assert.ok(Math.abs(availability.share - 0.77) < 1e-9, `${availability.share}`);
26
- });
27
-
28
- test("a quiet machine is not corrected at all", () => {
29
- const availability = availableShareFrom({ systemBusy: 0.05, encoderShare: 0.04, proxyShare: 0.01 });
30
- assert.equal(availability.unattributed, 0);
31
- assert.equal(availability.share, 1);
32
- assert.equal(correctForAvailability(4, availability), 4);
33
- });
34
-
35
- test("a host that cannot say says so, and nothing is corrected", () => {
36
- // Not every host has /proc. An uncorrected figure is honest; a figure
37
- // corrected by an invented share is not.
38
- const availability = availableShareFrom({ systemBusy: null, encoderShare: null, proxyShare: null });
39
- assert.equal(availability.known, false);
40
- assert.equal(availability.share, 1);
41
- assert.equal(correctForAvailability(2.5, availability), 2.5, "an unknown machine leaves the prediction alone");
42
- assert.equal(availableShareFrom().known, false);
43
- });
44
-
45
- test("readings that overlap by a rounding do not produce a negative machine", () => {
46
- // The two samples are taken microseconds apart, so our own share can come out
47
- // fractionally above the system total.
48
- const availability = availableShareFrom({ systemBusy: 0.60, encoderShare: 0.58, proxyShare: 0.05 });
49
- assert.equal(availability.unattributed, 0);
50
- assert.equal(availability.share, 1);
51
- });
52
-
53
- test("a machine given over entirely to other work leaves nothing", () => {
54
- const availability = availableShareFrom({ systemBusy: 1, encoderShare: 0, proxyShare: 0 });
55
- assert.equal(availability.share, 0);
56
- assert.equal(correctForAvailability(4, availability), 0, "a step cannot be offered on a machine with nothing left");
57
- });
58
-
59
- test("the correction is proportional, and only ever downwards", () => {
60
- const availability = availableShareFrom({ systemBusy: 0.99, encoderShare: 0.56, proxyShare: 0.2 });
61
- // The field case: a step predicted at 1.83x on a quiet box.
62
- const corrected = correctForAvailability(1.83, availability);
63
- assert.ok(Math.abs(corrected - 1.4091) < 0.001, `${corrected}`);
64
- // And the honest note this test exists to record: 1.41x is still above the
65
- // 1.01-1.12x that step actually ran at. This correction closes part of the
66
- // gap, not all of it — which is why the per-step field comparison ships with
67
- // it rather than after it.
68
- assert.ok(corrected > 1.12, "the remaining difference is what the field check is for");
69
- });
70
-
71
- test("nothing is corrected without a prediction to correct", () => {
72
- const availability = availableShareFrom({ systemBusy: 0.99, encoderShare: 0.5, proxyShare: 0.2 });
73
- assert.equal(correctForAvailability(0, availability), 0);
74
- assert.equal(correctForAvailability(Number.NaN, availability), Number.NaN);
75
- assert.equal(correctForAvailability(-1, availability), -1);
76
- });
@@ -1,190 +0,0 @@
1
- /**
2
- * Fetching a file nobody is playing yet, without taking anything from the
3
- * viewer.
4
- *
5
- * The ordering these checks pin is the whole point of the thing: what plays now
6
- * comes first, the other soundtracks and subtitle files next, and reading the
7
- * film far ahead last. The middle tier stays below the first by standing aside
8
- * whenever a reader on the torrent is inside a wait.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import test from "node:test";
13
- import { EventEmitter } from "node:events";
14
- import { fillFileInBackground, fillIsRunning } from "../services/torrent/worker/background-fill.js";
15
-
16
- /**
17
- * A file whose reads resolve when the test says so, recording what was asked
18
- * for.
19
- *
20
- * @param {{ length: number, name?: string }} params
21
- */
22
- function fakeFile({ length, name = "dub.mka" }) {
23
- const reads = [];
24
- return {
25
- name,
26
- length,
27
- reads,
28
- createReadStream({ start, end }) {
29
- const stream = new EventEmitter();
30
- stream.destroy = () => {};
31
- reads.push({ start, end });
32
- // Deliver on the next turn, so a test can observe the read in flight.
33
- queueMicrotask(() => {
34
- stream.emit("data", Buffer.alloc(end - start + 1));
35
- stream.emit("end");
36
- });
37
- return stream;
38
- }
39
- };
40
- }
41
-
42
- /**
43
- * @param {() => boolean} until
44
- * @param {number} [limit]
45
- */
46
- async function waitFor(until, limit = 2000) {
47
- const deadline = Date.now() + limit;
48
- while (!until()) {
49
- if (Date.now() > deadline) {
50
- throw new Error("condition was never reached");
51
- }
52
- await new Promise((resolve) => setTimeout(resolve, 5));
53
- }
54
- }
55
-
56
- test("a file is walked whole, a piece at a time", async () => {
57
- const file = fakeFile({ length: 10 });
58
- const torrent = { infoHash: "aaa", pieceLength: 4, files: [file] };
59
-
60
- const started = fillFileInBackground(torrent, 0, "source-a", { chunkBytes: 4 });
61
-
62
- assert.equal(started, true);
63
- await waitFor(() => !fillIsRunning("source-a", 0));
64
- // 0-3, 4-7, 8-9: the last chunk is short because the file ends, and its end
65
- // is the last byte rather than one past it.
66
- assert.deepEqual(file.reads, [
67
- { start: 0, end: 3 },
68
- { start: 4, end: 7 },
69
- { start: 8, end: 9 }
70
- ]);
71
- });
72
-
73
- test("one fill per file, however many times it is asked for", async () => {
74
- const file = fakeFile({ length: 8 });
75
- const torrent = { infoHash: "bbb", pieceLength: 4, files: [file] };
76
-
77
- const first = fillFileInBackground(torrent, 0, "source-b", { chunkBytes: 4 });
78
- const second = fillFileInBackground(torrent, 0, "source-b", { chunkBytes: 4 });
79
-
80
- assert.equal(first, true);
81
- assert.equal(second, false, "the second call does not start a second walk");
82
- await waitFor(() => !fillIsRunning("source-b", 0));
83
- assert.equal(file.reads.length, 2, "the file was walked once");
84
- });
85
-
86
- test("a file that is gone is not read, and does not throw", async () => {
87
- const torrent = { infoHash: "ccc", pieceLength: 4, files: [] };
88
-
89
- const started = fillFileInBackground(torrent, 0, "source-c", { chunkBytes: 4 });
90
-
91
- assert.equal(started, false);
92
- assert.equal(fillIsRunning("source-c", 0), false);
93
- });
94
-
95
- test("a read that returns nothing stops the walk instead of spinning", async () => {
96
- const file = {
97
- name: "dub.mka",
98
- length: 100,
99
- reads: [],
100
- createReadStream({ start, end }) {
101
- const stream = new EventEmitter();
102
- stream.destroy = () => {};
103
- this.reads.push({ start, end });
104
- queueMicrotask(() => stream.emit("error", new Error("gone")));
105
- return stream;
106
- }
107
- };
108
- const torrent = { infoHash: "ddd", pieceLength: 10, files: [file] };
109
-
110
- fillFileInBackground(torrent, 0, "source-d", { chunkBytes: 10 });
111
-
112
- await waitFor(() => !fillIsRunning("source-d", 0));
113
- assert.equal(file.reads.length, 1, "it gave up after the first failed read");
114
- });
115
-
116
- test("it stands aside while the viewer's own reading is blocked", async () => {
117
- const file = fakeFile({ length: 8 });
118
- const torrent = { infoHash: "eee", pieceLength: 4, files: [file] };
119
- let blocked = true;
120
-
121
- fillFileInBackground(torrent, 0, "source-e", { chunkBytes: 4, isBlocked: () => blocked });
122
-
123
- // The picture is starving, so nothing is asked of the swarm on the
124
- // soundtrack's behalf — however long that lasts.
125
- await new Promise((resolve) => setTimeout(resolve, 120));
126
- assert.deepEqual(file.reads, [], "not one read while a reader is inside a wait");
127
-
128
- blocked = false;
129
- await waitFor(() => !fillIsRunning("source-e", 0));
130
- assert.equal(file.reads.length, 2, "and it resumes once the room is there");
131
- });
132
-
133
- test("the gate is re-asked before every chunk, not once at the start", async () => {
134
- const file = fakeFile({ length: 12 });
135
- const torrent = { infoHash: "fff", pieceLength: 4, files: [file] };
136
- // Starve the viewer again the moment the first chunk has been read, and stay
137
- // that way until the test lifts it.
138
- let starvedAfterFirstChunk = false;
139
- let lifted = false;
140
- const isBlocked = () => {
141
- if (file.reads.length >= 1 && !lifted) {
142
- starvedAfterFirstChunk = true;
143
- }
144
- return starvedAfterFirstChunk && !lifted;
145
- };
146
-
147
- fillFileInBackground(torrent, 0, "source-f", { chunkBytes: 4, isBlocked });
148
-
149
- await waitFor(() => file.reads.length === 1);
150
- await new Promise((resolve) => setTimeout(resolve, 120));
151
- assert.equal(file.reads.length, 1, "a torrent healthy a moment ago is not evidence about now");
152
-
153
- lifted = true;
154
- await waitFor(() => !fillIsRunning("source-f", 0));
155
- assert.equal(file.reads.length, 3);
156
- });
157
-
158
- test("a stall during a chunk makes the next one wait, not just a stall in progress", async () => {
159
- // The case this exists for: a swarm delivering exactly what the film needs.
160
- // Nothing is blocked at the moment the gate is asked, but the viewer stalled
161
- // while the last chunk was in flight — which is the proof there was no room.
162
- const file = fakeFile({ length: 12 });
163
- const torrent = { infoHash: "ggg", pieceLength: 4, files: [file] };
164
- let stalls = 0;
165
- let starving = true;
166
- // A stall happens DURING every chunk, exactly as it does on a swarm with no
167
- // surplus — and nothing is blocked at the moment the gate is asked.
168
- const originalRead = file.createReadStream.bind(file);
169
- file.createReadStream = (range) => {
170
- if (starving) {
171
- stalls += 1;
172
- }
173
- return originalRead(range);
174
- };
175
-
176
- fillFileInBackground(torrent, 0, "source-g", {
177
- chunkBytes: 4,
178
- isBlocked: () => false,
179
- stallsSeen: () => stalls
180
- });
181
-
182
- await waitFor(() => file.reads.length === 1);
183
- await new Promise((resolve) => setTimeout(resolve, 300));
184
- assert.equal(file.reads.length, 1, "a stall during the chunk holds the next one back");
185
-
186
- // The swarm settles: no further stalls, so it may go on.
187
- starving = false;
188
- await waitFor(() => !fillIsRunning("source-g", 0), 8000);
189
- assert.equal(file.reads.length, 3);
190
- });
@@ -1,126 +0,0 @@
1
- /**
2
- * @file The browser's log, written beside the proxy's.
3
- *
4
- * What these hold is the naming, because the name is the whole point: the two
5
- * halves of a session have to join without guessing, and a torrent name is
6
- * arbitrary bytes chosen by a stranger that is about to become a path.
7
- */
8
-
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
12
- import { tmpdir } from "node:os";
13
- import { join } from "node:path";
14
-
15
- import { createClientLogConsole, createClientLogFiles } from "../utils/client-log-file.js";
16
-
17
- /** A directory with a proxy log in it, which is what the sink is given. */
18
- function aDirectory() {
19
- const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
20
- return { dir, proxyLog: join(dir, "proxy.log") };
21
- }
22
-
23
- const aSession = (over = {}) => ({
24
- sessionId: "abcd1234",
25
- startedAt: "2026-09-13T16:05:24.123Z",
26
- torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
27
- infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
28
- ...over
29
- });
30
-
31
- test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
32
- const { dir, proxyLog } = aDirectory();
33
- const logs = createClientLogFiles(proxyLog);
34
- logs.write(aSession(), ["first line"]);
35
- await logs.close();
36
-
37
- const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
38
- assert.ok(name, "a client log file was written");
39
- // The stamp is UTC and sorts; the session id joins it to the proxy's lines;
40
- // the film says which one it is without opening the file.
41
- assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
42
- assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
43
- });
44
-
45
- test("lines from one session go on appending to the one file", async () => {
46
- const { dir, proxyLog } = aDirectory();
47
- const logs = createClientLogFiles(proxyLog);
48
- logs.write(aSession(), ["one", "two"]);
49
- logs.write(aSession(), ["three"]);
50
- await logs.close();
51
-
52
- const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
53
- assert.equal(names.length, 1);
54
- assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
55
- });
56
-
57
- test("a session that has not chosen a film yet still gets its own file", async () => {
58
- // The page opening, a proxy being chosen, a connection failing — all before
59
- // any torrent exists, and all worth keeping.
60
- const { dir, proxyLog } = aDirectory();
61
- const logs = createClientLogFiles(proxyLog);
62
- logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
63
- await logs.close();
64
-
65
- const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
66
- assert.match(name, /-no-torrent-yet\.log$/);
67
- });
68
-
69
- test("the file keeps its name when the film arrives mid-session", async () => {
70
- // Keyed by the session, not by the name: the earlier lines belong to the same
71
- // viewing and must not start a second file.
72
- const { dir, proxyLog } = aDirectory();
73
- const logs = createClientLogFiles(proxyLog);
74
- logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
75
- logs.write(aSession(), ["after"]);
76
- await logs.close();
77
-
78
- const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
79
- assert.equal(names.length, 1);
80
- assert.match(names[0], /-no-torrent-yet\.log$/);
81
- assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
82
- });
83
-
84
- test("a hostile torrent name cannot escape the directory", async () => {
85
- const { dir, proxyLog } = aDirectory();
86
- const logs = createClientLogFiles(proxyLog);
87
- logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
88
- await logs.close();
89
-
90
- const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
91
- assert.equal(names.length, 1);
92
- assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
93
- assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
94
- });
95
-
96
- test("two sessions of one film are two files", async () => {
97
- const { dir, proxyLog } = aDirectory();
98
- const logs = createClientLogFiles(proxyLog);
99
- logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
100
- logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
101
- await logs.close();
102
-
103
- assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
104
- });
105
-
106
- test("no proxy log path means nothing is written and nothing throws", async () => {
107
- const logs = createClientLogFiles("");
108
- logs.write(aSession(), ["dropped"]);
109
- await logs.close();
110
- });
111
-
112
- test("without a log file the lines go to the console, each naming its session", async () => {
113
- // A proxy started from npm or Docker names no file; the browser's half of
114
- // the viewing must still reach the output the host keeps.
115
- const printed = [];
116
- const logs = createClientLogConsole((message) => printed.push(message));
117
- logs.write(aSession(), ["first line", "second line"]);
118
- logs.write(aSession({ sessionId: "" }), ["no id"]);
119
- await logs.close();
120
-
121
- assert.deepEqual(printed, [
122
- "client abcd1234 first line",
123
- "client abcd1234 second line",
124
- "client unknown no id"
125
- ]);
126
- });
@@ -1,34 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
-
4
- import { handleApiClientLogsPost } from "../routes/api/client-logs/post.js";
5
-
6
- /**
7
- * Post one body and return the lines the route wrote.
8
- *
9
- * @param {object} body
10
- * @returns {Promise<string[]>}
11
- */
12
- async function linesWrittenFor(body) {
13
- /** @type {string[]} */
14
- const written = [];
15
- const reply = { code: () => reply, send: () => reply };
16
- await handleApiClientLogsPost({ body }, reply, {
17
- clientLogs: { write: (_session, lines) => written.push(...lines) }
18
- });
19
- return written;
20
- }
21
-
22
- const line = { level: "info", ts: "12:00:00.000", msg: "hello" };
23
-
24
- test("each line names the page's send it came in, so the two receivers can be compared", async () => {
25
- assert.deepEqual(await linesWrittenFor({ tag: "Windows/Chrome", seq: 12, lines: [line] }), [
26
- "[Windows/Chrome batch=12] 12:00:00.000 info: hello"
27
- ]);
28
- });
29
-
30
- test("a page that numbers nothing is written as before", async () => {
31
- assert.deepEqual(await linesWrittenFor({ tag: "Windows/Chrome", seq: "12", lines: [line] }), [
32
- "[Windows/Chrome] 12:00:00.000 info: hello"
33
- ]);
34
- });
@@ -1,39 +0,0 @@
1
- /**
2
- * @file A request that arrives during a run gives one more run, never a lost
3
- * one and never a queue.
4
- */
5
-
6
- import test from "node:test";
7
- import assert from "node:assert/strict";
8
-
9
- import { coalescing } from "../utils/coalesce.js";
10
-
11
- test("asked during a run: one more run after it, however often it was asked", async () => {
12
- const runs = [];
13
- let release;
14
- const run = coalescing(async (label) => {
15
- runs.push(label);
16
- if (runs.length === 1) {
17
- await new Promise((resolve) => {
18
- release = resolve;
19
- });
20
- }
21
- });
22
- const first = run("file", "first");
23
- await run("file", "second");
24
- await run("file", "third");
25
- assert.deepEqual(runs, ["first"], "nothing ran beside the run in progress");
26
- release();
27
- await first;
28
- await new Promise((resolve) => setImmediate(resolve));
29
- assert.deepEqual(runs, ["first", "third"], "exactly one more run, with the latest arguments");
30
- });
31
-
32
- test("different keys run side by side", async () => {
33
- const runs = [];
34
- const run = coalescing(async (label) => {
35
- runs.push(label);
36
- });
37
- await Promise.all([run("a", "a"), run("b", "b")]);
38
- assert.deepEqual(runs.sort(), ["a", "b"]);
39
- });
@@ -1,115 +0,0 @@
1
- /**
2
- * @file Files downloaded whole are files, and outlive the torrent that fetched
3
- * them.
4
- *
5
- * Asked for in these words on 2026-09-11: as soon as a torrent is fully
6
- * downloaded, downloading stops, the torrent is deleted, and the artefacts —
7
- * what was downloaded — stay for as long as they are wanted.
8
- */
9
-
10
- import test from "node:test";
11
- import assert from "node:assert/strict";
12
- import fs from "node:fs/promises";
13
- import os from "node:os";
14
- import path from "node:path";
15
- import { Readable } from "node:stream";
16
- import { CompletedFiles } from "../services/storage/files/CompletedFiles.js";
17
-
18
- const INFO_HASH = "abcdef0123456789abcdef0123456789abcdef01";
19
-
20
- /** @returns {Promise<string>} */
21
- const directory = () => fs.mkdtemp(path.join(os.tmpdir(), "whole-files-"));
22
-
23
- /**
24
- * @param {Buffer} bytes
25
- * @param {number} [chunk]
26
- * @returns {() => NodeJS.ReadableStream}
27
- */
28
- const opens = (bytes, chunk = 7) => () => {
29
- const parts = [];
30
- for (let at = 0; at < bytes.length; at += chunk) {
31
- parts.push(bytes.subarray(at, Math.min(at + chunk, bytes.length)));
32
- }
33
- return Readable.from(parts);
34
- };
35
-
36
- test("a file read whole is kept whole, and reads back byte for byte", async () => {
37
- const root = await directory();
38
- const files = new CompletedFiles({ root });
39
- const bytes = Buffer.from("the whole of a small film", "utf8");
40
- try {
41
- const kept = await files.keep({ infoHash: INFO_HASH, fileIndex: 3, length: bytes.length, name: "film.mkv", open: opens(bytes) });
42
- assert.ok(kept, "the file was not kept");
43
- assert.equal(kept.length, bytes.length);
44
-
45
- const found = files.find(INFO_HASH, 3);
46
- assert.deepEqual(found, kept, "what was kept is not what is found");
47
- assert.deepEqual(await fs.readFile(found.path), bytes, "the bytes came back changed");
48
- } finally {
49
- await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
50
- }
51
- });
52
-
53
- test("a read that ends early leaves nothing that looks whole", async () => {
54
- const root = await directory();
55
- const files = new CompletedFiles({ root });
56
- const bytes = Buffer.from("half a film", "utf8");
57
- try {
58
- // The torrent says the file is longer than what its read produced — the
59
- // data went away mid-write, which over a torrent is ordinary.
60
- const kept = await files.keep({
61
- infoHash: INFO_HASH,
62
- fileIndex: 0,
63
- length: bytes.length + 100,
64
- open: opens(bytes)
65
- });
66
- assert.equal(kept, null, "a short file was kept as whole");
67
- assert.equal(files.find(INFO_HASH, 0), null);
68
- assert.deepEqual(
69
- (await fs.readdir(path.join(root, INFO_HASH))).filter((entry) => entry !== "manifest.json"),
70
- [],
71
- "a partial file was left behind"
72
- );
73
- } finally {
74
- await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
75
- }
76
- });
77
-
78
- test("what a previous life left is taken up, and what is the wrong size is not", async () => {
79
- const root = await directory();
80
- const bytes = Buffer.from("a film from the last time this ran", "utf8");
81
- try {
82
- const first = new CompletedFiles({ root });
83
- await first.keep({ infoHash: INFO_HASH, fileIndex: 2, length: bytes.length, name: "film.mkv", open: opens(bytes) });
84
- // And one that was being written when the process died, under a name that
85
- // says it is whole — the only way to tell is its size.
86
- await fs.writeFile(path.join(root, INFO_HASH, "5"), Buffer.alloc(3));
87
-
88
- const second = new CompletedFiles({ root });
89
- const adopted = await second.adopt((infoHash, fileIndex) =>
90
- infoHash === INFO_HASH && fileIndex === 2 ? bytes.length : 999
91
- );
92
- assert.equal(adopted, 1, "the whole file was not taken up, or the short one was");
93
- assert.equal(second.find(INFO_HASH, 2)?.name, "film.mkv", "the name the torrent gave it was lost");
94
- assert.equal(second.find(INFO_HASH, 5), null, "a file of the wrong size was taken up");
95
- } finally {
96
- await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
97
- }
98
- });
99
-
100
- test("forgetting a torrent removes its files and nobody else's", async () => {
101
- const root = await directory();
102
- const files = new CompletedFiles({ root });
103
- const other = "0123456789abcdef0123456789abcdef01234567";
104
- const bytes = Buffer.from("kept", "utf8");
105
- try {
106
- await files.keep({ infoHash: INFO_HASH, fileIndex: 1, length: bytes.length, name: "one.mkv", open: opens(bytes) });
107
- await files.keep({ infoHash: other, fileIndex: 1, length: bytes.length, name: "two.mkv", open: opens(bytes) });
108
-
109
- await files.forget(INFO_HASH);
110
- assert.equal(files.find(INFO_HASH, 1), null);
111
- assert.ok(files.find(other, 1), "another torrent's file went with it");
112
- } finally {
113
- await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
114
- }
115
- });