@torrent-tv/proxy 2.89.16 → 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/routes/api/client-logs/post.js +9 -2
  4. package/server.js +0 -11
  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 -2373
  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 -138
  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/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,131 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { wedgeIsCertain } from "../services/transport/data-channel-handler.js";
5
- import { PROBE_INTERVAL_MS } from "../services/transport/delivery-probe.js";
6
-
7
- const MEGABYTE = 1024 * 1024;
8
-
9
- test("a queue draining at the rate this link was measured at is not a wedge", () => {
10
- // 8 MB at 16 MB/s is half a second of draining. Half a second in is normal.
11
- const verdict = wedgeIsCertain({
12
- queuedBytes: 8 * MEGABYTE,
13
- bytesPerSecond: 16 * MEGABYTE,
14
- flatForMs: 200
15
- });
16
- assert.equal(verdict.certain, false);
17
- });
18
-
19
- test("the counter standing still past the queue's own drain time is a wedge", () => {
20
- const verdict = wedgeIsCertain({
21
- queuedBytes: 8 * MEGABYTE,
22
- bytesPerSecond: 16 * MEGABYTE,
23
- flatForMs: 4000
24
- });
25
- assert.equal(verdict.certain, true);
26
- assert.equal(verdict.needMs, 500);
27
- });
28
-
29
- test("a big queue on a slow link is given the time it genuinely needs", () => {
30
- // 67 MB at 1 MB/s is 67 seconds of honest draining — the field's own numbers
31
- // for the wedged channel, at a rate a thin link could really be running at.
32
- const patient = wedgeIsCertain({
33
- queuedBytes: 67 * MEGABYTE,
34
- bytesPerSecond: MEGABYTE,
35
- flatForMs: 30_000
36
- });
37
- assert.equal(patient.certain, false);
38
- const later = wedgeIsCertain({
39
- queuedBytes: 67 * MEGABYTE,
40
- bytesPerSecond: MEGABYTE,
41
- flatForMs: 70_000
42
- });
43
- assert.equal(later.certain, true);
44
- });
45
-
46
- test("the same queue on the link the field actually had is called quickly", () => {
47
- // 67 MB at 18 MB/s is under four seconds. The field episode stood still for
48
- // 3217 s; the previous rule waited a flat 30 s before recording anything.
49
- const verdict = wedgeIsCertain({
50
- queuedBytes: 67 * MEGABYTE,
51
- bytesPerSecond: 18 * MEGABYTE,
52
- flatForMs: 5000
53
- });
54
- assert.equal(verdict.certain, true);
55
- assert.ok(verdict.needMs < 30_000);
56
- });
57
-
58
- test("a tiny queue still waits for one round of probes", () => {
59
- const verdict = wedgeIsCertain({
60
- queuedBytes: 512,
61
- bytesPerSecond: 16 * MEGABYTE,
62
- flatForMs: PROBE_INTERVAL_MS - 1
63
- });
64
- assert.equal(verdict.certain, false);
65
- assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
66
- });
67
-
68
- test("nothing queued is still a wedge when the counter has been flat long enough — the channel queue was 0 on the real wedge while usrsctp held 399 MB", () => {
69
- const verdict = wedgeIsCertain({
70
- queuedBytes: 0,
71
- bytesPerSecond: 16 * MEGABYTE,
72
- flatForMs: 600_000
73
- });
74
- assert.equal(verdict.certain, true);
75
- assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
76
- });
77
-
78
- test("with no rate measured the answer is that nothing can be said", () => {
79
- const verdict = wedgeIsCertain({
80
- queuedBytes: 8 * MEGABYTE,
81
- bytesPerSecond: 0,
82
- flatForMs: 600_000
83
- });
84
- assert.equal(verdict.certain, false);
85
- assert.equal(verdict.needMs, null);
86
- });
87
-
88
- test("a pause no longer than this link's own longest healthy pause is not a wedge", () => {
89
- // A retransmission timeout stops the accepted-byte counter dead: a full send
90
- // buffer accepts nothing. If this connection has already paused 3 s while
91
- // healthy, a 3 s pause says nothing.
92
- const verdict = wedgeIsCertain({
93
- queuedBytes: 8 * MEGABYTE,
94
- bytesPerSecond: 18 * MEGABYTE,
95
- flatForMs: 2500,
96
- longestHealthyFlatMs: 3000
97
- });
98
- assert.equal(verdict.certain, false);
99
- assert.equal(verdict.needMs, 3000);
100
- });
101
-
102
- test("a pause longer than any this link has shown is a wedge", () => {
103
- const verdict = wedgeIsCertain({
104
- queuedBytes: 8 * MEGABYTE,
105
- bytesPerSecond: 18 * MEGABYTE,
106
- flatForMs: 3500,
107
- longestHealthyFlatMs: 3000
108
- });
109
- assert.equal(verdict.certain, true);
110
- });
111
-
112
- test("the quiet-probe rate cannot be what the queue is divided by", () => {
113
- // With the browser's buffer full, the only traffic is the probe: three
114
- // channels, a few dozen bytes, twice a second. Dividing 8 MB by that gives
115
- // six hours, and the wedge would never be called. The BEST rate seen is what
116
- // the watcher keeps, so this case must not arise — pinned here as the
117
- // arithmetic that made it matter.
118
- const wrong = wedgeIsCertain({
119
- queuedBytes: 8 * MEGABYTE,
120
- bytesPerSecond: 360,
121
- flatForMs: 60_000
122
- });
123
- assert.equal(wrong.certain, false);
124
- assert.ok(wrong.needMs > 6 * 60 * 60 * 1000 - 1);
125
- const right = wedgeIsCertain({
126
- queuedBytes: 8 * MEGABYTE,
127
- bytesPerSecond: 18 * MEGABYTE,
128
- flatForMs: 60_000
129
- });
130
- assert.equal(right.certain, true);
131
- });
@@ -1,30 +0,0 @@
1
- /**
2
- * @file A file kept whole is read from the disk by the subtitle walk.
3
- *
4
- * Once a file is kept whole its pieces leave the torrent's store while the
5
- * bitfield still says they are held, and a read through the torrent waited on
6
- * a store that no longer had them (field 2026-10-01). The reading itself is
7
- * checked here over a temporary file; the client that uses it starts the
8
- * torrent thread and is not built.
9
- */
10
-
11
- import test from "node:test";
12
- import assert from "node:assert/strict";
13
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
14
- import os from "node:os";
15
- import path from "node:path";
16
-
17
- import { readWholeFile } from "../services/torrent/worker/client.js";
18
-
19
- test("a range of a whole file is read from the disk, and a range past it is not here", async (t) => {
20
- const dir = await mkdtemp(path.join(os.tmpdir(), "ttv-whole-"));
21
- t.after(() => rm(dir, { recursive: true, force: true }));
22
- const file = path.join(dir, "film.mkv");
23
- await writeFile(file, Buffer.from("0123456789"));
24
- const whole = { path: file, length: 10, name: "film.mkv" };
25
-
26
- assert.equal((await readWholeFile(whole, 2, 5)).toString(), "2345");
27
- assert.equal((await readWholeFile(whole, 8, 20)).toString(), "89", "clamped to the file");
28
- assert.equal(await readWholeFile(whole, 12, 20), null);
29
- await assert.rejects(readWholeFile({ ...whole, path: path.join(dir, "gone.mkv") }, 0, 1), { code: "ENOENT" });
30
- });
@@ -1,212 +0,0 @@
1
- /**
2
- * @file One owner of the fact "this proxy has piece N".
3
- *
4
- * The store holds the bytes, so it owns the fact. The library keeps a second
5
- * copy of it in its completion bitfield, and until 2026-09-12 nothing
6
- * reconciled them: the disk tier dropped a piece once every reader was past it
7
- * — correctly, that is what bounds the spill — and the bitfield went on saying
8
- * the piece was verified. A read then concluded the piece was had, asked for it,
9
- * was told it was absent, and failed; nothing fetched it again either, because
10
- * the library does not download what it believes it owns. Field: a film played
11
- * 80 seconds and then answered `Piece 0 is verified but absent from the store`
12
- * for 92 minutes.
13
- *
14
- * Pinned here: the announcement and its one rule — said when the piece has gone
15
- * from EVERYWHERE, never when one tier alone lost it — and the withdrawal.
16
- */
17
-
18
- import test from "node:test";
19
- import assert from "node:assert/strict";
20
- import os from "node:os";
21
- import path from "node:path";
22
- import fs from "node:fs/promises";
23
- import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
24
- import { withdrawClaim } from "../services/torrent/download/withdraw-claim.js";
25
-
26
- const PIECE = 1024;
27
-
28
- /**
29
- * A store of four pieces with room for one, so every admission spills the last,
30
- * plus the announcements it made.
31
- */
32
- async function storeWithGone(extras = {}) {
33
- const directory = await fs.mkdtemp(path.join(os.tmpdir(), "withdraw-test-"));
34
- /** @type {number[]} */
35
- const gone = [];
36
- const store = new SharedPieceStore(PIECE, {
37
- length: 4 * PIECE,
38
- memoryBytes: PIECE,
39
- path: directory,
40
- name: "test",
41
- files: [{ offset: 0, length: 4 * PIECE, name: "file.bin" }],
42
- onPieceGone: ({ index }) => gone.push(index),
43
- ...extras
44
- });
45
- const put = (index) => new Promise((resolve, reject) => {
46
- store.put(index, Buffer.alloc(PIECE, index + 1), (error) => (error ? reject(error) : resolve()));
47
- });
48
- const clean = async () => {
49
- store.destroy(() => undefined);
50
- await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
51
- };
52
- return { store, gone, put, clean };
53
- }
54
-
55
- /**
56
- * Wait for the CONDITION, with a deadline only as a backstop. A fixed pause
57
- * here would measure the machine rather than the store.
58
- *
59
- * @param {() => boolean} ready
60
- * @param {string} what
61
- */
62
- async function until(ready, what) {
63
- const deadline = Date.now() + 5_000;
64
- while (!ready()) {
65
- if (Date.now() > deadline) {
66
- throw new Error(`timed out waiting for ${what}`);
67
- }
68
- await new Promise((resolve) => setImmediate(resolve));
69
- }
70
- }
71
-
72
- test("a piece left behind every reader is dropped AND the claim withdrawn", async () => {
73
- const { store, gone, put, clean } = await storeWithGone();
74
- try {
75
- await put(0);
76
- await put(1);
77
- await put(2);
78
- // WHERE THE READERS STAND, which is the whole trigger: the encoder ran
79
- // ahead, so pieces 0-1 are behind every one of them. This is the production
80
- // path — `reviseSpillCeiling` asks `forgetBehind(readHeads)` — and it is
81
- // what dropped 565 pieces in the field.
82
- store.protectRange("reader", 2, 3, 0);
83
- const revision = store.reviseSpillCeiling(null);
84
-
85
- assert.ok(revision.behind >= 1, `something should have been dropped, got ${revision.behind}`);
86
- assert.ok(gone.includes(0), `piece 0 has gone and should say so, got ${JSON.stringify(gone)}`);
87
- assert.ok(
88
- gone.every((index) => index < 2),
89
- `nothing a reader still wants may be announced, got ${JSON.stringify(gone)}`
90
- );
91
- } finally {
92
- await clean();
93
- }
94
- });
95
-
96
- test("a piece dropped as a duplicate of a file held whole is NOT announced", async () => {
97
- const { store, gone, put, clean } = await storeWithGone({
98
- // Every piece can be had from the assembled file, which is exactly why the
99
- // spilled copy is being dropped. Nothing has been lost, so nothing is said.
100
- isPieceElsewhere: () => true
101
- });
102
- try {
103
- await put(0);
104
- await put(1);
105
- await put(2);
106
- // The spill is what puts a piece on disk, and it finishes on its own time;
107
- // dropping duplicates deliberately leaves a piece whose spill is still in
108
- // flight alone, so the precondition is waited for rather than assumed.
109
- await until(() => store.stats().spilled >= 1, "a piece to reach the disk");
110
- const dropped = store.dropDuplicatesHeldElsewhere();
111
- assert.ok(dropped >= 1, "the spilled duplicate should have been dropped");
112
- assert.deepEqual(gone, [], "a piece still readable from a whole file has not gone");
113
- } finally {
114
- await clean();
115
- }
116
- });
117
-
118
- test("a closing store announces nothing, because its torrent is going too", async () => {
119
- const { store, gone, put, clean } = await storeWithGone();
120
- try {
121
- await put(0);
122
- await put(1);
123
- store.protectRange("reader", 2, 3, 0);
124
- store.close(() => undefined);
125
- store.reviseSpillCeiling(null);
126
- assert.deepEqual(gone, [], "a claim withdrawn against a dying torrent reaches nothing useful");
127
- } finally {
128
- await clean();
129
- }
130
- });
131
-
132
- test("the withdrawal is counted in the store's own figures", async () => {
133
- const { store, put, clean } = await storeWithGone();
134
- try {
135
- await put(0);
136
- await put(1);
137
- await put(2);
138
- store.protectRange("reader", 2, 3, 0);
139
- store.reviseSpillCeiling(null);
140
- assert.ok(
141
- store.stats().withdrawn >= 1,
142
- "the figure that makes the eviction's bargain checkable must move"
143
- );
144
- } finally {
145
- await clean();
146
- }
147
- });
148
-
149
- test("a piece the library thinks it has is withdrawn", () => {
150
- const asked = [];
151
- const torrent = {
152
- name: "film.mkv",
153
- destroyed: false,
154
- bitfield: { get: () => true },
155
- _markUnverified: (index) => asked.push(index)
156
- };
157
- assert.equal(withdrawClaim({ index: 3, files: [{ _torrent: torrent }] }), "withdrawn");
158
- assert.deepEqual(asked, [3]);
159
- });
160
-
161
- test("a piece the library already knows is missing is left alone", () => {
162
- let touched = 0;
163
- const torrent = {
164
- destroyed: false,
165
- bitfield: { get: () => false },
166
- _markUnverified: () => { touched += 1; }
167
- };
168
- assert.equal(
169
- withdrawClaim({ index: 7, torrent }),
170
- "nothing-to-withdraw",
171
- "re-creating a piece the library is already fetching would discard its blocks in flight"
172
- );
173
- assert.equal(touched, 0);
174
- });
175
-
176
- test("a destroyed torrent is left alone", () => {
177
- let touched = 0;
178
- const torrent = {
179
- destroyed: true,
180
- bitfield: { get: () => true },
181
- _markUnverified: () => { touched += 1; }
182
- };
183
- assert.equal(withdrawClaim({ index: 1, torrent }), "no-torrent");
184
- assert.equal(touched, 0);
185
- });
186
-
187
- test("a library that refuses says so instead of failing the eviction", () => {
188
- const said = [];
189
- const torrent = {
190
- name: "film.mkv",
191
- destroyed: false,
192
- bitfield: { get: () => true },
193
- _markUnverified: () => { throw new Error("no such method any more"); }
194
- };
195
- assert.equal(withdrawClaim({ index: 2, torrent, warn: (line) => said.push(line) }), "refused");
196
- assert.equal(said.length, 1);
197
- assert.match(said[0], /piece 2 of film\.mkv/);
198
- });
199
-
200
- test("a store with nobody listening evicts exactly as it did before", async () => {
201
- const { store, put, clean } = await storeWithGone({ onPieceGone: undefined });
202
- try {
203
- await put(0);
204
- await put(1);
205
- await put(2);
206
- store.protectRange("reader", 2, 3, 0);
207
- const revision = store.reviseSpillCeiling(null);
208
- assert.ok(revision.behind >= 1, "the eviction does not depend on anybody listening");
209
- } finally {
210
- await clean();
211
- }
212
- });
@@ -1,158 +0,0 @@
1
- /**
2
- * @file The worker transport's memory contract.
3
- *
4
- * These cover the defect that broke playback in 2.9.71-2.9.73: the worker
5
- * handed the main thread ownership of memory it did not own. WebTorrent's piece
6
- * cache keeps the buffer it returns and slices it again on the next read, so
7
- * transferring it detached the cache's own memory and every later read failed
8
- * with "Cannot perform %TypedArray%.prototype.slice on a detached ArrayBuffer".
9
- *
10
- * The field could not tell us this, because the error never reached anyone: the
11
- * worker sent READ_END from its `finally` before the error was posted, and the
12
- * main thread had no handler for a read error at all. So a failed read looked
13
- * exactly like an empty file.
14
- */
15
-
16
- import test from "node:test";
17
- import assert from "node:assert/strict";
18
- import { MessageChannel } from "node:worker_threads";
19
- import { createSendStream, createReceiveStream, createCaller } from "../services/torrent/worker/channel.js";
20
-
21
- /**
22
- * A buffer standing in for one owned by WebTorrent's piece cache: allocated
23
- * outside Node's shared pool, covering its whole region, and still referenced
24
- * by its owner after we hand it on.
25
- *
26
- * @param {number} size
27
- * @param {number} fill
28
- * @returns {Buffer}
29
- */
30
- function foreignPiece(size, fill) {
31
- const piece = Buffer.allocUnsafeSlow(size);
32
- piece.fill(fill);
33
- return piece;
34
- }
35
-
36
- test("sending a chunk leaves the source buffer usable by its owner", async () => {
37
- const { port1, port2 } = new MessageChannel();
38
- try {
39
- const sender = createSendStream({ port: port1, requestId: 1 });
40
- const piece = foreignPiece(1024 * 1024, 7);
41
-
42
- await sender.send(piece);
43
-
44
- // The owner reads its own buffer again, exactly as the piece cache does on
45
- // the next request for the same piece.
46
- assert.equal(piece.length, 1024 * 1024, "buffer was detached by the send");
47
- assert.equal(piece[0], 7);
48
- assert.equal(piece.subarray(0, 16).length, 16, "slicing the source failed");
49
- } finally {
50
- port1.close();
51
- port2.close();
52
- }
53
- });
54
-
55
- test("a second read of the same piece still returns its bytes", async () => {
56
- const { port1, port2 } = new MessageChannel();
57
- try {
58
- const piece = foreignPiece(512 * 1024, 3);
59
-
60
- for (const requestId of [1, 2]) {
61
- const sender = createSendStream({ port: port1, requestId });
62
- await sender.send(piece);
63
- sender.end();
64
- }
65
-
66
- assert.equal(piece[0], 3, "the piece did not survive being sent twice");
67
- } finally {
68
- port1.close();
69
- port2.close();
70
- }
71
- });
72
-
73
- test("chunks arrive with their contents intact", async () => {
74
- const { port1, port2 } = new MessageChannel();
75
- try {
76
- const received = [];
77
- // Waited on rather than slept through. This test used to sleep 50 ms and
78
- // then assert, so it failed whenever the run was busy — recorded as flaky in
79
- // roadmap items 53 and 54, and it failed again on 2026-09-02 for a reason
80
- // that shows exactly how little a chosen interval is worth: the worker had
81
- // gained one more module to load, and 50 ms stopped being enough.
82
- let arrived;
83
- const firstChunk = new Promise((resolve) => { arrived = resolve; });
84
- port2.on("message", (message) => {
85
- if (message?.type === "chunk") {
86
- received.push(Buffer.from(message.bytes));
87
- arrived();
88
- }
89
- });
90
-
91
- const sender = createSendStream({ port: port1, requestId: 1 });
92
- const piece = foreignPiece(256 * 1024, 42);
93
- await sender.send(piece);
94
- sender.end();
95
-
96
- await firstChunk;
97
- assert.equal(received.length, 1);
98
- assert.equal(received[0].length, 256 * 1024);
99
- assert.equal(received[0][0], 42);
100
- assert.equal(received[0][received[0].length - 1], 42);
101
- } finally {
102
- port1.close();
103
- port2.close();
104
- }
105
- });
106
-
107
- test("reads and commands never share a request id", () => {
108
- const { port1, port2 } = new MessageChannel();
109
- try {
110
- const commandIds = [];
111
- port2.on("message", (message) => commandIds.push(message.id));
112
-
113
- const caller = createCaller(port1);
114
- const readIds = [];
115
-
116
- // Interleaved exactly as the real client does it: commands through `call`,
117
- // reads taking an id directly, both over the same channel.
118
- for (let round = 0; round < 50; round += 1) {
119
- void caller.call("noop", {});
120
- readIds.push(caller.nextId());
121
- }
122
-
123
- // A repeat between the two sequences is the collision that let a read's
124
- // reply resolve a command — with its result, silently.
125
- const everyId = [...commandIds, ...readIds];
126
- assert.equal(
127
- new Set(everyId).size,
128
- everyId.length,
129
- "an id was handed out to both a command and a read"
130
- );
131
- } finally {
132
- port1.close();
133
- port2.close();
134
- }
135
- });
136
-
137
- test("a failed read surfaces on the reader instead of ending quietly", async () => {
138
- const { port1, port2 } = new MessageChannel();
139
- try {
140
- const receive = createReceiveStream({
141
- port: port1,
142
- requestId: 1,
143
- onCancel: () => undefined
144
- });
145
-
146
- receive.fail(new Error("read failed in the worker"));
147
-
148
- const reader = receive.stream.getReader();
149
- await assert.rejects(
150
- () => reader.read(),
151
- /read failed in the worker/,
152
- "the reader saw a clean end instead of the failure"
153
- );
154
- } finally {
155
- port1.close();
156
- port2.close();
157
- }
158
- });