@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,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
- });