@torrent-tv/proxy 2.86.0 → 2.87.1
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/CHANGELOG.md +2142 -1915
- package/CLAUDE.md +322 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/browser-proxy-contract.md +94 -0
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +9 -6
- package/routes/transcode/session-file/get.js +52 -268
- package/routes/transcode/variant-file/get.js +46 -7
- package/routes/transcode/variant-warm/get.js +19 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +307 -29
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1229 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +755 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +935 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/http-responses.js +60 -0
- package/services/server/transcode-session-files.js +325 -0
- package/services/server/wire-outputs.js +983 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +46 -9
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -55,6 +55,26 @@ export class CompletedFiles {
|
|
|
55
55
|
/** Assemblies in flight, so two passes cannot write one file at once. @type {Set<string>} */
|
|
56
56
|
#writing = new Set();
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* What this store may hold, handed to it by the owner of the disk.
|
|
60
|
+
*
|
|
61
|
+
* It had NO BOUND AT ALL until 2026-09-14 — a 2.8 GB film kept whole on a
|
|
62
|
+
* host whose disk is often a 32 GB card, with nothing to say when to stop.
|
|
63
|
+
* The torrent pool's own "disk cap" did not cover it either: that counted
|
|
64
|
+
* WebTorrent's downloaded bitfield, so a piece held purely in MEMORY told
|
|
65
|
+
* against a ceiling called disk, and the bytes here told against nothing.
|
|
66
|
+
*
|
|
67
|
+
* Null until the owner has divided anything, and null does not license
|
|
68
|
+
* growth: it is what a store that has not yet been told looks like, and the
|
|
69
|
+
* first revision is a minute away.
|
|
70
|
+
*
|
|
71
|
+
* @type {number | null}
|
|
72
|
+
*/
|
|
73
|
+
#allowanceBytes = null;
|
|
74
|
+
|
|
75
|
+
/** When each held file was last asked for. @type {Map<string, number>} */
|
|
76
|
+
#lastRead = new Map();
|
|
77
|
+
|
|
58
78
|
/**
|
|
59
79
|
* @param {object} params
|
|
60
80
|
* @param {string} params.root - Where whole files live. Outside any torrent's
|
|
@@ -100,7 +120,97 @@ export class CompletedFiles {
|
|
|
100
120
|
* @returns {{ path: string, length: number, name: string } | null}
|
|
101
121
|
*/
|
|
102
122
|
find(infoHash, fileIndex) {
|
|
103
|
-
|
|
123
|
+
const key = this.#keyOf(infoHash, fileIndex);
|
|
124
|
+
const file = this.#held.get(key) ?? null;
|
|
125
|
+
if (file) {
|
|
126
|
+
// WHEN IT WAS LAST WANTED, which is the order it leaves in. A whole file
|
|
127
|
+
// nobody has asked for in the longest is the one whose loss costs least:
|
|
128
|
+
// it can be downloaded again, and a read of it falls back to the pieces.
|
|
129
|
+
this.#lastRead.set(key, Date.now());
|
|
130
|
+
}
|
|
131
|
+
return file;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Say how much disk these files may take between them.
|
|
136
|
+
*
|
|
137
|
+
* Over the share, the longest-unread whole file goes — never one being
|
|
138
|
+
* written, and never the one just asked for. Losing a whole file is not
|
|
139
|
+
* losing data: the torrent can fetch it again, and until it does the read
|
|
140
|
+
* falls back to the pieces, which is what `piece-from-whole-file` exists for.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} bytes
|
|
143
|
+
* @returns {{ bytes: number, removed: number }} What is held after, and how
|
|
144
|
+
* many files went.
|
|
145
|
+
*/
|
|
146
|
+
async allow(bytes) {
|
|
147
|
+
this.#allowanceBytes = Number.isFinite(bytes) && bytes >= 0 ? bytes : null;
|
|
148
|
+
let removed = 0;
|
|
149
|
+
if (this.#allowanceBytes === null) {
|
|
150
|
+
return { bytes: this.bytes, removed };
|
|
151
|
+
}
|
|
152
|
+
while (this.bytes > this.#allowanceBytes) {
|
|
153
|
+
const victim = this.#longestUnread();
|
|
154
|
+
if (!victim) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
await this.#remove(victim);
|
|
158
|
+
removed += 1;
|
|
159
|
+
}
|
|
160
|
+
return { bytes: this.bytes, removed };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Whether there is room for a file of this size.
|
|
165
|
+
*
|
|
166
|
+
* Asked BEFORE one is assembled, because assembling it and then removing it
|
|
167
|
+
* is the same bytes written for nothing.
|
|
168
|
+
*
|
|
169
|
+
* @param {number} length
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
hasRoomFor(length) {
|
|
173
|
+
if (this.#allowanceBytes === null) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return this.bytes + Math.max(0, Number(length) || 0) <= this.#allowanceBytes;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The key of the whole file nobody has wanted for longest, or null.
|
|
181
|
+
*
|
|
182
|
+
* @returns {string | null}
|
|
183
|
+
*/
|
|
184
|
+
#longestUnread() {
|
|
185
|
+
let oldest = null;
|
|
186
|
+
let oldestAt = Number.POSITIVE_INFINITY;
|
|
187
|
+
for (const key of this.#held.keys()) {
|
|
188
|
+
if (this.#writing.has(key)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const at = this.#lastRead.get(key) ?? 0;
|
|
192
|
+
if (at < oldestAt) {
|
|
193
|
+
oldest = key;
|
|
194
|
+
oldestAt = at;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return oldest;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Take one whole file off the disk.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} key
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async #remove(key) {
|
|
207
|
+
const file = this.#held.get(key);
|
|
208
|
+
this.#held.delete(key);
|
|
209
|
+
this.#lastRead.delete(key);
|
|
210
|
+
if (!file) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await fs.rm(file.path, REMOVAL).catch(() => undefined);
|
|
104
214
|
}
|
|
105
215
|
|
|
106
216
|
/**
|
|
@@ -197,6 +307,11 @@ export class CompletedFiles {
|
|
|
197
307
|
if (this.#writing.has(key)) {
|
|
198
308
|
return null;
|
|
199
309
|
}
|
|
310
|
+
if (!this.hasRoomFor(length)) {
|
|
311
|
+
// Assembling it and then removing it is the same bytes written for
|
|
312
|
+
// nothing. The pieces still serve the read; this file simply is not kept.
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
200
315
|
this.#writing.add(key);
|
|
201
316
|
const directory = path.join(this.#root, String(infoHash));
|
|
202
317
|
const where = path.join(directory, String(fileIndex));
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { statfs } from "node:fs/promises";
|
|
17
|
+
import { statSync } from "node:fs";
|
|
17
18
|
import path from "node:path";
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -36,3 +37,35 @@ export async function freeBytesFor(directory) {
|
|
|
36
37
|
}
|
|
37
38
|
return null;
|
|
38
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Which device holds a directory, so claimants on one disk are told apart from
|
|
43
|
+
* claimants on another.
|
|
44
|
+
*
|
|
45
|
+
* A RESOURCE IS WHAT CANNOT BE SUBSTITUTED, and two filesystems are two of
|
|
46
|
+
* them. Measured on the addon host 2026-09-05: `/tmp`, where the segments and
|
|
47
|
+
* the spill live, is the overlay (`dev=68`); `/data`, where the evidence lives,
|
|
48
|
+
* is ext4 on the nvme (`dev=66305`). Dividing one figure between claimants on
|
|
49
|
+
* both gives each a share of a disk it does not write to.
|
|
50
|
+
*
|
|
51
|
+
* The nearest ancestor that exists, for the same reason `freeBytesFor` walks
|
|
52
|
+
* up: a claimant's own directory is often made later than this is asked.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} directory
|
|
55
|
+
* @returns {string} A name for the resource, stable for the life of the mount.
|
|
56
|
+
*/
|
|
57
|
+
export function deviceOf(directory) {
|
|
58
|
+
let at = path.resolve(directory);
|
|
59
|
+
for (let depth = 0; depth < 16; depth += 1) {
|
|
60
|
+
try {
|
|
61
|
+
return `disk:${statSync(at).dev}`;
|
|
62
|
+
} catch {
|
|
63
|
+
const up = path.dirname(at);
|
|
64
|
+
if (up === at) {
|
|
65
|
+
return "disk:unknown";
|
|
66
|
+
}
|
|
67
|
+
at = up;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return "disk:unknown";
|
|
71
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file How much of the machine's memory is available, read once.
|
|
3
|
+
*
|
|
4
|
+
* It was written THREE TIMES, in three layers, with the same body each time:
|
|
5
|
+
* `transport/health-collector.js` for the score a proxy publishes to the registry,
|
|
6
|
+
* `storage/memory-report.js` for the line the process prints every second, and
|
|
7
|
+
* `storage/piece-store/shared-piece-store.js` for the budget the piece store takes.
|
|
8
|
+
* One fact, three owners — and the copies had already drifted once: the piece
|
|
9
|
+
* store was corrected from `os.freemem()` to the kernel's own estimate on
|
|
10
|
+
* 2026-08-27 and the health collector went on publishing the wrong quantity
|
|
11
|
+
* until 2026-09-02, so every Linux proxy in the pool understated itself, each
|
|
12
|
+
* by a different amount according to how much cache it happened to hold.
|
|
13
|
+
*
|
|
14
|
+
* It lives in the storage layer because it is a fact about the RESOURCE, and
|
|
15
|
+
* this layer is the resource's owner: what is free, what we already hold, and
|
|
16
|
+
* what everything that is not us has been seen to need.
|
|
17
|
+
*
|
|
18
|
+
* **Why `MemAvailable` and not `os.freemem()`.** On Linux `freemem` counts only
|
|
19
|
+
* the pages free at that instant, and the kernel deliberately keeps that number
|
|
20
|
+
* low by filling the rest with reclaimable cache — so a machine reads as nearly
|
|
21
|
+
* full while it has gigabytes to give. `MemAvailable` is the kernel's own
|
|
22
|
+
* estimate of what an allocation could actually obtain. Where there is no
|
|
23
|
+
* `/proc` — not Linux, or it is unreadable — `freemem` is the best answer there
|
|
24
|
+
* is, and on those systems it is not misleading.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the machine could give an allocation right now.
|
|
32
|
+
*
|
|
33
|
+
* @returns {{ bytes: number, measured: boolean }} `measured` is false where the
|
|
34
|
+
* kernel's own estimate was not available, so a caller that prints the figure
|
|
35
|
+
* can say which of the two it is holding.
|
|
36
|
+
*/
|
|
37
|
+
export function availableMemory() {
|
|
38
|
+
try {
|
|
39
|
+
const match = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(readFileSync("/proc/meminfo", "utf8"));
|
|
40
|
+
if (match) {
|
|
41
|
+
return { bytes: Number(match[1]) * 1024, measured: true };
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// silent-ok: not Linux, or /proc is not readable.
|
|
45
|
+
}
|
|
46
|
+
return { bytes: os.freemem(), measured: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What the machine could give an allocation right now, as a plain number.
|
|
51
|
+
*
|
|
52
|
+
* @returns {number}
|
|
53
|
+
*/
|
|
54
|
+
export function availableMemoryBytes() {
|
|
55
|
+
return availableMemory().bytes;
|
|
56
|
+
}
|