@torrent-tv/proxy 2.85.1 → 2.87.0
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 +2138 -1909
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- 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 +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -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 +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -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 +745 -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 +931 -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/wire-outputs.js +958 -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 -489
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +46 -0
- 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 -44
- 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 -518
- 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 +40 -8
- 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 -172
- 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 -9689
- 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/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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Who takes what of this machine, and how each of them is told its share.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the owner because the owner knows nothing about this proxy —
|
|
5
|
+
* it reads what is free, divides it and hands out shares — and kept out of the
|
|
6
|
+
* session manager because a list of claimants is not a fact about a session.
|
|
7
|
+
*
|
|
8
|
+
* A CLAIMANT IS WHOEVER HOLDS BYTES, and every one of them is here. Four do:
|
|
9
|
+
* the torrent's pieces in memory, the pieces spilled from there to disk, the
|
|
10
|
+
* segments an encoder produced, the files downloaded whole — and the evidence,
|
|
11
|
+
* which is a claimant with a rule of its own (`Diagnostics.js`). Three of the
|
|
12
|
+
* five live on the torrent thread, so they arrive as pairs of closures over its
|
|
13
|
+
* channel rather than as the objects themselves.
|
|
14
|
+
*
|
|
15
|
+
* **Which RESOURCE each takes is read, not assumed.** Memory is one. Disk is
|
|
16
|
+
* one per device, because two filesystems cannot pay for each other: measured
|
|
17
|
+
* on the addon host 2026-09-05, `/tmp` — the segments and the spill — is the
|
|
18
|
+
* overlay, and `/data` — the evidence — is ext4 on the nvme. One figure divided
|
|
19
|
+
* between claimants on both gave each a share of a disk it does not write to.
|
|
20
|
+
*
|
|
21
|
+
* What is NOT a claimant is anything that merely counts somebody else's bytes.
|
|
22
|
+
* The torrent pool held a "disk cap" of 10 GB over `torrentDownloadedBytes` — a
|
|
23
|
+
* sum over WebTorrent's bitfield, so a piece held purely in MEMORY told against
|
|
24
|
+
* a ceiling called disk, while the bytes it was meant to bound belong to the
|
|
25
|
+
* spill and to the whole files, which have owners of their own. It owned
|
|
26
|
+
* nothing and is gone.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { deviceOf } from "./free.js";
|
|
30
|
+
import { MachineBudget } from "./MachineBudget.js";
|
|
31
|
+
import { availableMemoryBytes } from "./machine-memory.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the owner of this machine's room and register everything that takes any.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} params
|
|
37
|
+
* @param {{ root: string, stats: () => { bytes: number } }} params.segmentStore
|
|
38
|
+
* @param {{ held: () => number, allow: (bytes: number) => unknown }} [params.spill] -
|
|
39
|
+
* The pieces the memory store spills. They live on the torrent thread, so
|
|
40
|
+
* this is a pair of closures over the channel rather than the pool itself.
|
|
41
|
+
* @param {{ held: () => number, wanted: () => number, allow: (bytes: number) => unknown }} [params.memory] -
|
|
42
|
+
* The pieces held IN memory, on the torrent thread as well. Told their share
|
|
43
|
+
* by the same owner that divides the disk, because the two trade: what does
|
|
44
|
+
* not fit in memory is spilled, so this share decides how much disk is needed.
|
|
45
|
+
* @param {{ held: () => number, allow: (bytes: number) => unknown, root?: string }} [params.wholeFiles] -
|
|
46
|
+
* Files downloaded whole and kept as files, on the torrent thread as well.
|
|
47
|
+
* @param {import("./Diagnostics.js").Diagnostics} [params.diagnostics] - The
|
|
48
|
+
* evidence. Built where the collectors are, so one object is both registered
|
|
49
|
+
* here and consulted by them.
|
|
50
|
+
* @param {string} [params.diagnosticsRoot] - Where the evidence is written,
|
|
51
|
+
* which on the addon host is a different device from everything else.
|
|
52
|
+
* @param {(directory: string) => Promise<number | null>} params.readFree
|
|
53
|
+
* @param {import("./MachineBudget.js").BudgetPolicy} [params.policy] - What the
|
|
54
|
+
* operator has said this proxy may take. Absent, the measured default.
|
|
55
|
+
* @param {{ info: Function, warn?: Function }} [params.logger]
|
|
56
|
+
* @returns {{ revise: () => Promise<unknown>, segmentBytes: () => number, diagnostics: object | null, describe: () => string }}
|
|
57
|
+
* What the segments may hold is asked for rather than pushed: zero until the
|
|
58
|
+
* first revision, and zero stops growth rather than licensing it.
|
|
59
|
+
*/
|
|
60
|
+
export function wireMachineBudget({
|
|
61
|
+
segmentStore,
|
|
62
|
+
spill,
|
|
63
|
+
memory,
|
|
64
|
+
wholeFiles,
|
|
65
|
+
diagnostics,
|
|
66
|
+
diagnosticsRoot = "",
|
|
67
|
+
readFree,
|
|
68
|
+
policy,
|
|
69
|
+
logger
|
|
70
|
+
}) {
|
|
71
|
+
// THE OPERATOR NAMES ONE FIGURE FOR DISK, and disk is one resource per
|
|
72
|
+
// device — so the floor they named is given to every disk resource as it is
|
|
73
|
+
// defined, rather than asking them to name a device they have never heard of.
|
|
74
|
+
const floors = { ...(policy?.floors ?? {}) };
|
|
75
|
+
const diskFloorBytes = Math.max(0, Number(policy?.diskFloorBytes) || 0);
|
|
76
|
+
const effective = { ...(policy ?? {}), floors };
|
|
77
|
+
const budget = new MachineBudget({ policy: effective, logger });
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} name
|
|
80
|
+
* @param {string} directory
|
|
81
|
+
*/
|
|
82
|
+
const defineDisk = (name, directory) => {
|
|
83
|
+
if (diskFloorBytes > 0 && !(name in floors)) {
|
|
84
|
+
floors[name] = diskFloorBytes;
|
|
85
|
+
}
|
|
86
|
+
budget.defineResource({ name, readFree: () => readFree(directory) });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
budget.defineResource({ name: "memory", readFree: () => availableMemoryBytes() });
|
|
90
|
+
|
|
91
|
+
const segmentsOn = deviceOf(segmentStore.root);
|
|
92
|
+
defineDisk(segmentsOn, segmentStore.root);
|
|
93
|
+
|
|
94
|
+
let segmentBytes = 0;
|
|
95
|
+
budget.register({
|
|
96
|
+
name: "segments",
|
|
97
|
+
resource: segmentsOn,
|
|
98
|
+
held: () => segmentStore.stats().bytes,
|
|
99
|
+
// The whole of every film anybody is watching. There is no smaller honest
|
|
100
|
+
// answer, so it asks for everything and is cut in proportion like the rest.
|
|
101
|
+
wanted: () => Number.MAX_SAFE_INTEGER,
|
|
102
|
+
allow: (bytes) => {
|
|
103
|
+
segmentBytes = bytes;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (typeof memory?.allow === "function") {
|
|
108
|
+
budget.register({
|
|
109
|
+
name: "pieces in memory",
|
|
110
|
+
resource: "memory",
|
|
111
|
+
held: () => memory.held?.() ?? 0,
|
|
112
|
+
wanted: () => memory.wanted?.() ?? 0,
|
|
113
|
+
allow: (bytes) => {
|
|
114
|
+
void memory.allow(bytes);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof spill?.allow === "function") {
|
|
120
|
+
// The share travels the channel that already carries everything else, and
|
|
121
|
+
// the reply says what they hold — one exchange, both directions.
|
|
122
|
+
budget.register({
|
|
123
|
+
name: "spilled pieces",
|
|
124
|
+
resource: segmentsOn,
|
|
125
|
+
held: () => spill.held?.() ?? 0,
|
|
126
|
+
wanted: () => Number.MAX_SAFE_INTEGER,
|
|
127
|
+
allow: (bytes) => {
|
|
128
|
+
void spill.allow(bytes);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof wholeFiles?.allow === "function") {
|
|
134
|
+
const filesOn = wholeFiles.root ? deviceOf(wholeFiles.root) : segmentsOn;
|
|
135
|
+
defineDisk(filesOn, wholeFiles.root || segmentStore.root);
|
|
136
|
+
budget.register({
|
|
137
|
+
name: "whole files",
|
|
138
|
+
resource: filesOn,
|
|
139
|
+
held: () => wholeFiles.held?.() ?? 0,
|
|
140
|
+
// A whole file is worth keeping while anybody may read it again, and
|
|
141
|
+
// there is no smaller honest answer than "all of them" — like the rest,
|
|
142
|
+
// it is cut in proportion.
|
|
143
|
+
wanted: () => Number.MAX_SAFE_INTEGER,
|
|
144
|
+
allow: (bytes) => {
|
|
145
|
+
void wholeFiles.allow(bytes);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (diagnostics) {
|
|
151
|
+
const evidenceOn = diagnosticsRoot ? deviceOf(diagnosticsRoot) : segmentsOn;
|
|
152
|
+
defineDisk(evidenceOn, diagnosticsRoot || segmentStore.root);
|
|
153
|
+
budget.register({
|
|
154
|
+
name: "diagnostics",
|
|
155
|
+
resource: evidenceOn,
|
|
156
|
+
held: () => diagnostics.held(),
|
|
157
|
+
// What it holds plus one more of the largest kind seen — measured, and
|
|
158
|
+
// nothing at all before a fault has happened.
|
|
159
|
+
wanted: () => diagnostics.wanted(),
|
|
160
|
+
allow: (bytes) => diagnostics.allow(bytes)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
revise: async () => {
|
|
166
|
+
// Re-read the evidence before dividing: it is the one claimant whose
|
|
167
|
+
// bytes are written by something that never reports them.
|
|
168
|
+
await diagnostics?.measure();
|
|
169
|
+
return budget.revise();
|
|
170
|
+
},
|
|
171
|
+
segmentBytes: () => segmentBytes,
|
|
172
|
+
diagnostics: diagnostics ?? null,
|
|
173
|
+
describe: () => budget.describe()
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
Urgency,
|
|
30
30
|
urgencyName
|
|
31
31
|
} from "../demand/index.js";
|
|
32
|
-
import {
|
|
32
|
+
import { pieceStoreOf } from "../piece-store-of.js";
|
|
33
33
|
|
|
34
34
|
export class SwarmSelection {
|
|
35
35
|
#torrent;
|
|
@@ -50,7 +50,7 @@ export class SwarmSelection {
|
|
|
50
50
|
* piece store is reached. Injectable so a test can drive the memory
|
|
51
51
|
* projection without constructing a real store.
|
|
52
52
|
*/
|
|
53
|
-
constructor({ torrent, register, findStore =
|
|
53
|
+
constructor({ torrent, register, findStore = pieceStoreOf }) {
|
|
54
54
|
this.#torrent = torrent;
|
|
55
55
|
this.#register = register;
|
|
56
56
|
this.#findStore = findStore;
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* @file One demand register and one swarm selection per torrent, found from the
|
|
3
3
|
* torrent itself.
|
|
4
4
|
*
|
|
5
|
-
* The same shape the piece store already uses — `
|
|
5
|
+
* The same shape the piece store already uses — `pieceStoreOf(torrent)`
|
|
6
6
|
* reaches the store without it being threaded through every call — and for the
|
|
7
7
|
* same reason: the reader, the pool and the background fill all need the same
|
|
8
8
|
* instance, and passing it through six layers of arguments would make the
|
|
9
|
-
*
|
|
9
|
+
* argument lists bigger than the thing they carry.
|
|
10
10
|
*
|
|
11
11
|
* Kept in a live set as well as a weak map, because one question cannot be
|
|
12
12
|
* answered per torrent: whether ANYTHING anywhere is still missing something
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* picture — `Rus Sound/<name>.mka`, `Sub/[group]/<name>.ass` — and a viewer who
|
|
7
7
|
* cannot reach them is watching the release without half of what it carries.
|
|
8
8
|
* This module answers only which file goes with which; what is inside a file is
|
|
9
|
-
* the container layer's business (`services/container/`), and what a viewer is
|
|
9
|
+
* the container layer's business (`services/media/container/`), and what a viewer is
|
|
10
10
|
* shown is composed in the browser, where the locale is known.
|
|
11
11
|
*
|
|
12
12
|
* **Why it lives in `torrent/`.** A container knows only itself, and a track
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The piece store behind a torrent, reached through what it does.
|
|
3
|
+
*
|
|
4
|
+
* WebTorrent wraps the store it was given in stores of its own, each holding
|
|
5
|
+
* the next one as `store`, so the store the torrent component talks to is found
|
|
6
|
+
* by walking that chain. It is recognised by the interface this component uses
|
|
7
|
+
* — which pieces the readers have asked to keep (`protectedRanges`) — and not by
|
|
8
|
+
* its class: the torrent component does not import the storage component's
|
|
9
|
+
* implementation, it is handed it where the thread is assembled
|
|
10
|
+
* (`worker/worker.js`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** How deep WebTorrent's own wrappers are allowed to go before the walk gives up. */
|
|
14
|
+
const WRAPPER_DEPTH = 8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ store?: object } | null | undefined} torrent
|
|
18
|
+
* @returns {object | null}
|
|
19
|
+
*/
|
|
20
|
+
export function pieceStoreOf(torrent) {
|
|
21
|
+
let candidate = torrent?.store;
|
|
22
|
+
for (let depth = 0; candidate && depth < WRAPPER_DEPTH; depth += 1) {
|
|
23
|
+
if (typeof candidate.protectedRanges === "function") {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
candidate = candidate.store;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
* @file WebTorrent client pool.
|
|
3
3
|
*
|
|
4
4
|
* Manages a shared WebTorrent client instance and a map of active torrents
|
|
5
|
-
* keyed by their own infohash (see `torrent-source-key.js`), so a magnet and
|
|
5
|
+
* keyed by their own infohash (see `torrent/torrent-source-key.js`), so a magnet and
|
|
6
6
|
* a `.torrent` file for the same content share one entry. Tracks file-level
|
|
7
7
|
* usage so that only the pieces needed by active streams are selected for
|
|
8
8
|
* download.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { IDLE_KEEP_MS } from "
|
|
11
|
+
import { IDLE_KEEP_MS } from "../storage/keep.js";
|
|
12
12
|
import dns from "node:dns/promises";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { rmSync
|
|
15
|
+
import { rmSync } from "node:fs";
|
|
16
16
|
import WebTorrent from "webtorrent";
|
|
17
|
-
import { logger } from "
|
|
18
|
-
import {
|
|
17
|
+
import { logger } from "../../utils/logger.js";
|
|
18
|
+
import { pieceStoreOf } from "./piece-store-of.js";
|
|
19
19
|
import { Urgency, urgencyName } from "./demand/index.js";
|
|
20
20
|
import { demandFor, forgetTorrent, reconcileAll, hasUnmetDemand } from "./download/registry.js";
|
|
21
21
|
import { withdrawClaim } from "./download/withdraw-claim.js";
|
|
22
|
-
import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "
|
|
22
|
+
import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "../viewer/PriorityMap.js";
|
|
23
23
|
import { deriveSourceKey } from "./torrent-source-key.js";
|
|
24
24
|
|
|
25
25
|
/** How a window stated from the priority map names itself. */
|
|
@@ -89,15 +89,6 @@ const PRIORITY_WINDOW_BYTES = 16 * 1024 * 1024;
|
|
|
89
89
|
const HEADER_HEAD_BYTES = 256 * 1024;
|
|
90
90
|
const HEADER_TAIL_BYTES = 2 * 1024 * 1024;
|
|
91
91
|
|
|
92
|
-
// Global disk cap. Downloaded torrent data is removed on idle TTL and at
|
|
93
|
-
// shutdown, but under pressure (several large files within the TTL window)
|
|
94
|
-
// it can still fill a small HA host's disk (SD/eMMC), which can take down
|
|
95
|
-
// Home Assistant itself. When the total exceeds the cap, whole torrents with
|
|
96
|
-
// no active reader are evicted least-recently-used first. Active torrents are
|
|
97
|
-
// never evicted (we cannot delete what is playing).
|
|
98
|
-
const DISK_CAP_ABSOLUTE_MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GB
|
|
99
|
-
const DISK_CAP_SWEEP_INTERVAL_MS = 30_000;
|
|
100
|
-
|
|
101
92
|
// Adaptive upload. Seeding to the BitTorrent swarm does not help our viewer (we
|
|
102
93
|
// deliver over our own WebRTC/HTTPS channel) — it is pure uplink cost and the
|
|
103
94
|
// riskiest legal act (active distribution). So the default is minimal: a
|
|
@@ -528,23 +519,6 @@ function formatWarning(warning) {
|
|
|
528
519
|
return stack || warning.message;
|
|
529
520
|
}
|
|
530
521
|
|
|
531
|
-
/**
|
|
532
|
-
* @param {string} storePath
|
|
533
|
-
* @returns {number}
|
|
534
|
-
*/
|
|
535
|
-
function computeDefaultDiskCap(storePath) {
|
|
536
|
-
try {
|
|
537
|
-
const stat = statfsSync(storePath);
|
|
538
|
-
const freeBytes = stat.bavail * stat.bsize;
|
|
539
|
-
if (Number.isFinite(freeBytes) && freeBytes > 0) {
|
|
540
|
-
return Math.min(DISK_CAP_ABSOLUTE_MAX_BYTES, Math.floor(freeBytes / 2));
|
|
541
|
-
}
|
|
542
|
-
} catch {
|
|
543
|
-
// statfs unavailable (old Node / odd FS) — fall back to the fixed max.
|
|
544
|
-
}
|
|
545
|
-
return DISK_CAP_ABSOLUTE_MAX_BYTES;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
522
|
/**
|
|
549
523
|
* Decode a raw torrent source value into the format expected by WebTorrent.
|
|
550
524
|
*
|
|
@@ -1009,15 +983,9 @@ export class TorrentPool {
|
|
|
1009
983
|
*/
|
|
1010
984
|
#edgePrefetches = new Map();
|
|
1011
985
|
|
|
1012
|
-
/** Global disk cap in bytes (0 = disabled). */
|
|
1013
|
-
#maxDiskBytes = 0;
|
|
1014
|
-
|
|
1015
986
|
/** Memory budget per torrent for resident pieces; undefined = store default. */
|
|
1016
987
|
#memoryBytes;
|
|
1017
988
|
|
|
1018
|
-
/** Periodic disk-cap enforcement timer. */
|
|
1019
|
-
#diskSweepTimer = null;
|
|
1020
|
-
|
|
1021
989
|
/** Current client-wide upload limit in bytes/sec (adaptive). -1 = not yet set. */
|
|
1022
990
|
#uploadLimit = -1;
|
|
1023
991
|
|
|
@@ -1047,12 +1015,6 @@ export class TorrentPool {
|
|
|
1047
1015
|
*/
|
|
1048
1016
|
#swarmTimingByTorrent = new WeakMap();
|
|
1049
1017
|
|
|
1050
|
-
/**
|
|
1051
|
-
* @param {{ maxDiskBytes?: number }} [options]
|
|
1052
|
-
* `maxDiskBytes` caps total downloaded torrent data; when omitted a
|
|
1053
|
-
* default is computed from free disk (min(10 GB, half free)). Pass 0 to
|
|
1054
|
-
* disable the cap.
|
|
1055
|
-
*/
|
|
1056
1018
|
/**
|
|
1057
1019
|
* Sources every file of which this proxy holds whole.
|
|
1058
1020
|
*
|
|
@@ -1168,11 +1130,26 @@ export class TorrentPool {
|
|
|
1168
1130
|
}
|
|
1169
1131
|
}
|
|
1170
1132
|
|
|
1133
|
+
/** The chunk store class every torrent is built with. @type {Function} */
|
|
1134
|
+
#pieceStore;
|
|
1135
|
+
|
|
1171
1136
|
get claimsWithdrawn() {
|
|
1172
1137
|
return this.#claimsWithdrawn;
|
|
1173
1138
|
}
|
|
1174
1139
|
|
|
1175
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* @param {object} params
|
|
1142
|
+
* @param {number} [params.memoryBytes]
|
|
1143
|
+
* @param {string[]} [params.dhtBootstrap]
|
|
1144
|
+
* @param {Function} params.pieceStore - The chunk store WebTorrent builds per
|
|
1145
|
+
* torrent: the storage component's implementation, handed in where the
|
|
1146
|
+
* thread is assembled.
|
|
1147
|
+
*/
|
|
1148
|
+
constructor({ memoryBytes, dhtBootstrap, pieceStore } = {}) {
|
|
1149
|
+
if (typeof pieceStore !== "function") {
|
|
1150
|
+
throw new TypeError("TorrentPool needs the piece store to build torrents with");
|
|
1151
|
+
}
|
|
1152
|
+
this.#pieceStore = pieceStore;
|
|
1176
1153
|
this.#memoryBytes = Number.isFinite(memoryBytes) && memoryBytes > 0 ? memoryBytes : undefined;
|
|
1177
1154
|
|
|
1178
1155
|
// Sweep orphaned torrent data left by a previous hard kill (no graceful
|
|
@@ -1235,16 +1212,6 @@ export class TorrentPool {
|
|
|
1235
1212
|
logger.warn(`torrent-pool: client warning: ${formatWarning(warning)}`);
|
|
1236
1213
|
});
|
|
1237
1214
|
|
|
1238
|
-
this.#maxDiskBytes = Number.isFinite(maxDiskBytes) && maxDiskBytes >= 0
|
|
1239
|
-
? maxDiskBytes
|
|
1240
|
-
: computeDefaultDiskCap(os.tmpdir());
|
|
1241
|
-
if (this.#maxDiskBytes > 0) {
|
|
1242
|
-
const gigabytes = (this.#maxDiskBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
1243
|
-
logger.info(`torrent-pool: disk cap ${gigabytes} GB (LRU eviction of idle torrents above it)`);
|
|
1244
|
-
this.#diskSweepTimer = setInterval(() => this.#enforceDiskCap(), DISK_CAP_SWEEP_INTERVAL_MS);
|
|
1245
|
-
this.#diskSweepTimer.unref?.();
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
1215
|
// Adaptive upload: start with seeding OFF (nothing is being watched yet),
|
|
1249
1216
|
// then let the periodic adjuster raise it to the floor while a reader is
|
|
1250
1217
|
// active and to the boost when download is choke-starved. WebTorrent's
|
|
@@ -1321,7 +1288,7 @@ export class TorrentPool {
|
|
|
1321
1288
|
* @returns {{ ranges: Array<{ from: number, to: number }>, missing: boolean }}
|
|
1322
1289
|
*/
|
|
1323
1290
|
#readerDemand(torrent) {
|
|
1324
|
-
const store =
|
|
1291
|
+
const store = pieceStoreOf(torrent);
|
|
1325
1292
|
const ranges = typeof store?.protectedRanges === "function" ? store.protectedRanges() : [];
|
|
1326
1293
|
let missing = false;
|
|
1327
1294
|
for (const range of ranges) {
|
|
@@ -1610,61 +1577,6 @@ export class TorrentPool {
|
|
|
1610
1577
|
logger.info(`torrent-pool: upload limit -> ${Math.round(bytesPerSec / 1024)} KB/s (${reason})`);
|
|
1611
1578
|
}
|
|
1612
1579
|
|
|
1613
|
-
/**
|
|
1614
|
-
* Sum of downloaded bytes across pooled torrents — a cheap proxy for the
|
|
1615
|
-
* on-disk footprint (the FS store writes downloaded pieces).
|
|
1616
|
-
*
|
|
1617
|
-
* @returns {number}
|
|
1618
|
-
*/
|
|
1619
|
-
#currentDiskBytes() {
|
|
1620
|
-
let total = 0;
|
|
1621
|
-
for (const torrent of this.torrents.values()) {
|
|
1622
|
-
total += Math.max(0, torrentDownloadedBytes(torrent));
|
|
1623
|
-
}
|
|
1624
|
-
return total;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
/**
|
|
1628
|
-
* Evict whole torrents, least-recently-used first, while the total on-disk
|
|
1629
|
-
* footprint exceeds the cap. Only torrents with NO active file reader are
|
|
1630
|
-
* evictable — a playing torrent cannot be deleted. Best-effort.
|
|
1631
|
-
*
|
|
1632
|
-
* @returns {void}
|
|
1633
|
-
*/
|
|
1634
|
-
#enforceDiskCap() {
|
|
1635
|
-
if (this.#maxDiskBytes <= 0) {
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
let used = this.#currentDiskBytes();
|
|
1639
|
-
if (used <= this.#maxDiskBytes) {
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
// Candidates: pooled torrents nothing is wanted of, the longest unwanted
|
|
1643
|
-
// first.
|
|
1644
|
-
const candidates = [...this.torrents.values()]
|
|
1645
|
-
.filter((torrent) => !isWanted(torrent))
|
|
1646
|
-
.sort(
|
|
1647
|
-
(earlier, later) =>
|
|
1648
|
-
(this.#lastAccess.get(earlier) ?? 0) - (this.#lastAccess.get(later) ?? 0)
|
|
1649
|
-
);
|
|
1650
|
-
|
|
1651
|
-
for (const torrent of candidates) {
|
|
1652
|
-
if (used <= this.#maxDiskBytes) {
|
|
1653
|
-
break;
|
|
1654
|
-
}
|
|
1655
|
-
const freed = Math.max(0, torrentDownloadedBytes(torrent));
|
|
1656
|
-
const name = typeof torrent?.name === "string" ? torrent.name : "(unknown)";
|
|
1657
|
-
const gigabytes = (this.#maxDiskBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
1658
|
-
logger.info(
|
|
1659
|
-
`torrent-pool: disk cap ${gigabytes} GB exceeded — evicting idle torrent "${name}" ` +
|
|
1660
|
-
`(~${(freed / (1024 * 1024)).toFixed(0)} MB)`
|
|
1661
|
-
);
|
|
1662
|
-
this.#cancelIdleRemoval(torrent);
|
|
1663
|
-
this.#removeTorrent(torrent, "disk-cap");
|
|
1664
|
-
used -= freed;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
1580
|
/**
|
|
1669
1581
|
* Attach peer-discovery diagnostics to a freshly added torrent: tracker
|
|
1670
1582
|
* announce results (seeders/leechers per announce) and torrent-level
|
|
@@ -1914,7 +1826,7 @@ export class TorrentPool {
|
|
|
1914
1826
|
this.#readPositionByTorrent.delete(existing);
|
|
1915
1827
|
this.client.remove(existing, { destroyStore: true }, () => {
|
|
1916
1828
|
const addedReplacement = this.client.add(torrentId, {
|
|
1917
|
-
store:
|
|
1829
|
+
store: this.#pieceStore,
|
|
1918
1830
|
storeCacheSlots: 0,
|
|
1919
1831
|
storeOpts: this.#storeOptions(),
|
|
1920
1832
|
deselect: true
|
|
@@ -1947,7 +1859,7 @@ export class TorrentPool {
|
|
|
1947
1859
|
// hands out, holds pieces in shared memory the main thread can read
|
|
1948
1860
|
// directly, and spills to disk instead of losing them.
|
|
1949
1861
|
const added = this.client.add(torrentId, {
|
|
1950
|
-
store:
|
|
1862
|
+
store: this.#pieceStore,
|
|
1951
1863
|
storeCacheSlots: 0,
|
|
1952
1864
|
storeOpts: this.#storeOptions(),
|
|
1953
1865
|
// Nothing is fetched until somebody says they want it. WebTorrent's own
|
|
@@ -2561,7 +2473,7 @@ export class TorrentPool {
|
|
|
2561
2473
|
* whole-file selection, a window around the read head, and the reader itself
|
|
2562
2474
|
* — and they overwrote each other on every request. Every need is now stated
|
|
2563
2475
|
* in one register (`services/demand/`) and one class turns the register into
|
|
2564
|
-
* requests to the swarm (`services/download/SwarmSelection.js`); several
|
|
2476
|
+
* requests to the swarm (`services/torrent/download/SwarmSelection.js`); several
|
|
2565
2477
|
* readers on one file therefore produce the union of their windows instead of
|
|
2566
2478
|
* the last caller's opinion.
|
|
2567
2479
|
*
|
|
@@ -2667,11 +2579,6 @@ export class TorrentPool {
|
|
|
2667
2579
|
* @returns {Promise<void>}
|
|
2668
2580
|
*/
|
|
2669
2581
|
async destroyAll() {
|
|
2670
|
-
// Stop periodic disk-cap enforcement.
|
|
2671
|
-
if (this.#diskSweepTimer) {
|
|
2672
|
-
clearInterval(this.#diskSweepTimer);
|
|
2673
|
-
this.#diskSweepTimer = null;
|
|
2674
|
-
}
|
|
2675
2582
|
// Stop periodic adaptive-upload adjustment.
|
|
2676
2583
|
if (this.#uploadAdjustTimer) {
|
|
2677
2584
|
clearInterval(this.#uploadAdjustTimer);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* this must not be asking the swarm for anything.
|
|
20
20
|
*
|
|
21
21
|
* WHY IT IS A READ AND NOT A SELECTION. `file.select()` claims every piece of a
|
|
22
|
-
* file at once. `#syncSelections` in `torrent-pool.js` records what that cost
|
|
22
|
+
* file at once. `#syncSelections` in `torrent/torrent-pool.js` records what that cost
|
|
23
23
|
* when it was done alongside the readers' own moving windows: a claim covering
|
|
24
24
|
* everything always outranked the window, and a seek to 89.1 % of a 4.7 GB film
|
|
25
25
|
* waited 93 s while the swarm fetched 2.47 GB in file order. So this walks the
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* is reading and gives it back.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { logger } from "
|
|
30
|
+
import { logger } from "../../../utils/logger.js";
|
|
31
31
|
import { readersAreBlockedOn, stallsSeenOn } from "./piece-reader.js";
|
|
32
32
|
|
|
33
33
|
/**
|