@torrent-tv/proxy 2.89.1 → 2.89.2
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 +6 -1
- package/CHANGELOG.md +2299 -2288
- 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 +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +42 -8
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- 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 +380 -380
- 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/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 +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- 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 +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- 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 +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- 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/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- 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-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +476 -458
- package/services/encode/segment-formats/index.js +103 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +604 -541
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +790 -786
- 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 +1024 -1020
- 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/segment-store/SegmentStore.js +1255 -1255
- 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 +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +836 -836
- 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 -0
- package/services/{server → viewer}/playback-readiness.js +635 -516
- 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 -70
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- 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 +462 -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 +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- 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 +46 -46
- 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/fmp4-presentation-clock.test.js +146 -162
- 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 +136 -136
- package/test/helpers/manager.js +57 -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 +112 -112
- 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 +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -0
- 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 +206 -206
- 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 +88 -88
- 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-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 -483
- package/test/playback-start-regressions.test.js +101 -99
- 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 +162 -162
- package/test/quality-variants.test.js +1257 -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 +65 -65
- package/test/same-height-switch.test.js +664 -664
- 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-closed-on-both-branches.test.js +96 -96
- 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 +221 -221
- 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 +105 -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 +209 -209
- 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 +177 -177
- 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 +504 -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 +89 -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/whole-file-held-read.test.js +30 -30
- 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,107 +1,107 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How much of a machine's resource the stores may hold between them.
|
|
3
|
-
*
|
|
4
|
-
* Written once and used twice, because memory and disk are the same question
|
|
5
|
-
* asked of two resources: what is free now, plus what we already hold, less
|
|
6
|
-
* what everything that is not us has recently been seen to need.
|
|
7
|
-
*
|
|
8
|
-
* It used to exist only for memory, inside the memory store, keyed on module
|
|
9
|
-
* state. Disk had no such rule at all — the spill file grew until the machine
|
|
10
|
-
* did, 14.4 GB in a single viewing on 2026-08-31 — and copying the memory rule
|
|
11
|
-
* across would have made two rules to keep in step. One rule, two readings.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* How many observations of other processes' demand are kept.
|
|
16
|
-
*
|
|
17
|
-
* A window rather than a high-water: a single spike would otherwise stand for
|
|
18
|
-
* the life of the process and hold the stores down long after whatever caused
|
|
19
|
-
* it had finished.
|
|
20
|
-
*/
|
|
21
|
-
const OTHER_DEMAND_SAMPLES = 60;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* What everything that is not us has recently been seen to need.
|
|
25
|
-
*
|
|
26
|
-
* One instance per resource. A fall in what is free that we did not cause is a
|
|
27
|
-
* measurement of somebody else's demand; a fall we did cause is our own doing
|
|
28
|
-
* and says nothing about the machine.
|
|
29
|
-
*/
|
|
30
|
-
export class OtherDemand {
|
|
31
|
-
/** @type {number[]} */
|
|
32
|
-
#falls = [];
|
|
33
|
-
|
|
34
|
-
#lastFreeBytes = 0;
|
|
35
|
-
|
|
36
|
-
#lastHeldBytes = 0;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Take a reading, and answer what to reserve for others.
|
|
40
|
-
*
|
|
41
|
-
* @param {number} freeBytes - What the machine says is free now.
|
|
42
|
-
* @param {number} heldBytes - What the stores hold of this resource now.
|
|
43
|
-
* @returns {number} The reserve, in bytes.
|
|
44
|
-
*/
|
|
45
|
-
note(freeBytes, heldBytes) {
|
|
46
|
-
if (this.#lastFreeBytes > 0) {
|
|
47
|
-
const fell = this.#lastFreeBytes - freeBytes;
|
|
48
|
-
const ours = heldBytes - this.#lastHeldBytes;
|
|
49
|
-
this.#falls.push(Math.max(0, fell - ours));
|
|
50
|
-
if (this.#falls.length > OTHER_DEMAND_SAMPLES) {
|
|
51
|
-
this.#falls.shift();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
this.#lastFreeBytes = freeBytes;
|
|
55
|
-
this.#lastHeldBytes = heldBytes;
|
|
56
|
-
return this.reserve();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @returns {number} */
|
|
60
|
-
reserve() {
|
|
61
|
-
return this.#falls.length === 0 ? 0 : Math.max(...this.#falls);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Forget the readings. For tests, which share a module. */
|
|
65
|
-
forget() {
|
|
66
|
-
this.#falls = [];
|
|
67
|
-
this.#lastFreeBytes = 0;
|
|
68
|
-
this.#lastHeldBytes = 0;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* How much of the resource the stores may hold between them.
|
|
74
|
-
*
|
|
75
|
-
* What the machine reports free is what could be taken ON TOP of what is
|
|
76
|
-
* already held, so the stores' own bytes are added back: the pair is the
|
|
77
|
-
* ceiling the stores could reach.
|
|
78
|
-
*
|
|
79
|
-
* @param {number} freeBytes
|
|
80
|
-
* @param {number} heldBytes
|
|
81
|
-
* @param {number} reserveBytes
|
|
82
|
-
* @returns {number}
|
|
83
|
-
*/
|
|
84
|
-
export function machineAllowanceBytes(freeBytes, heldBytes, reserveBytes) {
|
|
85
|
-
return Math.max(0, Math.max(freeBytes, 0) + Math.max(heldBytes, 0) - Math.max(reserveBytes, 0));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Divide what the machine allows between the stores, by what each is asking
|
|
90
|
-
* for.
|
|
91
|
-
*
|
|
92
|
-
* When everyone's ask fits, everyone gets it and the machine's limit never
|
|
93
|
-
* binds. When the asks do not fit, each store is cut in proportion to what it
|
|
94
|
-
* asked, so a store wanting little is not cut to make room for one wanting
|
|
95
|
-
* much.
|
|
96
|
-
*
|
|
97
|
-
* @param {number[]} wantedBytes - What each store is asking for, in order.
|
|
98
|
-
* @param {number} allowanceBytes
|
|
99
|
-
* @returns {number[]} What each store may hold, in the same order.
|
|
100
|
-
*/
|
|
101
|
-
export function divideAllowance(wantedBytes, allowanceBytes) {
|
|
102
|
-
const total = wantedBytes.reduce((sum, want) => sum + Math.max(0, want), 0);
|
|
103
|
-
if (total <= allowanceBytes || total === 0) {
|
|
104
|
-
return wantedBytes.map((want) => Math.max(0, want));
|
|
105
|
-
}
|
|
106
|
-
return wantedBytes.map((want) => Math.floor(allowanceBytes * (Math.max(0, want) / total)));
|
|
107
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file How much of a machine's resource the stores may hold between them.
|
|
3
|
+
*
|
|
4
|
+
* Written once and used twice, because memory and disk are the same question
|
|
5
|
+
* asked of two resources: what is free now, plus what we already hold, less
|
|
6
|
+
* what everything that is not us has recently been seen to need.
|
|
7
|
+
*
|
|
8
|
+
* It used to exist only for memory, inside the memory store, keyed on module
|
|
9
|
+
* state. Disk had no such rule at all — the spill file grew until the machine
|
|
10
|
+
* did, 14.4 GB in a single viewing on 2026-08-31 — and copying the memory rule
|
|
11
|
+
* across would have made two rules to keep in step. One rule, two readings.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How many observations of other processes' demand are kept.
|
|
16
|
+
*
|
|
17
|
+
* A window rather than a high-water: a single spike would otherwise stand for
|
|
18
|
+
* the life of the process and hold the stores down long after whatever caused
|
|
19
|
+
* it had finished.
|
|
20
|
+
*/
|
|
21
|
+
const OTHER_DEMAND_SAMPLES = 60;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What everything that is not us has recently been seen to need.
|
|
25
|
+
*
|
|
26
|
+
* One instance per resource. A fall in what is free that we did not cause is a
|
|
27
|
+
* measurement of somebody else's demand; a fall we did cause is our own doing
|
|
28
|
+
* and says nothing about the machine.
|
|
29
|
+
*/
|
|
30
|
+
export class OtherDemand {
|
|
31
|
+
/** @type {number[]} */
|
|
32
|
+
#falls = [];
|
|
33
|
+
|
|
34
|
+
#lastFreeBytes = 0;
|
|
35
|
+
|
|
36
|
+
#lastHeldBytes = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Take a reading, and answer what to reserve for others.
|
|
40
|
+
*
|
|
41
|
+
* @param {number} freeBytes - What the machine says is free now.
|
|
42
|
+
* @param {number} heldBytes - What the stores hold of this resource now.
|
|
43
|
+
* @returns {number} The reserve, in bytes.
|
|
44
|
+
*/
|
|
45
|
+
note(freeBytes, heldBytes) {
|
|
46
|
+
if (this.#lastFreeBytes > 0) {
|
|
47
|
+
const fell = this.#lastFreeBytes - freeBytes;
|
|
48
|
+
const ours = heldBytes - this.#lastHeldBytes;
|
|
49
|
+
this.#falls.push(Math.max(0, fell - ours));
|
|
50
|
+
if (this.#falls.length > OTHER_DEMAND_SAMPLES) {
|
|
51
|
+
this.#falls.shift();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.#lastFreeBytes = freeBytes;
|
|
55
|
+
this.#lastHeldBytes = heldBytes;
|
|
56
|
+
return this.reserve();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @returns {number} */
|
|
60
|
+
reserve() {
|
|
61
|
+
return this.#falls.length === 0 ? 0 : Math.max(...this.#falls);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Forget the readings. For tests, which share a module. */
|
|
65
|
+
forget() {
|
|
66
|
+
this.#falls = [];
|
|
67
|
+
this.#lastFreeBytes = 0;
|
|
68
|
+
this.#lastHeldBytes = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How much of the resource the stores may hold between them.
|
|
74
|
+
*
|
|
75
|
+
* What the machine reports free is what could be taken ON TOP of what is
|
|
76
|
+
* already held, so the stores' own bytes are added back: the pair is the
|
|
77
|
+
* ceiling the stores could reach.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} freeBytes
|
|
80
|
+
* @param {number} heldBytes
|
|
81
|
+
* @param {number} reserveBytes
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
export function machineAllowanceBytes(freeBytes, heldBytes, reserveBytes) {
|
|
85
|
+
return Math.max(0, Math.max(freeBytes, 0) + Math.max(heldBytes, 0) - Math.max(reserveBytes, 0));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Divide what the machine allows between the stores, by what each is asking
|
|
90
|
+
* for.
|
|
91
|
+
*
|
|
92
|
+
* When everyone's ask fits, everyone gets it and the machine's limit never
|
|
93
|
+
* binds. When the asks do not fit, each store is cut in proportion to what it
|
|
94
|
+
* asked, so a store wanting little is not cut to make room for one wanting
|
|
95
|
+
* much.
|
|
96
|
+
*
|
|
97
|
+
* @param {number[]} wantedBytes - What each store is asking for, in order.
|
|
98
|
+
* @param {number} allowanceBytes
|
|
99
|
+
* @returns {number[]} What each store may hold, in the same order.
|
|
100
|
+
*/
|
|
101
|
+
export function divideAllowance(wantedBytes, allowanceBytes) {
|
|
102
|
+
const total = wantedBytes.reduce((sum, want) => sum + Math.max(0, want), 0);
|
|
103
|
+
if (total <= allowanceBytes || total === 0) {
|
|
104
|
+
return wantedBytes.map((want) => Math.max(0, want));
|
|
105
|
+
}
|
|
106
|
+
return wantedBytes.map((want) => Math.floor(allowanceBytes * (Math.max(0, want) / total)));
|
|
107
|
+
}
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Keep the last few core dumps and no more.
|
|
3
|
-
*
|
|
4
|
-
* A native fault on the torrent worker writes the whole address space out: on
|
|
5
|
-
* the field host that is **4.18 GB each**, and four of them had nearly filled a
|
|
6
|
-
* 235 GB disk by 2026-08-21. The dumps are worth having — the one read that day
|
|
7
|
-
* named a fault three days of reasoning had not — but only the recent ones are,
|
|
8
|
-
* and a full disk costs more than an old dump is worth.
|
|
9
|
-
*
|
|
10
|
-
* Deliberately not "delete them all": the newest are the evidence for the fault
|
|
11
|
-
* that is still open (roadmap item 7).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readdir, rm, stat } from "node:fs/promises";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
|
|
17
|
-
import { logger } from "../../utils/logger.js";
|
|
18
|
-
|
|
19
|
-
/** How many to keep, newest first. */
|
|
20
|
-
export const CORE_DUMPS_KEPT = 2;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Which dumps to remove, given what is there.
|
|
24
|
-
*
|
|
25
|
-
* Pure, so the rule can be tested without a filesystem: newest first by the
|
|
26
|
-
* time they were written, keep `keep`, name the rest.
|
|
27
|
-
*
|
|
28
|
-
* @param {Array<{ name: string, writtenAt: number }>} dumps
|
|
29
|
-
* @param {number} [keep]
|
|
30
|
-
* @returns {string[]} Names to delete, oldest first.
|
|
31
|
-
*/
|
|
32
|
-
export function dumpsToRemove(dumps, keep = CORE_DUMPS_KEPT) {
|
|
33
|
-
const sorted = [...(Array.isArray(dumps) ? dumps : [])]
|
|
34
|
-
.filter((dump) => typeof dump?.name === "string" && Number.isFinite(dump?.writtenAt))
|
|
35
|
-
.sort((left, right) => right.writtenAt - left.writtenAt);
|
|
36
|
-
return sorted.slice(Math.max(0, keep)).map((dump) => dump.name).reverse();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Whether a file name is a core dump this host wrote.
|
|
41
|
-
*
|
|
42
|
-
* The kernel's pattern on the addon host produces `core.<thread>.<pid>.<epoch>`
|
|
43
|
-
* — every one seen so far is `core.WorkerThread.81.…`, the thread the torrent
|
|
44
|
-
* client runs on. Matched loosely on the `core.` prefix so a differently
|
|
45
|
-
* configured host is still swept.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} name
|
|
48
|
-
* @returns {boolean}
|
|
49
|
-
*/
|
|
50
|
-
export function isCoreDump(name) {
|
|
51
|
-
return typeof name === "string" && /^core\.[^/\\]+$/.test(name);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Delete all but the newest few core dumps in `dir`.
|
|
56
|
-
*
|
|
57
|
-
* Best-effort and never fatal: a proxy that cannot tidy its dumps still has to
|
|
58
|
-
* serve video.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} dir
|
|
61
|
-
* @param {number} [keep]
|
|
62
|
-
* @returns {Promise<void>}
|
|
63
|
-
*/
|
|
64
|
-
export async function pruneCoreDumps(dir, keep = CORE_DUMPS_KEPT) {
|
|
65
|
-
if (typeof dir !== "string" || dir.length === 0) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
/** @type {Array<{ name: string, writtenAt: number, bytes: number }>} */
|
|
69
|
-
const dumps = [];
|
|
70
|
-
try {
|
|
71
|
-
for (const name of await readdir(dir)) {
|
|
72
|
-
if (!isCoreDump(name)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
const info = await stat(path.join(dir, name));
|
|
77
|
-
if (info.isFile()) {
|
|
78
|
-
dumps.push({ name, writtenAt: info.mtimeMs, bytes: info.size });
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
// silent-ok: it went away between listing and reading, which is the
|
|
82
|
-
// outcome this function wanted anyway.
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
return; // No such directory, or unreadable. Nothing to tidy.
|
|
87
|
-
}
|
|
88
|
-
if (dumps.length === 0) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const doomed = dumpsToRemove(dumps, keep);
|
|
92
|
-
const freed = dumps
|
|
93
|
-
.filter((dump) => doomed.includes(dump.name))
|
|
94
|
-
.reduce((total, dump) => total + dump.bytes, 0);
|
|
95
|
-
for (const name of doomed) {
|
|
96
|
-
try {
|
|
97
|
-
await rm(path.join(dir, name), { force: true });
|
|
98
|
-
} catch {
|
|
99
|
-
// silent-ok: best effort, and the next start tries again.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
logger.info(
|
|
103
|
-
`core dumps: ${dumps.length} present, keeping the newest ${Math.min(keep, dumps.length)}` +
|
|
104
|
-
(doomed.length > 0 ? `, removed ${doomed.length} (${(freed / 1e9).toFixed(2)} GB)` : "")
|
|
105
|
-
);
|
|
106
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Keep the last few core dumps and no more.
|
|
3
|
+
*
|
|
4
|
+
* A native fault on the torrent worker writes the whole address space out: on
|
|
5
|
+
* the field host that is **4.18 GB each**, and four of them had nearly filled a
|
|
6
|
+
* 235 GB disk by 2026-08-21. The dumps are worth having — the one read that day
|
|
7
|
+
* named a fault three days of reasoning had not — but only the recent ones are,
|
|
8
|
+
* and a full disk costs more than an old dump is worth.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately not "delete them all": the newest are the evidence for the fault
|
|
11
|
+
* that is still open (roadmap item 7).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { logger } from "../../utils/logger.js";
|
|
18
|
+
|
|
19
|
+
/** How many to keep, newest first. */
|
|
20
|
+
export const CORE_DUMPS_KEPT = 2;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Which dumps to remove, given what is there.
|
|
24
|
+
*
|
|
25
|
+
* Pure, so the rule can be tested without a filesystem: newest first by the
|
|
26
|
+
* time they were written, keep `keep`, name the rest.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array<{ name: string, writtenAt: number }>} dumps
|
|
29
|
+
* @param {number} [keep]
|
|
30
|
+
* @returns {string[]} Names to delete, oldest first.
|
|
31
|
+
*/
|
|
32
|
+
export function dumpsToRemove(dumps, keep = CORE_DUMPS_KEPT) {
|
|
33
|
+
const sorted = [...(Array.isArray(dumps) ? dumps : [])]
|
|
34
|
+
.filter((dump) => typeof dump?.name === "string" && Number.isFinite(dump?.writtenAt))
|
|
35
|
+
.sort((left, right) => right.writtenAt - left.writtenAt);
|
|
36
|
+
return sorted.slice(Math.max(0, keep)).map((dump) => dump.name).reverse();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a file name is a core dump this host wrote.
|
|
41
|
+
*
|
|
42
|
+
* The kernel's pattern on the addon host produces `core.<thread>.<pid>.<epoch>`
|
|
43
|
+
* — every one seen so far is `core.WorkerThread.81.…`, the thread the torrent
|
|
44
|
+
* client runs on. Matched loosely on the `core.` prefix so a differently
|
|
45
|
+
* configured host is still swept.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function isCoreDump(name) {
|
|
51
|
+
return typeof name === "string" && /^core\.[^/\\]+$/.test(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Delete all but the newest few core dumps in `dir`.
|
|
56
|
+
*
|
|
57
|
+
* Best-effort and never fatal: a proxy that cannot tidy its dumps still has to
|
|
58
|
+
* serve video.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} dir
|
|
61
|
+
* @param {number} [keep]
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export async function pruneCoreDumps(dir, keep = CORE_DUMPS_KEPT) {
|
|
65
|
+
if (typeof dir !== "string" || dir.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
/** @type {Array<{ name: string, writtenAt: number, bytes: number }>} */
|
|
69
|
+
const dumps = [];
|
|
70
|
+
try {
|
|
71
|
+
for (const name of await readdir(dir)) {
|
|
72
|
+
if (!isCoreDump(name)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const info = await stat(path.join(dir, name));
|
|
77
|
+
if (info.isFile()) {
|
|
78
|
+
dumps.push({ name, writtenAt: info.mtimeMs, bytes: info.size });
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// silent-ok: it went away between listing and reading, which is the
|
|
82
|
+
// outcome this function wanted anyway.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return; // No such directory, or unreadable. Nothing to tidy.
|
|
87
|
+
}
|
|
88
|
+
if (dumps.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const doomed = dumpsToRemove(dumps, keep);
|
|
92
|
+
const freed = dumps
|
|
93
|
+
.filter((dump) => doomed.includes(dump.name))
|
|
94
|
+
.reduce((total, dump) => total + dump.bytes, 0);
|
|
95
|
+
for (const name of doomed) {
|
|
96
|
+
try {
|
|
97
|
+
await rm(path.join(dir, name), { force: true });
|
|
98
|
+
} catch {
|
|
99
|
+
// silent-ok: best effort, and the next start tries again.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
logger.info(
|
|
103
|
+
`core dumps: ${dumps.length} present, keeping the newest ${Math.min(keep, dumps.length)}` +
|
|
104
|
+
(doomed.length > 0 ? `, removed ${doomed.length} (${(freed / 1e9).toFixed(2)} GB)` : "")
|
|
105
|
+
);
|
|
106
|
+
}
|