@torrent-tv/proxy 2.89.17 → 2.89.18

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 (299) hide show
  1. package/README.md +14 -3
  2. package/package.json +10 -2
  3. package/server.js +0 -11
  4. package/.claude/commands/opsx/apply.md +0 -155
  5. package/.claude/commands/opsx/archive.md +0 -160
  6. package/.claude/commands/opsx/explore.md +0 -174
  7. package/.claude/commands/opsx/propose.md +0 -109
  8. package/.claude/commands/opsx/sync.md +0 -143
  9. package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
  10. package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
  11. package/.claude/skills/openspec-explore/SKILL.md +0 -289
  12. package/.claude/skills/openspec-propose/SKILL.md +0 -113
  13. package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
  14. package/.dockerignore +0 -5
  15. package/AGENTS.md +0 -363
  16. package/CHANGELOG.md +0 -2377
  17. package/Dockerfile +0 -30
  18. package/biome.json +0 -412
  19. package/corpus.tmp.json +0 -1
  20. package/docs/browser-proxy-contract.md +0 -155
  21. package/docs/container-architecture.md +0 -273
  22. package/docs/download-architecture.md +0 -261
  23. package/docs/encode-architecture.md +0 -765
  24. package/docs/encode-run-state.md +0 -96
  25. package/docs/logs.md +0 -151
  26. package/docs/playback-start-forecast.md +0 -113
  27. package/docs/socks5-egress-research.md +0 -114
  28. package/docs/storage-architecture.md +0 -273
  29. package/knip.json +0 -14
  30. package/main2.cpuprofile +0 -1
  31. package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
  32. package/openspec/changes/adaptive-bitrate/design.md +0 -92
  33. package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
  34. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
  35. package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
  36. package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
  37. package/openspec/changes/binary-distribution/proposal.md +0 -53
  38. package/openspec/changes/chunked-request-bodies/design.md +0 -81
  39. package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
  40. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
  41. package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
  42. package/openspec/changes/cold-start/design.md +0 -98
  43. package/openspec/changes/cold-start/proposal.md +0 -62
  44. package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
  45. package/openspec/changes/cold-start/tasks.md +0 -48
  46. package/openspec/changes/disk-cap/.openspec.yaml +0 -2
  47. package/openspec/changes/disk-cap/proposal.md +0 -37
  48. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
  49. package/openspec/changes/disk-cap/tasks.md +0 -19
  50. package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
  51. package/openspec/changes/proxy-observability/design.md +0 -38
  52. package/openspec/changes/proxy-observability/proposal.md +0 -49
  53. package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
  54. package/openspec/changes/proxy-observability/tasks.md +0 -21
  55. package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
  56. package/openspec/changes/seek-debounce/design.md +0 -118
  57. package/openspec/changes/seek-debounce/proposal.md +0 -59
  58. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
  59. package/openspec/changes/seek-debounce/tasks.md +0 -33
  60. package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
  61. package/openspec/changes/subtitle-language/proposal.md +0 -50
  62. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
  63. package/openspec/changes/subtitle-language/tasks.md +0 -26
  64. package/openspec/changes/track-selection/.openspec.yaml +0 -2
  65. package/openspec/changes/track-selection/design.md +0 -47
  66. package/openspec/changes/track-selection/proposal.md +0 -45
  67. package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
  68. package/openspec/changes/track-selection/tasks.md +0 -20
  69. package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
  70. package/openspec/changes/transcode-quality/proposal.md +0 -51
  71. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
  72. package/openspec/changes/transcode-quality/tasks.md +0 -63
  73. package/openspec/config.yaml +0 -38
  74. package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
  75. package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
  76. package/research/double-spawn-2026-09-10.md +0 -171
  77. package/research/handover-2026-09-13.md +0 -209
  78. package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
  79. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
  80. package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
  81. package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
  82. package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
  83. package/scripts/build-audio-samples.mjs +0 -31
  84. package/scripts/render-run-graph.js +0 -173
  85. package/test/asset-paths.test.js +0 -28
  86. package/test/audio-buffer-continuity.test.js +0 -77
  87. package/test/audio-calibration.test.js +0 -23
  88. package/test/audio-inventory.test.js +0 -212
  89. package/test/audio-track-bitrate.test.js +0 -40
  90. package/test/audio-warm-recorded.test.js +0 -63
  91. package/test/auto-quality-step.test.js +0 -762
  92. package/test/available-share.test.js +0 -76
  93. package/test/background-fill.test.js +0 -190
  94. package/test/client-log-file.test.js +0 -126
  95. package/test/client-logs-route.test.js +0 -34
  96. package/test/coalesce.test.js +0 -39
  97. package/test/completed-files.test.js +0 -115
  98. package/test/concurrent-cost.test.js +0 -148
  99. package/test/container-media-info.test.js +0 -228
  100. package/test/contention.test.js +0 -76
  101. package/test/core-dumps.test.js +0 -57
  102. package/test/coverage-follows-the-disk.test.js +0 -233
  103. package/test/coverage-map.test.js +0 -195
  104. package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
  105. package/test/cut-times-timeline.test.js +0 -65
  106. package/test/cuts-follow-published-grid.test.js +0 -109
  107. package/test/data-channel-frame.test.js +0 -63
  108. package/test/data-channel-viewer-lifecycle.test.js +0 -193
  109. package/test/dead-torrent-handle.test.js +0 -52
  110. package/test/declared-rates.test.js +0 -116
  111. package/test/declared-tracks.test.js +0 -43
  112. package/test/decode-cost-fit.test.js +0 -202
  113. package/test/decode-cost.test.js +0 -475
  114. package/test/delivery-probe-field.test.js +0 -173
  115. package/test/delivery-probe.test.js +0 -382
  116. package/test/demand-register.test.js +0 -206
  117. package/test/dht-bootstrap.test.js +0 -72
  118. package/test/drain-threat.test.js +0 -65
  119. package/test/duplicate-add.test.js +0 -48
  120. package/test/encode-admission.test.js +0 -170
  121. package/test/encode-cost.test.js +0 -275
  122. package/test/encode-exit.test.js +0 -111
  123. package/test/encode-orchestrator.test.js +0 -426
  124. package/test/encode-plan-viewers.test.js +0 -725
  125. package/test/encode-plan.test.js +0 -545
  126. package/test/encode-run-state.test.js +0 -314
  127. package/test/encode-run.test.js +0 -248
  128. package/test/encode-slope.test.js +0 -46
  129. package/test/encoded-output.test.js +0 -39
  130. package/test/encoder-kinds.test.js +0 -122
  131. package/test/encoder-readings.test.js +0 -68
  132. package/test/episode-naming.test.js +0 -131
  133. package/test/every-claimant-is-told-its-share.test.js +0 -152
  134. package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
  135. package/test/failed-start-breaker.test.js +0 -202
  136. package/test/fastest-wires.test.js +0 -143
  137. package/test/ffmpeg-stream-counts.test.js +0 -101
  138. package/test/file-edges.test.js +0 -191
  139. package/test/files-in-use.test.js +0 -34
  140. package/test/fixtures/audio-frame-boundary.json +0 -1
  141. package/test/fixtures/refused-audio-20261002.json +0 -10
  142. package/test/fmp4-presentation-clock.test.js +0 -148
  143. package/test/fmp4-self-contained.test.js +0 -80
  144. package/test/fmp4-timestamps.test.js +0 -140
  145. package/test/fmp4-track-completeness.test.js +0 -41
  146. package/test/format-keyed-outputs.test.js +0 -78
  147. package/test/health-metrics.test.js +0 -36
  148. package/test/held-read-ownership.test.js +0 -77
  149. package/test/held-request-width.test.js +0 -157
  150. package/test/helpers/encode-run.js +0 -248
  151. package/test/helpers/held-file.js +0 -77
  152. package/test/helpers/manager.js +0 -62
  153. package/test/helpers/matroska-file.js +0 -296
  154. package/test/helpers/output-spec.js +0 -63
  155. package/test/host-load.test.js +0 -68
  156. package/test/index-accuracy-is-about-the-file.test.js +0 -128
  157. package/test/init-compat.test.js +0 -219
  158. package/test/input-lost-quiets-the-plan.test.js +0 -261
  159. package/test/input-unavailable.test.js +0 -48
  160. package/test/installation-id.test.js +0 -39
  161. package/test/keeping-period.test.js +0 -83
  162. package/test/keyframe-index-accuracy.test.js +0 -188
  163. package/test/keyframe-table-budget.test.js +0 -123
  164. package/test/keyframes-belong-to-the-file.test.js +0 -249
  165. package/test/learned-median.test.js +0 -37
  166. package/test/letting-go-asks-who-is-watching.test.js +0 -191
  167. package/test/limit-row-by-area.test.js +0 -60
  168. package/test/link-budget.test.js +0 -111
  169. package/test/link-probe.test.js +0 -56
  170. package/test/local-encoding-data.test.js +0 -273
  171. package/test/logger-repeats.test.js +0 -134
  172. package/test/look-ahead-coverage.test.js +0 -56
  173. package/test/manager-surface.test.js +0 -291
  174. package/test/matroska-blocks.test.js +0 -0
  175. package/test/matroska-cluster-discovery.test.js +0 -323
  176. package/test/matroska-cues-track.test.js +0 -192
  177. package/test/media-time.test.js +0 -62
  178. package/test/memory-budget.test.js +0 -390
  179. package/test/move-cost.test.js +0 -216
  180. package/test/mp4-composition-times.test.js +0 -0
  181. package/test/mp4-subtitles.test.js +0 -173
  182. package/test/one-authority.test.js +0 -285
  183. package/test/one-budget.test.js +0 -149
  184. package/test/one-output-one-session.test.js +0 -273
  185. package/test/one-resource-divided.test.js +0 -185
  186. package/test/one-serving-output-per-height.test.js +0 -458
  187. package/test/orchestrator-wired.test.js +0 -207
  188. package/test/output-catalog.test.js +0 -39
  189. package/test/output-shape.test.js +0 -21
  190. package/test/output-spec.test.js +0 -251
  191. package/test/output-speed.test.js +0 -92
  192. package/test/outputs-being-prepared.test.js +0 -40
  193. package/test/packet-witness-ring.test.js +0 -236
  194. package/test/packet-witness.test.js +0 -148
  195. package/test/piece-completeness.test.js +0 -78
  196. package/test/piece-disk-store.test.js +0 -401
  197. package/test/piece-from-whole-file.test.js +0 -129
  198. package/test/piece-lru.test.js +0 -319
  199. package/test/piece-reader.test.js +0 -162
  200. package/test/piece-selection-offset.test.js +0 -85
  201. package/test/piece-store-eviction.test.js +0 -595
  202. package/test/piece-store-never-refuses.test.js +0 -153
  203. package/test/piece-store-reservations.test.js +0 -403
  204. package/test/piece-store-slow-disk.test.js +0 -159
  205. package/test/piece-tail.test.js +0 -97
  206. package/test/plan-host-timings.test.js +0 -68
  207. package/test/plan-is-steady.test.js +0 -225
  208. package/test/playback-readiness.test.js +0 -518
  209. package/test/playback-start-regressions.test.js +0 -101
  210. package/test/priority-map-download.test.js +0 -174
  211. package/test/priority-map-emptied.test.js +0 -204
  212. package/test/priority-map-per-output.test.js +0 -215
  213. package/test/priority-map.test.js +0 -163
  214. package/test/probe-wedge-certainty.test.js +0 -60
  215. package/test/produced-copy-choice.test.js +0 -213
  216. package/test/published-timeline.test.js +0 -83
  217. package/test/quality-offer.test.js +0 -160
  218. package/test/quality-variants.test.js +0 -1274
  219. package/test/rate-control-in-the-output.test.js +0 -181
  220. package/test/read-survives-withdrawal.test.js +0 -164
  221. package/test/read-window.test.js +0 -365
  222. package/test/reads-not-yet-arrived.test.js +0 -218
  223. package/test/resume-warm.test.js +0 -39
  224. package/test/run-graph-drift.test.js +0 -37
  225. package/test/run-inputs.test.js +0 -80
  226. package/test/run-intervals.test.js +0 -100
  227. package/test/run-position-follows-published-grid.test.js +0 -89
  228. package/test/run-progress.test.js +0 -64
  229. package/test/same-height-switch.test.js +0 -663
  230. package/test/seek-frees-the-viewers-requests.test.js +0 -217
  231. package/test/seek-landing.test.js +0 -109
  232. package/test/seek-target-not-superseded.test.js +0 -195
  233. package/test/segment-closed-on-both-branches.test.js +0 -114
  234. package/test/segment-cut-times.test.js +0 -63
  235. package/test/segment-demand.test.js +0 -80
  236. package/test/segment-hold-supersede.test.js +0 -91
  237. package/test/segment-judged-by-its-own-header.test.js +0 -222
  238. package/test/segment-serve-wiring.test.js +0 -481
  239. package/test/segment-store-eviction.test.js +0 -247
  240. package/test/segment-store.test.js +0 -352
  241. package/test/segment-true-start.test.js +0 -86
  242. package/test/segments-are-shared.test.js +0 -176
  243. package/test/send-chunk-bytes.test.js +0 -86
  244. package/test/serving-existing-output.test.js +0 -111
  245. package/test/serving-output.test.js +0 -102
  246. package/test/shared-piece-store.test.js +0 -310
  247. package/test/sidecar-naming.test.js +0 -142
  248. package/test/soundtrack-rate-bound.test.js +0 -289
  249. package/test/source-file.test.js +0 -133
  250. package/test/source-files-route.test.js +0 -113
  251. package/test/source-registry.test.js +0 -18
  252. package/test/split-a-stretch.test.js +0 -139
  253. package/test/stale-request-after-seek.test.js +0 -158
  254. package/test/startup-readings.test.js +0 -124
  255. package/test/stream-route.test.js +0 -237
  256. package/test/subtitle-cue-framing.test.js +0 -200
  257. package/test/subtitle-cue-source.test.js +0 -102
  258. package/test/subtitle-cue-walk.test.js +0 -385
  259. package/test/subtitle-cursor.test.js +0 -63
  260. package/test/subtitle-defaults.test.js +0 -118
  261. package/test/subtitle-language.test.js +0 -252
  262. package/test/subtitle-metadata.test.js +0 -15
  263. package/test/subtitle-plan-waits.test.js +0 -125
  264. package/test/subtitle-track-numbering.test.js +0 -391
  265. package/test/supply-margin.test.js +0 -190
  266. package/test/swarm-follows-readers.test.js +0 -174
  267. package/test/swarm-reach.test.js +0 -130
  268. package/test/swarm-selection.test.js +0 -220
  269. package/test/tail-duplication.test.js +0 -167
  270. package/test/the-channel-has-both-ends.test.js +0 -91
  271. package/test/the-torrent-thread-serves-bytes.test.js +0 -107
  272. package/test/thread-pool.test.js +0 -53
  273. package/test/timeline.test.js +0 -84
  274. package/test/torrent-contents.test.js +0 -235
  275. package/test/torrent-cost.test.js +0 -71
  276. package/test/torrent-files.test.js +0 -221
  277. package/test/torrent-naming.test.js +0 -255
  278. package/test/tracks-begin-together.test.js +0 -209
  279. package/test/tunnel-renewal.test.js +0 -167
  280. package/test/two-viewers-one-picture.test.js +0 -508
  281. package/test/upload-hurry.test.js +0 -186
  282. package/test/usrsctp-state.test.js +0 -131
  283. package/test/video-facts.test.js +0 -102
  284. package/test/viewer-assignments.test.js +0 -143
  285. package/test/viewer-outputs.test.js +0 -285
  286. package/test/viewer-presence.test.js +0 -116
  287. package/test/viewer-requests-admission.test.js +0 -92
  288. package/test/viewer-starts-on-map.test.js +0 -27
  289. package/test/viewer-states.test.js +0 -192
  290. package/test/viewer-subtitle-subscription.test.js +0 -96
  291. package/test/viewer.test.js +0 -139
  292. package/test/viewing-generations.test.js +0 -390
  293. package/test/visible-rung.test.js +0 -37
  294. package/test/wait-ledger.test.js +0 -100
  295. package/test/webrtc-shim.test.js +0 -149
  296. package/test/wedge-certainty.test.js +0 -131
  297. package/test/whole-file-held-read.test.js +0 -30
  298. package/test/withdraw-piece-claim.test.js +0 -212
  299. package/test/worker-channel.test.js +0 -158
