@torrent-tv/proxy 2.89.5 → 2.89.7
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.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +338 -338
- package/CHANGELOG.md +16 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -753
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +240 -240
- package/test/helpers/manager.js +62 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +87 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/quality-variants.test.js +1270 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +111 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +508 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +92 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
import { logger } from "../../../../utils/logger.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Return download statistics for a registered torrent source.
|
|
5
|
-
*
|
|
6
|
-
* Provides peer count, transfer speeds, and per-file download progress so
|
|
7
|
-
* that the browser client can display meaningful feedback while the proxy is
|
|
8
|
-
* pre-fetching file metadata (MOOV atom / EBML headers) before codec probing.
|
|
9
|
-
*
|
|
10
|
-
* GET /api/sources/:sourceKey/stats?fileIndex=N
|
|
11
|
-
*
|
|
12
|
-
* @param {import("fastify").FastifyRequest} req
|
|
13
|
-
* @param {import("fastify").FastifyReply} reply
|
|
14
|
-
* @param {{
|
|
15
|
-
* sourceRegistry: ReturnType<import("../../../../store/source-registry.js").createSourceRegistry>,
|
|
16
|
-
* torrentPool: import("../../../../services/torrent/torrent-pool.js").TorrentPool
|
|
17
|
-
* }} deps
|
|
18
|
-
* @returns {Promise<void>}
|
|
19
|
-
*/
|
|
20
|
-
export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torrentPool }) {
|
|
21
|
-
const sourceKey = typeof req.params.sourceKey === "string" ? req.params.sourceKey.trim() : "";
|
|
22
|
-
if (!sourceKey) {
|
|
23
|
-
return reply.code(400).send({ error: "sourceKey is required." });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const sourceRecord = sourceRegistry.get(sourceKey);
|
|
27
|
-
if (!sourceRecord) {
|
|
28
|
-
return reply.code(404).send({ error: "Source key was not found." });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let torrent;
|
|
32
|
-
try {
|
|
33
|
-
// getTorrent resolves immediately when the torrent is already loaded.
|
|
34
|
-
torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
-
logger.warn(`stats: getTorrent failed for ${sourceKey.slice(0, 8)}: ${message} stack=${error instanceof Error ? error.stack?.split("\n")[1]?.trim() ?? "" : ""}`);
|
|
38
|
-
return reply.code(500).send({ error: `Failed to load torrent: ${message}` });
|
|
39
|
-
}
|
|
40
|
-
if (!torrent) {
|
|
41
|
-
logger.warn(`stats: torrent missing for ${sourceKey.slice(0, 8)} (getTorrent returned null/undefined)`);
|
|
42
|
-
return reply.code(500).send({ error: "Torrent instance not found" });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
|
|
46
|
-
const fileIndex = fileIndexRaw !== "" && /^\d+$/.test(fileIndexRaw) ? Number(fileIndexRaw) : null;
|
|
47
|
-
|
|
48
|
-
// Optional: pin the resume window to a FIXED byte offset for the duration of
|
|
49
|
-
// one buffering episode (see getFileStats JSDoc) instead of the live, moving
|
|
50
|
-
// read position — otherwise "bytes needed" can jump up mid-poll as the window
|
|
51
|
-
// slides forward with playback/encoding progress.
|
|
52
|
-
const resumeAnchorRaw = typeof req.query.resumeAnchorByteStart === "string" ? req.query.resumeAnchorByteStart : "";
|
|
53
|
-
const resumeAnchorByteStart = resumeAnchorRaw !== "" && /^\d+$/.test(resumeAnchorRaw) ? Number(resumeAnchorRaw) : null;
|
|
54
|
-
|
|
55
|
-
// Awaited: with the torrent on its own thread this is a round trip, not a
|
|
56
|
-
// local lookup. Without the await the reply was the pending promise itself,
|
|
57
|
-
// which serialises to `{}` — the empty stats seen in the field 2026-08-02.
|
|
58
|
-
const stats = await torrentPool.getFileStats(torrent, fileIndex, { resumeAnchorByteStart });
|
|
59
|
-
|
|
60
|
-
// Diagnostic: surface the real swarm state per poll so a cold-start download
|
|
61
|
-
// stall (0 peers / header not advancing → playback-plan blocks on the codec
|
|
62
|
-
// probe → browser timeout) is visible in the proxy log.
|
|
63
|
-
const downKbps = (stats.downloadSpeed / 1024).toFixed(0);
|
|
64
|
-
const filePct = stats.fileProgress != null ? `${(stats.fileProgress * 100).toFixed(1)}%` : "n/a";
|
|
65
|
-
const header =
|
|
66
|
-
stats.headerBytes != null
|
|
67
|
-
? `${stats.headerDownloadedBytes}/${stats.headerBytes}B`
|
|
68
|
-
: "n/a";
|
|
69
|
-
// The swarm, said so that the question this line is asked can be answered
|
|
70
|
-
// from it. "Connected" and "known" are different numbers and their
|
|
71
|
-
// difference is the diagnosis: a tracker offering five while we are
|
|
72
|
-
// connected to none is a connectivity fault, and nobody offering anything is
|
|
73
|
-
// a supply fault. Until 2026-08-21 the line printed `peers=` beside
|
|
74
|
-
// `wires=?` — two quantities of which one looked unknown, while in truth the
|
|
75
|
-
// first WAS the connection count and the second was a field that never
|
|
76
|
-
// printed anything, because the torrent lives on another thread and the
|
|
77
|
-
// property does not exist on this side of it.
|
|
78
|
-
const known = stats.knownPeers === null || stats.knownPeers === undefined ? "?" : stats.knownPeers;
|
|
79
|
-
const queued = stats.queuedPeers === null || stats.queuedPeers === undefined ? "?" : stats.queuedPeers;
|
|
80
|
-
// The BEST answer any tracker gave, and how many answered — not the most
|
|
81
|
-
// recent one. Trackers answer separately, and a dead one replying `0` after a
|
|
82
|
-
// live one replied `500` would otherwise turn "several offered" into "nobody
|
|
83
|
-
// offered", which is the distinction this whole line exists to make.
|
|
84
|
-
const offered = stats.trackerSeeders === null || stats.trackerSeeders === undefined
|
|
85
|
-
? (stats.trackersAnswered
|
|
86
|
-
// Answered, and none of them knew of anybody — a different state from
|
|
87
|
-
// "no tracker has replied at all", and the line must not read as the
|
|
88
|
-
// second when it means the first.
|
|
89
|
-
? `${stats.trackersAnswered} tracker(s) answered, none reported a count`
|
|
90
|
-
: "no tracker answer yet")
|
|
91
|
-
: `${stats.trackersAnswered ?? "?"} tracker(s) offered up to ${stats.trackerSeeders} seeders ` +
|
|
92
|
-
`${stats.trackerLeechers ?? "?"} leechers`;
|
|
93
|
-
// The wait for a first peer can be the whole of a cold start — 257 s of it,
|
|
94
|
-
// measured — and it was neither counted nor shown.
|
|
95
|
-
const firstPeer = Number.isFinite(stats.secondsToFirstPeer)
|
|
96
|
-
? ` firstPeerAfter=${stats.secondsToFirstPeer.toFixed(1)}s`
|
|
97
|
-
: Number.isFinite(stats.secondsWaitingForFirstPeer)
|
|
98
|
-
? ` noPeerFor=${stats.secondsWaitingForFirstPeer.toFixed(1)}s`
|
|
99
|
-
: "";
|
|
100
|
-
// When the answer is empty, say WHICH thing is missing. Field 2026-08-05: a
|
|
101
|
-
// source reported `peers=0 file=n/a header=n/a` for minutes while that very
|
|
102
|
-
// torrent was announcing to trackers with hundreds of seeders — and the line
|
|
103
|
-
// above cannot tell apart "the torrent handle is not the live one", "the file
|
|
104
|
-
// index did not resolve" and "no file index was asked for". That is what the
|
|
105
|
-
// viewer's loading screen was showing at the time, so it has to be
|
|
106
|
-
// answerable from the log rather than by reasoning about it afterwards.
|
|
107
|
-
const emptyAnswer = stats.fileProgress == null || (stats.numPeers === 0 && torrent.done !== true);
|
|
108
|
-
const detail = emptyAnswer
|
|
109
|
-
? ` | files=${torrent.files?.length ?? "?"}` +
|
|
110
|
-
` fileIndex=${fileIndex ?? "none"} resolved=${torrent.files?.[fileIndex ?? -1] ? "yes" : "no"}` +
|
|
111
|
-
` done=${torrent.done === true}`
|
|
112
|
-
: "";
|
|
113
|
-
// The infohash is on EVERY line, not only on the ones that look empty: it is
|
|
114
|
-
// the one identifier the pool's own lines, the worker's and this one share,
|
|
115
|
-
// and a line that carries it can be lined up with them without a guess.
|
|
116
|
-
// WHAT WE GAVE, which no line has ever carried. Without it "the reads are the
|
|
117
|
-
// swarm being served" is an inference from two numbers agreeing, and on
|
|
118
|
-
// 2026-09-11 it was the whole explanation of a store that read 63 416 times
|
|
119
|
-
// with nothing encoding: 596 peers, a complete file, and an upload nobody
|
|
120
|
-
// needed.
|
|
121
|
-
const uploaded = Number.isFinite(torrent.uploaded) ? Math.round(torrent.uploaded / 1048576) : "?";
|
|
122
|
-
const upKbps = Number.isFinite(torrent.uploadSpeed) ? Math.round(torrent.uploadSpeed / 1024) : "?";
|
|
123
|
-
logger.info(
|
|
124
|
-
`[stats] ${sourceKey.slice(0, 8)} ${String(torrent.infoHash).slice(0, 8)} ` +
|
|
125
|
-
`peers=${stats.connectedPeers ?? stats.numPeers} connected of ${known} known ` +
|
|
126
|
-
`(${stats.deliveringPeers ?? "?"} delivering, ${queued} queued, ${offered})` +
|
|
127
|
-
`${firstPeer} down=${downKbps}KB/s up=${upKbps}KB/s gave=${uploaded}MB ` +
|
|
128
|
-
`file=${filePct} header=${header}${detail}`
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
return reply.send(stats);
|
|
132
|
-
}
|
|
1
|
+
import { logger } from "../../../../utils/logger.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return download statistics for a registered torrent source.
|
|
5
|
+
*
|
|
6
|
+
* Provides peer count, transfer speeds, and per-file download progress so
|
|
7
|
+
* that the browser client can display meaningful feedback while the proxy is
|
|
8
|
+
* pre-fetching file metadata (MOOV atom / EBML headers) before codec probing.
|
|
9
|
+
*
|
|
10
|
+
* GET /api/sources/:sourceKey/stats?fileIndex=N
|
|
11
|
+
*
|
|
12
|
+
* @param {import("fastify").FastifyRequest} req
|
|
13
|
+
* @param {import("fastify").FastifyReply} reply
|
|
14
|
+
* @param {{
|
|
15
|
+
* sourceRegistry: ReturnType<import("../../../../store/source-registry.js").createSourceRegistry>,
|
|
16
|
+
* torrentPool: import("../../../../services/torrent/torrent-pool.js").TorrentPool
|
|
17
|
+
* }} deps
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torrentPool }) {
|
|
21
|
+
const sourceKey = typeof req.params.sourceKey === "string" ? req.params.sourceKey.trim() : "";
|
|
22
|
+
if (!sourceKey) {
|
|
23
|
+
return reply.code(400).send({ error: "sourceKey is required." });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sourceRecord = sourceRegistry.get(sourceKey);
|
|
27
|
+
if (!sourceRecord) {
|
|
28
|
+
return reply.code(404).send({ error: "Source key was not found." });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let torrent;
|
|
32
|
+
try {
|
|
33
|
+
// getTorrent resolves immediately when the torrent is already loaded.
|
|
34
|
+
torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
logger.warn(`stats: getTorrent failed for ${sourceKey.slice(0, 8)}: ${message} stack=${error instanceof Error ? error.stack?.split("\n")[1]?.trim() ?? "" : ""}`);
|
|
38
|
+
return reply.code(500).send({ error: `Failed to load torrent: ${message}` });
|
|
39
|
+
}
|
|
40
|
+
if (!torrent) {
|
|
41
|
+
logger.warn(`stats: torrent missing for ${sourceKey.slice(0, 8)} (getTorrent returned null/undefined)`);
|
|
42
|
+
return reply.code(500).send({ error: "Torrent instance not found" });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
|
|
46
|
+
const fileIndex = fileIndexRaw !== "" && /^\d+$/.test(fileIndexRaw) ? Number(fileIndexRaw) : null;
|
|
47
|
+
|
|
48
|
+
// Optional: pin the resume window to a FIXED byte offset for the duration of
|
|
49
|
+
// one buffering episode (see getFileStats JSDoc) instead of the live, moving
|
|
50
|
+
// read position — otherwise "bytes needed" can jump up mid-poll as the window
|
|
51
|
+
// slides forward with playback/encoding progress.
|
|
52
|
+
const resumeAnchorRaw = typeof req.query.resumeAnchorByteStart === "string" ? req.query.resumeAnchorByteStart : "";
|
|
53
|
+
const resumeAnchorByteStart = resumeAnchorRaw !== "" && /^\d+$/.test(resumeAnchorRaw) ? Number(resumeAnchorRaw) : null;
|
|
54
|
+
|
|
55
|
+
// Awaited: with the torrent on its own thread this is a round trip, not a
|
|
56
|
+
// local lookup. Without the await the reply was the pending promise itself,
|
|
57
|
+
// which serialises to `{}` — the empty stats seen in the field 2026-08-02.
|
|
58
|
+
const stats = await torrentPool.getFileStats(torrent, fileIndex, { resumeAnchorByteStart });
|
|
59
|
+
|
|
60
|
+
// Diagnostic: surface the real swarm state per poll so a cold-start download
|
|
61
|
+
// stall (0 peers / header not advancing → playback-plan blocks on the codec
|
|
62
|
+
// probe → browser timeout) is visible in the proxy log.
|
|
63
|
+
const downKbps = (stats.downloadSpeed / 1024).toFixed(0);
|
|
64
|
+
const filePct = stats.fileProgress != null ? `${(stats.fileProgress * 100).toFixed(1)}%` : "n/a";
|
|
65
|
+
const header =
|
|
66
|
+
stats.headerBytes != null
|
|
67
|
+
? `${stats.headerDownloadedBytes}/${stats.headerBytes}B`
|
|
68
|
+
: "n/a";
|
|
69
|
+
// The swarm, said so that the question this line is asked can be answered
|
|
70
|
+
// from it. "Connected" and "known" are different numbers and their
|
|
71
|
+
// difference is the diagnosis: a tracker offering five while we are
|
|
72
|
+
// connected to none is a connectivity fault, and nobody offering anything is
|
|
73
|
+
// a supply fault. Until 2026-08-21 the line printed `peers=` beside
|
|
74
|
+
// `wires=?` — two quantities of which one looked unknown, while in truth the
|
|
75
|
+
// first WAS the connection count and the second was a field that never
|
|
76
|
+
// printed anything, because the torrent lives on another thread and the
|
|
77
|
+
// property does not exist on this side of it.
|
|
78
|
+
const known = stats.knownPeers === null || stats.knownPeers === undefined ? "?" : stats.knownPeers;
|
|
79
|
+
const queued = stats.queuedPeers === null || stats.queuedPeers === undefined ? "?" : stats.queuedPeers;
|
|
80
|
+
// The BEST answer any tracker gave, and how many answered — not the most
|
|
81
|
+
// recent one. Trackers answer separately, and a dead one replying `0` after a
|
|
82
|
+
// live one replied `500` would otherwise turn "several offered" into "nobody
|
|
83
|
+
// offered", which is the distinction this whole line exists to make.
|
|
84
|
+
const offered = stats.trackerSeeders === null || stats.trackerSeeders === undefined
|
|
85
|
+
? (stats.trackersAnswered
|
|
86
|
+
// Answered, and none of them knew of anybody — a different state from
|
|
87
|
+
// "no tracker has replied at all", and the line must not read as the
|
|
88
|
+
// second when it means the first.
|
|
89
|
+
? `${stats.trackersAnswered} tracker(s) answered, none reported a count`
|
|
90
|
+
: "no tracker answer yet")
|
|
91
|
+
: `${stats.trackersAnswered ?? "?"} tracker(s) offered up to ${stats.trackerSeeders} seeders ` +
|
|
92
|
+
`${stats.trackerLeechers ?? "?"} leechers`;
|
|
93
|
+
// The wait for a first peer can be the whole of a cold start — 257 s of it,
|
|
94
|
+
// measured — and it was neither counted nor shown.
|
|
95
|
+
const firstPeer = Number.isFinite(stats.secondsToFirstPeer)
|
|
96
|
+
? ` firstPeerAfter=${stats.secondsToFirstPeer.toFixed(1)}s`
|
|
97
|
+
: Number.isFinite(stats.secondsWaitingForFirstPeer)
|
|
98
|
+
? ` noPeerFor=${stats.secondsWaitingForFirstPeer.toFixed(1)}s`
|
|
99
|
+
: "";
|
|
100
|
+
// When the answer is empty, say WHICH thing is missing. Field 2026-08-05: a
|
|
101
|
+
// source reported `peers=0 file=n/a header=n/a` for minutes while that very
|
|
102
|
+
// torrent was announcing to trackers with hundreds of seeders — and the line
|
|
103
|
+
// above cannot tell apart "the torrent handle is not the live one", "the file
|
|
104
|
+
// index did not resolve" and "no file index was asked for". That is what the
|
|
105
|
+
// viewer's loading screen was showing at the time, so it has to be
|
|
106
|
+
// answerable from the log rather than by reasoning about it afterwards.
|
|
107
|
+
const emptyAnswer = stats.fileProgress == null || (stats.numPeers === 0 && torrent.done !== true);
|
|
108
|
+
const detail = emptyAnswer
|
|
109
|
+
? ` | files=${torrent.files?.length ?? "?"}` +
|
|
110
|
+
` fileIndex=${fileIndex ?? "none"} resolved=${torrent.files?.[fileIndex ?? -1] ? "yes" : "no"}` +
|
|
111
|
+
` done=${torrent.done === true}`
|
|
112
|
+
: "";
|
|
113
|
+
// The infohash is on EVERY line, not only on the ones that look empty: it is
|
|
114
|
+
// the one identifier the pool's own lines, the worker's and this one share,
|
|
115
|
+
// and a line that carries it can be lined up with them without a guess.
|
|
116
|
+
// WHAT WE GAVE, which no line has ever carried. Without it "the reads are the
|
|
117
|
+
// swarm being served" is an inference from two numbers agreeing, and on
|
|
118
|
+
// 2026-09-11 it was the whole explanation of a store that read 63 416 times
|
|
119
|
+
// with nothing encoding: 596 peers, a complete file, and an upload nobody
|
|
120
|
+
// needed.
|
|
121
|
+
const uploaded = Number.isFinite(torrent.uploaded) ? Math.round(torrent.uploaded / 1048576) : "?";
|
|
122
|
+
const upKbps = Number.isFinite(torrent.uploadSpeed) ? Math.round(torrent.uploadSpeed / 1024) : "?";
|
|
123
|
+
logger.info(
|
|
124
|
+
`[stats] ${sourceKey.slice(0, 8)} ${String(torrent.infoHash).slice(0, 8)} ` +
|
|
125
|
+
`peers=${stats.connectedPeers ?? stats.numPeers} connected of ${known} known ` +
|
|
126
|
+
`(${stats.deliveringPeers ?? "?"} delivering, ${queued} queued, ${offered})` +
|
|
127
|
+
`${firstPeer} down=${downKbps}KB/s up=${upKbps}KB/s gave=${uploaded}MB ` +
|
|
128
|
+
`file=${filePct} header=${header}${detail}`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return reply.send(stats);
|
|
132
|
+
}
|