@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,149 +0,0 @@
1
- /**
2
- * @file One budget: one owner, one policy, a share per resource.
3
- *
4
- * What it replaced was two owners applying one rule to two readings — memory
5
- * divided inside the torrent thread, disk divided on the main one, neither able
6
- * to see the other. The claimants TRADE ACROSS RESOURCES: pieces that do not
7
- * fit in memory are spilled to disk, so how much memory the piece store is
8
- * given decides how much disk it needs. Field 2026-08-31: 14 400 MB spilled in
9
- * fifty minutes while the memory store held 312-424 MB.
10
- *
11
- * And "disk" is not one resource. Measured on the addon host 2026-09-05,
12
- * `/tmp` is the overlay filesystem and `/data` is ext4 on the nvme — two
13
- * devices, so one figure divided between claimants on both gives each a share
14
- * of a disk it does not write to.
15
- */
16
-
17
- import test from "node:test";
18
- import assert from "node:assert/strict";
19
- import { MachineBudget } from "../services/storage/MachineBudget.js";
20
-
21
- const MEGABYTE = 1024 * 1024;
22
-
23
- /**
24
- * @param {{ name: string, resource: string, held?: number, wanted: number, minimum?: number }} shape
25
- * @returns {{ claimant: object, allowed: () => number }}
26
- */
27
- function claimantOf({ name, resource, held = 0, wanted, minimum }) {
28
- let allowed = 0;
29
- return {
30
- claimant: {
31
- name,
32
- resource,
33
- minimum,
34
- held: () => held,
35
- wanted: () => wanted,
36
- allow: (bytes) => {
37
- allowed = bytes;
38
- }
39
- },
40
- allowed: () => allowed
41
- };
42
- }
43
-
44
- test("each resource is divided on its own — a share of memory is not a share of disk", async () => {
45
- const budget = new MachineBudget({ logger: { info: () => {}, warn: () => {} } });
46
- budget.defineResource({ name: "memory", readFree: () => 100 * MEGABYTE });
47
- budget.defineResource({ name: "disk:tmp", readFree: () => 10 * MEGABYTE });
48
-
49
- const pieces = claimantOf({ name: "pieces in memory", resource: "memory", wanted: Number.MAX_SAFE_INTEGER });
50
- const segments = claimantOf({ name: "segments", resource: "disk:tmp", wanted: Number.MAX_SAFE_INTEGER });
51
- budget.register(pieces.claimant);
52
- budget.register(segments.claimant);
53
-
54
- await budget.revise();
55
-
56
- assert.equal(pieces.allowed(), 100 * MEGABYTE, "memory is divided from what memory has");
57
- assert.equal(segments.allowed(), 10 * MEGABYTE, "and disk from what that disk has");
58
- });
59
-
60
- test("two devices are two resources — one is not cut to pay for the other", async () => {
61
- const budget = new MachineBudget({ logger: { info: () => {}, warn: () => {} } });
62
- budget.defineResource({ name: "disk:66305", readFree: () => 8 * MEGABYTE });
63
- budget.defineResource({ name: "disk:68", readFree: () => 80 * MEGABYTE });
64
-
65
- // The shape on the addon host: segments and spill on the overlay, the
66
- // evidence on the nvme.
67
- const segments = claimantOf({ name: "segments", resource: "disk:68", wanted: Number.MAX_SAFE_INTEGER });
68
- const spill = claimantOf({ name: "spilled pieces", resource: "disk:68", wanted: Number.MAX_SAFE_INTEGER });
69
- const evidence = claimantOf({ name: "diagnostics", resource: "disk:66305", wanted: Number.MAX_SAFE_INTEGER });
70
- for (const one of [segments, spill, evidence]) {
71
- budget.register(one.claimant);
72
- }
73
-
74
- await budget.revise();
75
-
76
- assert.equal(evidence.allowed(), 8 * MEGABYTE, "the evidence gets the disk it actually writes to, whole");
77
- assert.equal(segments.allowed() + spill.allowed(), 80 * MEGABYTE, "and the other device is divided among its own");
78
- });
79
-
80
- test("a fixed ceiling is the operator's, and it binds", async () => {
81
- const budget = new MachineBudget({
82
- policy: { kind: "fixed", bytes: 5 * MEGABYTE },
83
- logger: { info: () => {}, warn: () => {} }
84
- });
85
- budget.defineResource({ name: "disk", readFree: () => 1000 * MEGABYTE });
86
- const segments = claimantOf({ name: "segments", resource: "disk", wanted: Number.MAX_SAFE_INTEGER });
87
- budget.register(segments.claimant);
88
-
89
- await budget.revise();
90
-
91
- assert.equal(segments.allowed(), 5 * MEGABYTE, "a disk with a gigabyte free is still capped at what was asked for");
92
- });
93
-
94
- test("a share of free is the operator's too", async () => {
95
- const budget = new MachineBudget({
96
- policy: { kind: "share", share: 0.25 },
97
- logger: { info: () => {}, warn: () => {} }
98
- });
99
- budget.defineResource({ name: "disk", readFree: () => 40 * MEGABYTE });
100
- const segments = claimantOf({ name: "segments", resource: "disk", held: 0, wanted: Number.MAX_SAFE_INTEGER });
101
- budget.register(segments.claimant);
102
-
103
- await budget.revise();
104
-
105
- assert.equal(segments.allowed(), 10 * MEGABYTE);
106
- });
107
-
108
- test("a floor lifts a policy that would allow less, and cannot invent room", async () => {
109
- const budget = new MachineBudget({
110
- policy: { kind: "fixed", bytes: MEGABYTE, floors: { disk: 6 * MEGABYTE } },
111
- logger: { info: () => {}, warn: () => {} }
112
- });
113
- budget.defineResource({ name: "disk", readFree: () => 4 * MEGABYTE });
114
- const segments = claimantOf({ name: "segments", resource: "disk", wanted: Number.MAX_SAFE_INTEGER });
115
- budget.register(segments.claimant);
116
-
117
- await budget.revise();
118
-
119
- assert.equal(
120
- segments.allowed(),
121
- 4 * MEGABYTE,
122
- "the floor lifts the ceiling above the fixed one, but never past what the machine has"
123
- );
124
- });
125
-
126
- test("a resource that cannot be read allows nothing to grow", async () => {
127
- const budget = new MachineBudget({ logger: { info: () => {}, warn: () => {} } });
128
- budget.defineResource({ name: "disk", readFree: () => null });
129
- const segments = claimantOf({ name: "segments", resource: "disk", held: 0, wanted: Number.MAX_SAFE_INTEGER });
130
- budget.register(segments.claimant);
131
-
132
- await budget.revise();
133
-
134
- assert.equal(segments.allowed(), 0, "unreadable is not the same as unbounded");
135
- });
136
-
137
- test("a claimant below its own minimum says so", async () => {
138
- const said = [];
139
- const budget = new MachineBudget({ logger: { info: () => {}, warn: (line) => said.push(line) } });
140
- budget.defineResource({ name: "memory", readFree: () => MEGABYTE });
141
- budget.register(claimantOf({
142
- name: "pieces in memory", resource: "memory", wanted: Number.MAX_SAFE_INTEGER, minimum: 64 * MEGABYTE
143
- }).claimant);
144
-
145
- await budget.revise();
146
-
147
- assert.equal(said.length, 1, "working below what it needs is a fault of the machine, and invisible otherwise");
148
- assert.match(said[0], /below the 64MB it needs/);
149
- });
@@ -1,273 +0,0 @@
1
- /**
2
- * @file Many viewers of one output are handed one session.
3
- *
4
- * This is the product behaviour the whole output key exists for: two people
5
- * watching the same film, in the same form, at whatever moments, are served by
6
- * one encode and one set of pieces. It had no check of any kind — every test
7
- * that needed a session either built one by hand or replaced
8
- * `createOrGetSession` with a stub — so the one line that decides it could have
9
- * been broken by any edit without a word.
10
- *
11
- * What is checked here is the decision, not the creation: a session under the
12
- * name this request produces already exists, and the request must be answered
13
- * with it rather than with a new one. The name is the format produced, so the
14
- * request is first taken as far as the format — the file's facts and its
15
- * keyframe table are given as already known — and making the FIRST session,
16
- * which reaches the disk, is a different subject.
17
- */
18
-
19
- import test from "node:test";
20
- import assert from "node:assert/strict";
21
- import { wireOutputs } from "../services/server/wire-outputs.js";
22
- import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/encode/output/OutputSpec.js";
23
- import { Timeline } from "../services/encode/output/Timeline.js";
24
- import { SourceFile } from "../services/media/SourceFile.js";
25
-
26
- const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
27
-
28
- /**
29
- * What a browser asks for: this file, copied, sound inside the stream.
30
- *
31
- * @param {object} [over]
32
- * @returns {object}
33
- */
34
- function request(over = {}) {
35
- return {
36
- sourceKey: TORRENT,
37
- fileIndex: 0,
38
- fileName: "film.mkv",
39
- transcodeVideo: false,
40
- transcodeAudio: false,
41
- audioTrackIndex: 0,
42
- segmentFormatId: "fmp4",
43
- ...over
44
- };
45
- }
46
-
47
- /**
48
- * The name the manager will look that request up under, built the way it builds
49
- * it: a copied picture carrying its own sound, cut at the file's keyframes.
50
- *
51
- * @returns {string}
52
- */
53
- function specOfThatOutput({ trackIndex = 0, transcode = false } = {}) {
54
- return new OutputSpec({
55
- sourceKey: TORRENT,
56
- segmentFormatId: "fmp4",
57
- grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
58
- video: new VideoOutput({ fileIndex: 0, encode: null }),
59
- audio: new AudioOutput({ fileIndex: 0, trackIndex, transcode })
60
- });
61
- }
62
-
63
- /** @returns {string} */
64
- function nameOfThatOutput(sound) {
65
- return specOfThatOutput(sound).toName();
66
- }
67
-
68
- /**
69
- * @param {object} manager
70
- * @param {{ trackIndex?: number, transcode?: boolean }} [sound] - The soundtrack
71
- * the seeded output carries; the file's own, copied, when left out.
72
- * @returns {object} The session it is seeded with.
73
- */
74
- function seedOneSession(manager, sound = {}) {
75
- const session = {
76
- id: nameOfThatOutput(sound),
77
- spec: specOfThatOutput(sound),
78
- // Its own address, so two seeded outputs are two places for viewers.
79
- outputKey: specOfThatOutput(sound).toKey(),
80
- state: "ready",
81
- file: new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }),
82
- timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
83
- runs: new Set(),
84
- progress: { processedSeconds: 0, startPositionSeconds: 0, updatedAt: Date.now() },
85
- lastAccessedAt: Date.now(),
86
- // READY, because a joining viewer waits for the session to be able to
87
- // answer them, and this file is about which session answers rather than
88
- // about that wait. A session publishing its own playlist from the probed
89
- // duration has nothing to wait for; left out, every check here would sit
90
- // out the warm-up deadline and would be measuring that timer.
91
- useSyntheticPlaylist: true
92
- };
93
- manager.outputs.set(session.id, session);
94
- return session;
95
- }
96
-
97
- /**
98
- * What the proxy already knows about the file when a second viewer arrives:
99
- * what a probe said, and where its keyframes are.
100
- *
101
- * @param {object} manager
102
- */
103
- function knownFile(manager) {
104
- // This check exercises output identity. Machine admission has its own tests.
105
- manager.admission.admitsWatching = () => ({ admitted: true, reason: "", speedX: 8 });
106
- manager.admission.previewCandidate = () => ({ admitted: true, reason: "", speedX: 8 });
107
- manager.getCachedMediaInfo = () => ({ durationSeconds: 8, width: 1920, height: 1080, fps: 24 });
108
- // The soundtracks the plan listed: the file's own, at the rate it states, and
109
- // a dub beside it that states nothing — which no link can be asked about as a
110
- // copy, so it is sent re-encoded.
111
- manager.getCachedAudioTracks = () => [
112
- { index: 0, fileIndex: 0, sourceTrackIndex: 0, bitrateKbps: 128 },
113
- { index: 1, fileIndex: 0, sourceTrackIndex: 1, bitrateKbps: null, peakKbps: null }
114
- ];
115
- manager.keyframeTables.learn({ sourceKey: TORRENT, fileIndex: 0 }, { times: [0, 4], format: "test" });
116
- }
117
-
118
- test("a second viewer of one output is served by the session that exists", async (t) => {
119
- const manager = wireOutputs({
120
- enabled: true,
121
- ffmpegBin: "ffmpeg",
122
- localBindHost: "127.0.0.1",
123
- localPort: 9090
124
- });
125
- t.after(() => manager.lifecycle.disposeAll());
126
- // No plan runs here: this file is about which session answers a request, and
127
- // placing encoders would spawn real processes.
128
- manager.encodeRuns.planEncodersNow = () => {};
129
- manager.encodeRuns.planEncodersSoon = () => {};
130
- knownFile(manager);
131
- const seeded = seedOneSession(manager);
132
-
133
- const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
134
-
135
- assert.equal(answered, seeded, "one output, one session");
136
- assert.ok(manager.viewers.forOutput(seeded).has("viewer-two"), "and the second viewer is on it");
137
- });
138
-
139
- test("a request whose parameters differ is not that session", async (t) => {
140
- // The other half, and it is what makes the first mean anything: the name is
141
- // the output's, so a request producing something else must not find it. Here
142
- // the sound is to be re-encoded, which is a different output and a different
143
- // set of bytes.
144
- const manager = wireOutputs({
145
- enabled: true,
146
- ffmpegBin: "ffmpeg",
147
- localBindHost: "127.0.0.1",
148
- localPort: 9090,
149
- // A session made for real here waits for a playlist that nothing is going
150
- // to write. Zero, so this check spends no time on a deadline it is not
151
- // about — the deadline has its own subject elsewhere.
152
- startupWaitMs: 0
153
- });
154
- t.after(() => manager.lifecycle.disposeAll());
155
- manager.encodeRuns.planEncodersNow = () => {};
156
- manager.encodeRuns.planEncodersSoon = () => {};
157
- const seeded = seedOneSession(manager);
158
-
159
- let answered = null;
160
- try {
161
- answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two", transcodeAudio: true }));
162
- } catch {
163
- // Making a session for real needs a probe and a disk, which this file does
164
- // not give it. Failing there is the proof: it did not take the seeded one.
165
- answered = null;
166
- }
167
-
168
- assert.notEqual(answered, seeded);
169
- assert.ok(!manager.viewers.forOutput(seeded).has("viewer-two"), "and nobody was added to somebody else's output");
170
- });
171
-
172
- test("two screens that come to the same format share one output, and a different format does not", async (t) => {
173
- const manager = wireOutputs({
174
- enabled: true,
175
- ffmpegBin: "ffmpeg",
176
- localBindHost: "127.0.0.1",
177
- localPort: 9090,
178
- startupWaitMs: 0,
179
- // A host with no ladder to choose from: the box asked for, fitted to the
180
- // source, is what is produced.
181
- videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
182
- });
183
- t.after(() => manager.lifecycle.disposeAll());
184
- manager.encodeRuns.planEncodersNow = () => {};
185
- manager.encodeRuns.planEncodersSoon = () => {};
186
- knownFile(manager);
187
- const spec = new OutputSpec({
188
- sourceKey: TORRENT,
189
- segmentFormatId: "fmp4",
190
- grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
191
- video: new VideoOutput({
192
- fileIndex: 0,
193
- encode: { encoder: "h264_vaapi", width: 1920, height: 1080, fps: 24, preset: null, tonemap: false }
194
- }),
195
- audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: false })
196
- });
197
- const seeded = { ...seedOneSession(manager), id: spec.toName(), spec, timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "uniform" }) };
198
- manager.outputs.delete(nameOfThatOutput());
199
- manager.outputs.set(seeded.id, seeded);
200
-
201
- // A taller window than the film: fitted to the source it is the same 1920x1080.
202
- const joined = await manager.viewerRequests.createOrGetSession(
203
- request({ consumerId: "tall-window", transcodeVideo: true, targetWidth: 1920, targetHeight: 1200 })
204
- );
205
- assert.equal(joined, seeded, "the key names the format produced, not the window it was asked for");
206
-
207
- let other = null;
208
- try {
209
- other = await manager.viewerRequests.createOrGetSession(
210
- request({ consumerId: "small-window", transcodeVideo: true, targetWidth: 1280, targetHeight: 720 })
211
- );
212
- } catch {
213
- other = null;
214
- }
215
- assert.notEqual(other, seeded, "a smaller picture is another output");
216
- assert.ok(!manager.viewers.forOutput(seeded).has("small-window"));
217
- });
218
-
219
- test("a remembered dub with no stated rate opens on the output that sends it as AAC, not on its copy", async (t) => {
220
- // The field case of 2026-10-01: the next episode opened on a dub the page
221
- // remembered, which its browser plays as it stands and nothing states a rate
222
- // for. Opened as a copy, no link could be asked about it and the episode was
223
- // refused; it is the AAC output that answers.
224
- const manager = wireOutputs({
225
- enabled: true,
226
- ffmpegBin: "ffmpeg",
227
- localBindHost: "127.0.0.1",
228
- localPort: 9090,
229
- // A measured copy speed, so this machine can price the output and the
230
- // admission answers on room rather than on knowing nothing.
231
- copySpeedX: 20
232
- });
233
- t.after(() => manager.lifecycle.disposeAll());
234
- manager.encodeRuns.planEncodersNow = () => {};
235
- manager.encodeRuns.planEncodersSoon = () => {};
236
- knownFile(manager);
237
- const copied = seedOneSession(manager, { trackIndex: 1, transcode: false });
238
- const encoded = seedOneSession(manager, { trackIndex: 1, transcode: true });
239
-
240
- const answered = await manager.viewerRequests.createOrGetSession(
241
- request({ consumerId: "viewer-two", audioTrackIndex: 1, transcodeAudio: false })
242
- );
243
-
244
- assert.equal(answered, encoded, "the dub is re-encoded");
245
- assert.notEqual(answered, copied);
246
- assert.deepEqual(
247
- manager.renditions.soundtrackOf(answered, "viewer-two"),
248
- { trackIndex: 1, transcode: true },
249
- "and the viewer is recorded as receiving AAC"
250
- );
251
- });
252
-
253
- test("a track whose rate is stated is still copied for a browser that plays it", async (t) => {
254
- const manager = wireOutputs({
255
- enabled: true,
256
- ffmpegBin: "ffmpeg",
257
- localBindHost: "127.0.0.1",
258
- localPort: 9090,
259
- // A measured copy speed, so this machine can price the output and the
260
- // admission answers on room rather than on knowing nothing.
261
- copySpeedX: 20
262
- });
263
- t.after(() => manager.lifecycle.disposeAll());
264
- manager.encodeRuns.planEncodersNow = () => {};
265
- manager.encodeRuns.planEncodersSoon = () => {};
266
- knownFile(manager);
267
- const copied = seedOneSession(manager);
268
- seedOneSession(manager, { trackIndex: 0, transcode: true });
269
-
270
- const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
271
-
272
- assert.equal(answered, copied);
273
- });
@@ -1,185 +0,0 @@
1
- /**
2
- * @file One disk, one owner, three claimants.
3
- *
4
- * What these pin is the thing that was wrong: each consumer read the free space
5
- * as though it were alone, so three ceilings each stood for the whole disk.
6
- */
7
-
8
- import test from "node:test";
9
- import assert from "node:assert/strict";
10
- import { MachineBudget } from "../services/storage/MachineBudget.js";
11
-
12
- const MEGABYTE = 1024 * 1024;
13
-
14
- /**
15
- * @param {string} name
16
- * @param {number} held
17
- * @param {number} wanted
18
- */
19
- function consumerOf(name, held, wanted) {
20
- const state = { allowed: -1 };
21
- return {
22
- state,
23
- consumer: {
24
- name,
25
- held: () => held,
26
- wanted: () => wanted,
27
- allow: (bytes) => {
28
- state.allowed = bytes;
29
- }
30
- }
31
- };
32
- }
33
-
34
- /**
35
- * A budget over ONE resource, which is what this file is about: the division
36
- * itself, with the several-resources question asked in `one-budget.test.js`.
37
- *
38
- * @param {{ readFree: () => Promise<number | null>, logger?: object }} params
39
- */
40
- function diskOf({ readFree, logger }) {
41
- const budget = new MachineBudget({ logger });
42
- budget.defineResource({ name: "disk", readFree });
43
- return {
44
- register: (consumer) => budget.register({ ...consumer, resource: "disk" }),
45
- forget: (name) => budget.forget(name),
46
- describe: () => budget.describe(),
47
- revise: async () => {
48
- const readings = await budget.revise();
49
- return readings.get("disk") ?? { freeBytes: 0, allowanceBytes: 0, shares: [] };
50
- }
51
- };
52
- }
53
-
54
- test("what everyone may hold together never exceeds what the disk has", async () => {
55
- const free = 100 * MEGABYTE;
56
- const space = diskOf({ readFree: async () => free });
57
- const segments = consumerOf("segments", 10 * MEGABYTE, 1000 * MEGABYTE);
58
- const pieces = consumerOf("pieces", 20 * MEGABYTE, 1000 * MEGABYTE);
59
- const diagnostics = consumerOf("diagnostics", 5 * MEGABYTE, 1000 * MEGABYTE);
60
- space.register(segments.consumer);
61
- space.register(pieces.consumer);
62
- space.register(diagnostics.consumer);
63
-
64
- const { allowanceBytes } = await space.revise();
65
-
66
- const together = segments.state.allowed + pieces.state.allowed + diagnostics.state.allowed;
67
- assert.ok(together <= allowanceBytes, "the shares add up to more than the allowance");
68
- // Free space plus what they already hold: that pair is the ceiling they could
69
- // reach, which is the same statement the memory allowance makes.
70
- assert.equal(allowanceBytes, free + 35 * MEGABYTE);
71
- assert.ok(
72
- together <= free + 35 * MEGABYTE,
73
- "three consumers were between them allowed more than the disk has"
74
- );
75
- });
76
-
77
- test("when everyone's ask fits, everyone gets it and the disk never binds", async () => {
78
- const space = diskOf({ readFree: async () => 1000 * MEGABYTE });
79
- const small = consumerOf("diagnostics", 0, 5 * MEGABYTE);
80
- const large = consumerOf("segments", 0, 500 * MEGABYTE);
81
- space.register(small.consumer);
82
- space.register(large.consumer);
83
-
84
- await space.revise();
85
-
86
- assert.equal(small.state.allowed, 5 * MEGABYTE);
87
- assert.equal(large.state.allowed, 500 * MEGABYTE);
88
- });
89
-
90
- test("when they do not fit, each is cut in proportion to what it asked", async () => {
91
- // The same rule memory divides by, and the same reason: a share taken from
92
- // whoever asked most would hand the disk to whoever grew fastest.
93
- const space = diskOf({ readFree: async () => 200 * MEGABYTE });
94
- const modest = consumerOf("diagnostics", 0, 100 * MEGABYTE);
95
- const greedy = consumerOf("segments", 0, 300 * MEGABYTE);
96
- space.register(modest.consumer);
97
- space.register(greedy.consumer);
98
-
99
- await space.revise();
100
-
101
- assert.equal(modest.state.allowed, 50 * MEGABYTE);
102
- assert.equal(greedy.state.allowed, 150 * MEGABYTE);
103
- assert.equal(modest.state.allowed + greedy.state.allowed, 200 * MEGABYTE);
104
- });
105
-
106
- test("a disk that fills lowers every share, rather than keeping one taken when it was empty", async () => {
107
- let free = 1000 * MEGABYTE;
108
- const space = diskOf({ readFree: async () => free });
109
- const segments = consumerOf("segments", 0, 10_000 * MEGABYTE);
110
- const pieces = consumerOf("pieces", 0, 10_000 * MEGABYTE);
111
- space.register(segments.consumer);
112
- space.register(pieces.consumer);
113
-
114
- await space.revise();
115
- const roomy = segments.state.allowed;
116
-
117
- free = 40 * MEGABYTE;
118
- await space.revise();
119
-
120
- assert.ok(segments.state.allowed < roomy, "the share did not follow the disk down");
121
- assert.ok(pieces.state.allowed < roomy, "the other share did not follow either");
122
- });
123
-
124
- test("what another process took is left for it, not spent", async () => {
125
- // Between two readings the disk lost 400 MB that none of our consumers took.
126
- // That is a measurement of somebody else's demand, and it is what the next
127
- // allowance leaves alone.
128
- let free = 1000 * MEGABYTE;
129
- const space = diskOf({ readFree: async () => free });
130
- const only = consumerOf("segments", 0, 10_000 * MEGABYTE);
131
- space.register(only.consumer);
132
-
133
- await space.revise();
134
- free = 600 * MEGABYTE;
135
- const { allowanceBytes } = await space.revise();
136
-
137
- assert.equal(allowanceBytes, 200 * MEGABYTE, "the fall we did not cause was spent anyway");
138
- });
139
-
140
- test("a disk that cannot be read allows nothing to grow", async () => {
141
- const space = diskOf({ readFree: async () => null });
142
- const segments = consumerOf("segments", 0, 500 * MEGABYTE);
143
- space.register(segments.consumer);
144
-
145
- const { freeBytes, allowanceBytes } = await space.revise();
146
-
147
- assert.equal(freeBytes, 0);
148
- assert.equal(allowanceBytes, 0);
149
- assert.equal(segments.state.allowed, 0, "an unreadable disk was treated as an empty one");
150
- });
151
-
152
- test("it says what the disk has and what each claimant may hold", async () => {
153
- const space = diskOf({ readFree: async () => 100 * MEGABYTE });
154
- assert.match(space.describe(), /nothing has claimed/);
155
- space.register(consumerOf("segments", 4 * MEGABYTE, 8 * MEGABYTE).consumer);
156
- await space.revise();
157
- assert.match(space.describe(), /disk: 100MB free; segments 4MB of 8MB/);
158
- });
159
-
160
- test("it says what it decided, every pass", async () => {
161
- const lines = [];
162
- const space = diskOf({
163
- readFree: async () => 100 * MEGABYTE,
164
- logger: { info: (line) => lines.push(line) }
165
- });
166
- space.register(consumerOf("segments", 4 * MEGABYTE, 8 * MEGABYTE).consumer);
167
- await space.revise();
168
- assert.equal(lines.length, 1, "a pass that decided the shares said nothing about them");
169
- assert.match(lines[0], /disk: 100MB free; segments 4MB of 8MB/);
170
- });
171
-
172
- test("the free space is read from the nearest directory that exists", async () => {
173
- // The segments' directory is made when the first session starts and removed
174
- // when the proxy stops, so at every start `statfs` on it fails. Field
175
- // 2026-09-10: the first reading said `disk: 0MB free` on a host with 103 GB,
176
- // and zero means "no room" to everything that reads it.
177
- const { freeBytesFor } = await import("../services/storage/free.js");
178
- const os = await import("node:os");
179
- const path = await import("node:path");
180
- const missing = path.join(os.tmpdir(), "no-such-directory-here", "nor-here");
181
-
182
- const bytes = await freeBytesFor(missing);
183
-
184
- assert.ok(Number.isFinite(bytes) && bytes > 0, "a directory that does not exist read as no disk");
185
- });