@@ -1,209 +0,0 @@
1
- /**
2
- * @file Picture and sound must begin their runs at the same real instant.
3
- *
4
- * The two branches are asked for the same time and land in different places: a
5
- * copied picture may only begin at a real keyframe and may not begin before the
6
- * time asked for, so it moves FORWARD to the next one; a soundtrack has no
7
- * keyframes and begins exactly where asked. Measured 2026-08-17, the difference
8
- * was 0.58-2.96 s on one file, and the viewer got sound with no new picture for
9
- * as long as it lasted.
10
- *
11
- * The picture's true start is measured from the piece it produced. This pins
12
- * that the measurement is carried to the other members of the family.
13
- */
14
-
15
- import assert from "node:assert/strict";
16
- import { SourceFile } from "../services/media/SourceFile.js";
17
- import { startRunOn } from "./helpers/encode-run.js";
18
- import { Timeline } from "../services/encode/output/Timeline.js";
19
- import test from "node:test";
20
-
21
- import { wireOutputs } from "../services/server/wire-outputs.js";
22
- import { trueStartOf } from "../services/encode/run-command.js";
23
- import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
24
- import { ENCODE_RUN_STATE, INITIAL_RUN_STATE } from "../services/encode/encode-run-state.js";
25
- import { outputSpec } from "./helpers/output-spec.js";
26
-
27
- const BOUNDARIES = [0, 4, 8, 12, 16, 20];
28
-
29
- /**
30
- * A film's family: the picture, and a soundtrack rendition of it. Both runs
31
- * begin at boundary #2, which the container's table puts at 8 s.
32
- *
33
- * @returns {{ manager: object, picture: object, sound: object }}
34
- */
35
- function familyAtBoundaryTwo() {
36
- // ONE table for the film. Where it is cut is a fact about the FILE, so the
37
- // picture and its soundtrack hold the same array rather than a copy each.
38
- const boundaries = [...BOUNDARIES];
39
- const manager = wireOutputs({
40
- enabled: true,
41
- ffmpegBin: "ffmpeg",
42
- localBindHost: "127.0.0.1",
43
- localPort: 9090
44
- });
45
- const picture = {
46
- id: "picture",
47
- spec: outputSpec({ transcodeVideo: false }),
48
- get outputKey() { return this.spec.toKey(); },
49
- state: "ready",
50
- timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
51
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
52
- // An ordinary session reads its own file, and its sound is inside it. The
53
- // three differ only for a soundtrack shipped as a file of its own — and the
54
- // command is built from all three, so a fixture that states only the first
55
- // describes a session the product cannot make.
56
- get inputFile() { return this.file; },
57
- get audioFile() { return this.file; },
58
- // How its pieces are packaged. The command asks the format where to cut and
59
- // what to name the files, so a session without one is not a session.
60
- segmentFormat: fmp4Format,
61
- // Where its encoder has got to, which a run writes into as it works.
62
- progress: { processedSeconds: 0 },
63
- runs: new Set()
64
- };
65
- // The soundtrack of that picture: the same file, published on its own. Found
66
- // by what it is, since no list of ids names it any more.
67
- const sound = {
68
- id: "sound",
69
- spec: outputSpec({ audioOnly: true }),
70
- get outputKey() { return this.spec.toKey(); },
71
- state: "ready",
72
- baseSessionId: "picture",
73
- timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
74
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
75
- // An ordinary session reads its own file, and its sound is inside it. The
76
- // three differ only for a soundtrack shipped as a file of its own — and the
77
- // command is built from all three, so a fixture that states only the first
78
- // describes a session the product cannot make.
79
- get inputFile() { return this.file; },
80
- get audioFile() { return this.file; },
81
- // How its pieces are packaged. The command asks the format where to cut and
82
- // what to name the files, so a session without one is not a session.
83
- segmentFormat: fmp4Format,
84
- // Where its encoder has got to, which a run writes into as it works.
85
- progress: { processedSeconds: 0 },
86
- runs: new Set()
87
- };
88
- const pictureRun = startRunOn(picture, { from: 2 });
89
- const soundRun = startRunOn(sound, { from: 2 });
90
- manager.encodeOrchestrator.adopt(picture.outputKey, pictureRun);
91
- manager.encodeOrchestrator.adopt(sound.outputKey, soundRun);
92
- manager.outputs.set("picture", picture);
93
- manager.outputs.set("sound", sound);
94
-
95
- return { manager, picture, sound };
96
- }
97
-
98
- test("a soundtrack follows the picture to the instant the picture really began", () => {
99
- const { manager, picture, sound } = familyAtBoundaryTwo();
100
- const runBefore = [...sound.runs][0];
101
-
102
- manager.outputTimes.correctBoundaryFromSegment(picture, 2, 10.5);
103
-
104
- assert.deepEqual(
105
- picture.timeline.boundaries,
106
- [0, 4, 10.5, 12, 16, 20],
107
- "the family's table must hold what the file itself said"
108
- );
109
- assert.equal(
110
- sound.timeline.boundaries[2],
111
- 10.5,
112
- "and every member's table with it — one film, one table, nothing to keep in step"
113
- );
114
- // THE RUN AT THE WRONG INSTANT IS STOPPED, and nothing here places its
115
- // replacement. What has been learned is that this run is producing in the
116
- // wrong place, which nothing but the piece it produced could say — the plan
117
- // reasons about numbers and cannot know it. So the fact is acted on as far as
118
- // it is known and no further: the run goes, the stretch it held returns to the
119
- // map, and where the next one stands follows from where the viewers are.
120
- assert.equal(runBefore.state, ENCODE_RUN_STATE.STOPPED, "the run in the wrong place goes");
121
- assert.equal(
122
- manager.encodeOrchestrator.runsOn(sound.outputKey).filter((run) => run.isAlive).length,
123
- 0,
124
- "and this path places nothing"
125
- );
126
- // The corrected instant reaches whatever the plan places there through the
127
- // table, which every session of the file shares. It used to be passed as an
128
- // argument from this one call site, so only a run started by that line ever
129
- // had it — a run the plan placed at the same number landed apart again.
130
- assert.equal(
131
- trueStartOf(sound.timeline, 2),
132
- 10.5,
133
- "the live table holds the measurement and the published one the prediction"
134
- );
135
- });
136
-
137
- test("where a number really begins is answered only where it has been measured", () => {
138
- const { sound } = familyAtBoundaryTwo();
139
-
140
- assert.equal(
141
- trueStartOf(sound.timeline, 2),
142
- undefined,
143
- "the two tables agree, so there is nothing measured to prefer"
144
- );
145
- sound.timeline.boundaries[2] = 10.5;
146
- assert.equal(trueStartOf(sound.timeline, 2), 10.5);
147
- assert.equal(trueStartOf(sound.timeline, 3), undefined, "and only about the number measured");
148
- assert.equal(trueStartOf(sound.timeline, 99), undefined, "a number the file does not have");
149
- assert.equal(trueStartOf(null, 0), undefined, "and no table at all");
150
- });
151
-
152
- test("a correction the table already holds moves nobody", () => {
153
- const { manager, picture, sound } = familyAtBoundaryTwo();
154
- const before = [...sound.timeline.boundaries];
155
- // Within the tolerance: the reading agrees with the table, so there is
156
- // nothing to correct and nothing to move. This is what makes the repositioning
157
- // converge instead of repeating on every produced segment.
158
- manager.outputTimes.correctBoundaryFromSegment(picture, 2, 8.1);
159
- assert.deepEqual(sound.timeline.boundaries, before);
160
- });
161
-
162
- test("a member that is not running is left alone", () => {
163
- const { manager, picture, sound } = familyAtBoundaryTwo();
164
- // A rung the viewer switched away from has no process. Moving it would start
165
- // an encoder for nobody — the failure that put three ffmpeg runs on one file.
166
- const soundRun = [...sound.runs][0];
167
- soundRun.stop("the viewer switched away");
168
- // A stopped run is no longer live, so nothing of this session begins at #2
169
- // any more — which is what "left alone" means here.
170
- manager.outputTimes.correctBoundaryFromSegment(picture, 2, 10.5);
171
- assert.equal(
172
- manager.encodeOrchestrator.runsOn(sound.outputKey).filter((run) => run.isAlive).length,
173
- 0,
174
- "a stopped member is not started again for nobody"
175
- );
176
- assert.equal(soundRun.from, 2, "a stopped member keeps its place and its silence");
177
- assert.equal(soundRun.state, ENCODE_RUN_STATE.STOPPED);
178
- assert.notEqual(INITIAL_RUN_STATE, ENCODE_RUN_STATE.STOPPED);
179
- });
180
-
181
- test("a soundtrack does not move the grid the picture is cut on", () => {
182
- const { manager, picture, sound } = familyAtBoundaryTwo();
183
- const pictureBefore = [...picture.timeline.boundaries];
184
- const soundBefore = [...sound.timeline.boundaries];
185
-
186
- // The sound reports where IT began, which is where it was asked to begin, to
187
- // within one audio frame. The picture's own boundary is a keyframe of the
188
- // file and can be seconds away from that — both readings correct, about
189
- // different things.
190
- //
191
- // Field 2026-08-20: boundary #521 of one film was corrected 2086.084 →
192
- // 2084.082 by the picture and 2084.082 → 2086.033 by the sound 1.6 s later,
193
- // 1.951 s apart, each overwriting the other for as long as the film ran. The
194
- // table never converged, so the guard that stops a correction the table
195
- // already holds never fired.
196
- manager.outputTimes.correctBoundaryFromSegment(sound, 2, 10.5);
197
-
198
- assert.deepEqual(
199
- picture.timeline.boundaries,
200
- pictureBefore,
201
- "the grid is the picture's cut list and a soundtrack may not move it"
202
- );
203
- assert.deepEqual(sound.timeline.boundaries, soundBefore);
204
- assert.equal(
205
- manager.encodeOrchestrator.runsOn(picture.outputKey).filter((run) => run.isAlive).length,
206
- 1,
207
- "and nothing is restarted on a soundtrack's say-so"
208
- );
209
- });
@@ -1,167 +0,0 @@
1
- /**
2
- * @file The tunnel is replaced before anything upstream ends it, and the
3
- * replacement takes over first.
4
- *
5
- * Something between the proxy and the server closes the socket after exactly
6
- * 100 min 15 s whatever is flowing over it — measured across a day of logs on
7
- * 2026-08-20, `code=1006` each time, with a 30 s keepalive running throughout,
8
- * so it is a lifetime cap and not an idle timeout. Reconnecting afterwards
9
- * costs five seconds in which the proxy does not exist as far as the registry
10
- * is concerned, and a viewer arriving then is told there is no proxy.
11
- *
12
- * What is pinned here is the property that removes that window: at no instant
13
- * is the server without a registered connection for this proxy.
14
- */
15
-
16
- import assert from "node:assert/strict";
17
- import test from "node:test";
18
- import { WebSocketServer } from "ws";
19
-
20
- import { createTunnelClient } from "../services/transport/tunnel-client.js";
21
-
22
- /**
23
- * Wait for the thing being asserted, not for a length of time.
24
- *
25
- * A chosen interval followed by an assertion does not test — it samples, and
26
- * what it samples is how busy the machine is. The deadline here is a backstop
27
- * that turns a hang into a failure; it is never the measurement.
28
- *
29
- * @param {() => boolean} until
30
- * @param {string} what - Named in the failure, since a timeout otherwise says
31
- * only that something did not happen.
32
- * @param {number} [limit]
33
- * @returns {Promise<void>}
34
- */
35
- async function waitFor(until, what, limit = 10_000) {
36
- const deadline = Date.now() + limit;
37
- while (!until()) {
38
- if (Date.now() > deadline) {
39
- throw new Error(`${what} never happened`);
40
- }
41
- await new Promise((resolve) => { setTimeout(resolve, 5); });
42
- }
43
- }
44
-
45
- /**
46
- * A stand-in for the registry's tunnel endpoint, with the one behaviour that
47
- * matters here: a new connection for a proxy REPLACES the previous one, which
48
- * is what `registerConnection` does in `server/services/proxy-tunnel-server.js`.
49
- *
50
- * @returns {Promise<{ url: string, close: () => Promise<void>, registered: () => number, opened: () => number, everEmpty: () => boolean }>}
51
- */
52
- async function startRegistry() {
53
- const server = new WebSocketServer({ port: 0 });
54
- await new Promise((resolve) => { server.on("listening", resolve); });
55
- /** @type {import("ws").WebSocket | null} */
56
- let current = null;
57
- let opened = 0;
58
- let everEmpty = false;
59
- /** Every socket ever accepted, so the server can be shut without waiting. */
60
- const accepted = new Set();
61
- server.on("connection", (socket) => {
62
- opened += 1;
63
- accepted.add(socket);
64
- socket.on("close", () => { accepted.delete(socket); });
65
- const previous = current;
66
- current = socket;
67
- // The replacement is registered BEFORE the old one is closed, so a reader
68
- // of `current` never sees nothing.
69
- if (previous && previous.readyState < 2) {
70
- previous.close(1000, "replaced");
71
- }
72
- socket.on("close", () => {
73
- if (current === socket) {
74
- current = null;
75
- everEmpty = true;
76
- }
77
- });
78
- });
79
- const { port } = server.address();
80
- return {
81
- url: `http://127.0.0.1:${port}`,
82
- // `close` waits for every connection to end, and a renewal can leave one
83
- // still closing, so they are ended here rather than waited on.
84
- close: () => new Promise((resolve) => {
85
- for (const socket of accepted) {
86
- socket.terminate();
87
- }
88
- accepted.clear();
89
- server.close(() => resolve());
90
- }),
91
- registered: () => (current && current.readyState === 1 ? 1 : 0),
92
- killCurrent: () => { current?.terminate(); },
93
- opened: () => opened,
94
- everEmpty: () => everEmpty
95
- };
96
- }
97
-
98
- test("the connection is replaced before its lifetime runs out, without a gap", async (t) => {
99
- const registry = await startRegistry();
100
- /** @type {string[]} */
101
- const lines = [];
102
- const client = createTunnelClient({
103
- serverUrl: registry.url,
104
- proxyId: "p1",
105
- token: "t",
106
- proxyPort: 9090,
107
- onLog: (line) => lines.push(line),
108
- // The real cap is 100 min 15 s and the real renewal is at 90 min; the
109
- // ratio is what matters, not the magnitude.
110
- connectionLifetimeMs: 150
111
- });
112
- t.after(async () => {
113
- client.disconnect();
114
- await registry.close();
115
- });
116
-
117
- client.connect();
118
- // Until the renewals have happened, however long this machine takes over
119
- // them. Waited out as 700 ms instead — about four lifetimes — this asserted
120
- // how many the scheduler had got round to.
121
- await waitFor(() => registry.opened() >= 3, "three connections");
122
-
123
- assert.ok(registry.opened() >= 3, `expected several renewals, saw ${registry.opened()}`);
124
- // The property this exists for: the registry was never left with nothing.
125
- assert.equal(registry.everEmpty(), false, "the registry lost its connection at some point");
126
- assert.equal(registry.registered(), 1);
127
- // And the proxy knows the difference between a handover and going down. A
128
- // "Reconnecting in" line here would mean it had treated its own renewal as a
129
- // failure and waited five seconds before coming back.
130
- assert.ok(lines.some((line) => line.includes("Tunnel renewing")), lines.join("\n"));
131
- assert.ok(lines.some((line) => line.includes("Tunnel handed over")), lines.join("\n"));
132
- assert.equal(lines.filter((line) => line.includes("Reconnecting in")).length, 0, lines.join("\n"));
133
- });
134
-
135
- test("a connection killed from outside is still reconnected", async (t) => {
136
- const registry = await startRegistry();
137
- /** @type {string[]} */
138
- const lines = [];
139
- const client = createTunnelClient({
140
- serverUrl: registry.url,
141
- proxyId: "p2",
142
- token: "t",
143
- proxyPort: 9090,
144
- onLog: (line) => lines.push(line),
145
- // Far longer than this test runs, so nothing renews and the only close is
146
- // the one forced below — which is what the upstream cap looks like from
147
- // here: an abrupt end nobody asked for.
148
- connectionLifetimeMs: 60_000
149
- });
150
- t.after(async () => {
151
- client.disconnect();
152
- await registry.close();
153
- });
154
-
155
- client.connect();
156
- await waitFor(() => registry.opened() === 1, "the first connection");
157
- // And exactly one: the lifetime above is far longer than this test runs, so
158
- // nothing renews and a second would mean something else had opened it.
159
- assert.equal(registry.opened(), 1);
160
-
161
- registry.killCurrent();
162
- // The renewal must not have taken the ordinary reconnect away with it.
163
- await waitFor(
164
- () => lines.some((line) => line.includes("Reconnecting in")),
165
- "the reconnect after a connection was killed from outside"
166
- );
167
- });