@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file One budget: one owner, one policy, a share per resource.
|
|
3
|
+
*
|
|
4
|
+
* What it replaced was two owners applying one rule to two readings — memory
|
|
5
|
+
* divided inside the torrent thread, disk divided on the main one, neither able
|
|
6
|
+
* to see the other. The claimants TRADE ACROSS RESOURCES: pieces that do not
|
|
7
|
+
* fit in memory are spilled to disk, so how much memory the piece store is
|
|
8
|
+
* given decides how much disk it needs. Field 2026-08-31: 14 400 MB spilled in
|
|
9
|
+
* fifty minutes while the memory store held 312-424 MB.
|
|
10
|
+
*
|
|
11
|
+
* And "disk" is not one resource. Measured on the addon host 2026-09-05,
|
|
12
|
+
* `/tmp` is the overlay filesystem and `/data` is ext4 on the nvme — two
|
|
13
|
+
* devices, so one figure divided between claimants on both gives each a share
|
|
14
|
+
* of a disk it does not write to.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import { MachineBudget } from "../services/storage/MachineBudget.js";
|
|
20
|
+
|
|
21
|
+
const MEGABYTE = 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{ name: string, resource: string, held?: number, wanted: number, minimum?: number }} shape
|
|
25
|
+
* @returns {{ claimant: object, allowed: () => number }}
|
|
26
|
+
*/
|
|
27
|
+
function claimantOf({ name, resource, held = 0, wanted, minimum }) {
|
|
28
|
+
let allowed = 0;
|
|
29
|
+
return {
|
|
30
|
+
claimant: {
|
|
31
|
+
name,
|
|
32
|
+
resource,
|
|
33
|
+
minimum,
|
|
34
|
+
held: () => held,
|
|
35
|
+
wanted: () => wanted,
|
|
36
|
+
allow: (bytes) => {
|
|
37
|
+
allowed = bytes;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
allowed: () => allowed
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("each resource is divided on its own — a share of memory is not a share of disk", async () => {
|
|
45
|
+
const budget = new MachineBudget({ logger: { info: () => {}, warn: () => {} } });
|
|
46
|
+
budget.defineResource({ name: "memory", readFree: () => 100 * MEGABYTE });
|
|
47
|
+
budget.defineResource({ name: "disk:tmp", readFree: () => 10 * MEGABYTE });
|
|
48
|
+
|
|
49
|
+
const pieces = claimantOf({ name: "pieces in memory", resource: "memory", wanted: Number.MAX_SAFE_INTEGER });
|
|
50
|
+
const segments = claimantOf({ name: "segments", resource: "disk:tmp", wanted: Number.MAX_SAFE_INTEGER });
|
|
51
|
+
budget.register(pieces.claimant);
|
|
52
|
+
budget.register(segments.claimant);
|
|
53
|
+
|
|
54
|
+
await budget.revise();
|
|
55
|
+
|
|
56
|
+
assert.equal(pieces.allowed(), 100 * MEGABYTE, "memory is divided from what memory has");
|
|
57
|
+
assert.equal(segments.allowed(), 10 * MEGABYTE, "and disk from what that disk has");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("two devices are two resources — one is not cut to pay for the other", async () => {
|
|
61
|
+
const budget = new MachineBudget({ logger: { info: () => {}, warn: () => {} } });
|
|
62
|
+
budget.defineResource({ name: "disk:66305", readFree: () => 8 * MEGABYTE });
|
|
63
|
+
budget.defineResource({ name: "disk:68", readFree: () => 80 * MEGABYTE });
|
|
64
|
+
|
|
65
|
+
// The shape on the addon host: segments and spill on the overlay, the
|
|
66
|
+
// evidence on the nvme.
|
|
67
|
+
const segments = claimantOf({ name: "segments", resource: "disk:68", wanted: Number.MAX_SAFE_INTEGER });
|
|
68
|
+
const spill = claimantOf({ name: "spilled pieces", resource: "disk:68", wanted: Number.MAX_SAFE_INTEGER });
|
|
69
|
+
const evidence = claimantOf({ name: "diagnostics", resource: "disk:66305", wanted: Number.MAX_SAFE_INTEGER });
|
|
70
|
+
for (const one of [segments, spill, evidence]) {
|
|
71
|
+
budget.register(one.claimant);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await budget.revise();
|
|
75
|
+
|
|
76
|
+
assert.equal(evidence.allowed(), 8 * MEGABYTE, "the evidence gets the disk it actually writes to, whole");
|
|
77
|
+
assert.equal(segments.allowed() + spill.allowed(), 80 * MEGABYTE, "and the other device is divided among its own");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a fixed ceiling is the operator's, and it binds", async () => {
|
|
81
|
+
const budget = new MachineBudget({
|
|
82
|
+
policy: { kind: "fixed", bytes: 5 * MEGABYTE },
|
|
83
|
+
logger: { info: () => {}, warn: () => {} }
|
|
84
|
+
});
|
|
85
|
+
budget.defineResource({ name: "disk", readFree: () => 1000 * MEGABYTE });
|
|
86
|
+
const segments = claimantOf({ name: "segments", resource: "disk", wanted: Number.MAX_SAFE_INTEGER });
|
|
87
|
+
budget.register(segments.claimant);
|
|
88
|
+
|
|
89
|
+
await budget.revise();
|
|
90
|
+
|
|
91
|
+
assert.equal(segments.allowed(), 5 * MEGABYTE, "a disk with a gigabyte free is still capped at what was asked for");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a share of free is the operator's too", async () => {
|
|
95
|
+
const budget = new MachineBudget({
|
|
96
|
+
policy: { kind: "share", share: 0.25 },
|
|
97
|
+
logger: { info: () => {}, warn: () => {} }
|
|
98
|
+
});
|
|
99
|
+
budget.defineResource({ name: "disk", readFree: () => 40 * MEGABYTE });
|
|
100
|
+
const segments = claimantOf({ name: "segments", resource: "disk", held: 0, wanted: Number.MAX_SAFE_INTEGER });
|
|
101
|
+
budget.register(segments.claimant);
|
|
102
|
+
|
|
103
|
+
await budget.revise();
|
|
104
|
+
|
|
105
|
+
assert.equal(segments.allowed(), 10 * MEGABYTE);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a floor lifts a policy that would allow less, and cannot invent room", async () => {
|
|
109
|
+
const budget = new MachineBudget({
|
|
110
|
+
policy: { kind: "fixed", bytes: MEGABYTE, floors: { disk: 6 * MEGABYTE } },
|
|
111
|
+
logger: { info: () => {}, warn: () => {} }
|
|
112
|
+
});
|
|
113
|
+
budget.defineResource({ name: "disk", readFree: () => 4 * MEGABYTE });
|
|
114
|
+
const segments = claimantOf({ name: "segments", resource: "disk", wanted: Number.MAX_SAFE_INTEGER });
|
|
115
|
+
budget.register(segments.claimant);
|
|
116
|
+
|
|
117
|
+
await budget.revise();
|
|
118
|
+
|
|
119
|
+
assert.equal(
|
|
120
|
+
segments.allowed(),
|
|
121
|
+
4 * MEGABYTE,
|
|
122
|
+
"the floor lifts the ceiling above the fixed one, but never past what the machine has"
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a resource that cannot be read allows nothing to grow", async () => {
|
|
127
|
+
const budget = new MachineBudget({ logger: { info: () => {}, warn: () => {} } });
|
|
128
|
+
budget.defineResource({ name: "disk", readFree: () => null });
|
|
129
|
+
const segments = claimantOf({ name: "segments", resource: "disk", held: 0, wanted: Number.MAX_SAFE_INTEGER });
|
|
130
|
+
budget.register(segments.claimant);
|
|
131
|
+
|
|
132
|
+
await budget.revise();
|
|
133
|
+
|
|
134
|
+
assert.equal(segments.allowed(), 0, "unreadable is not the same as unbounded");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a claimant below its own minimum says so", async () => {
|
|
138
|
+
const said = [];
|
|
139
|
+
const budget = new MachineBudget({ logger: { info: () => {}, warn: (line) => said.push(line) } });
|
|
140
|
+
budget.defineResource({ name: "memory", readFree: () => MEGABYTE });
|
|
141
|
+
budget.register(claimantOf({
|
|
142
|
+
name: "pieces in memory", resource: "memory", wanted: Number.MAX_SAFE_INTEGER, minimum: 64 * MEGABYTE
|
|
143
|
+
}).claimant);
|
|
144
|
+
|
|
145
|
+
await budget.revise();
|
|
146
|
+
|
|
147
|
+
assert.equal(said.length, 1, "working below what it needs is a fault of the machine, and invisible otherwise");
|
|
148
|
+
assert.match(said[0], /below the 64MB it needs/);
|
|
149
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Many viewers of one output are handed one session.
|
|
3
|
+
*
|
|
4
|
+
* This is the product behaviour the whole output key exists for: two people
|
|
5
|
+
* watching the same film, in the same form, at whatever moments, are served by
|
|
6
|
+
* one encode and one set of pieces. It had no check of any kind — every test
|
|
7
|
+
* that needed a session either built one by hand or replaced
|
|
8
|
+
* `createOrGetSession` with a stub — so the one line that decides it could have
|
|
9
|
+
* been broken by any edit without a word.
|
|
10
|
+
*
|
|
11
|
+
* What is checked here is the decision, not the creation: a session under the
|
|
12
|
+
* name this request produces already exists, and the request must be answered
|
|
13
|
+
* with it rather than with a new one. The name is the format produced, so the
|
|
14
|
+
* request is first taken as far as the format — the file's facts and its
|
|
15
|
+
* keyframe table are given as already known — and making the FIRST session,
|
|
16
|
+
* which reaches the disk, is a different subject.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import test from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
22
|
+
import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/encode/output/OutputSpec.js";
|
|
23
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
24
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
25
|
+
|
|
26
|
+
const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What a browser asks for: this file, copied, sound inside the stream.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} [over]
|
|
32
|
+
* @returns {object}
|
|
33
|
+
*/
|
|
34
|
+
function request(over = {}) {
|
|
35
|
+
return {
|
|
36
|
+
sourceKey: TORRENT,
|
|
37
|
+
fileIndex: 0,
|
|
38
|
+
fileName: "film.mkv",
|
|
39
|
+
transcodeVideo: false,
|
|
40
|
+
transcodeAudio: false,
|
|
41
|
+
audioTrackIndex: 0,
|
|
42
|
+
segmentFormatId: "fmp4",
|
|
43
|
+
...over
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The name the manager will look that request up under, built the way it builds
|
|
49
|
+
* it: a copied picture carrying its own sound, cut at the file's keyframes.
|
|
50
|
+
*
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function specOfThatOutput() {
|
|
54
|
+
return new OutputSpec({
|
|
55
|
+
sourceKey: TORRENT,
|
|
56
|
+
segmentFormatId: "fmp4",
|
|
57
|
+
grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
|
|
58
|
+
video: new VideoOutput({ fileIndex: 0, encode: null }),
|
|
59
|
+
audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: false })
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @returns {string} */
|
|
64
|
+
function nameOfThatOutput() {
|
|
65
|
+
return specOfThatOutput().toName();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {object} manager
|
|
70
|
+
* @returns {object} The session it is seeded with.
|
|
71
|
+
*/
|
|
72
|
+
function seedOneSession(manager) {
|
|
73
|
+
const session = {
|
|
74
|
+
id: nameOfThatOutput(),
|
|
75
|
+
spec: specOfThatOutput(),
|
|
76
|
+
outputKey: "seeded",
|
|
77
|
+
state: "ready",
|
|
78
|
+
file: new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }),
|
|
79
|
+
timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
|
|
80
|
+
runs: new Set(),
|
|
81
|
+
progress: { processedSeconds: 0, startPositionSeconds: 0, updatedAt: Date.now() },
|
|
82
|
+
lastAccessedAt: Date.now(),
|
|
83
|
+
// READY, because a joining viewer waits for the session to be able to
|
|
84
|
+
// answer them, and this file is about which session answers rather than
|
|
85
|
+
// about that wait. A session publishing its own playlist from the probed
|
|
86
|
+
// duration has nothing to wait for; left out, every check here would sit
|
|
87
|
+
// out the warm-up deadline and would be measuring that timer.
|
|
88
|
+
useSyntheticPlaylist: true
|
|
89
|
+
};
|
|
90
|
+
manager.outputs.set(session.id, session);
|
|
91
|
+
return session;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What the proxy already knows about the file when a second viewer arrives:
|
|
96
|
+
* what a probe said, and where its keyframes are.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} manager
|
|
99
|
+
*/
|
|
100
|
+
function knownFile(manager) {
|
|
101
|
+
manager.getCachedMediaInfo = () => ({ durationSeconds: 8, width: 1920, height: 1080, fps: 24 });
|
|
102
|
+
manager.keyframeTables.learn({ sourceKey: TORRENT, fileIndex: 0 }, { times: [0, 4], format: "test" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
test("a second viewer of one output is served by the session that exists", async (t) => {
|
|
106
|
+
const manager = wireOutputs({
|
|
107
|
+
enabled: true,
|
|
108
|
+
ffmpegBin: "ffmpeg",
|
|
109
|
+
localBindHost: "127.0.0.1",
|
|
110
|
+
localPort: 9090
|
|
111
|
+
});
|
|
112
|
+
t.after(() => manager.lifecycle.disposeAll());
|
|
113
|
+
// No plan runs here: this file is about which session answers a request, and
|
|
114
|
+
// placing encoders would spawn real processes.
|
|
115
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
116
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
117
|
+
knownFile(manager);
|
|
118
|
+
const seeded = seedOneSession(manager);
|
|
119
|
+
|
|
120
|
+
const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
|
|
121
|
+
|
|
122
|
+
assert.equal(answered, seeded, "one output, one session");
|
|
123
|
+
assert.ok(manager.viewers.forOutput(seeded).has("viewer-two"), "and the second viewer is on it");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a request whose parameters differ is not that session", async (t) => {
|
|
127
|
+
// The other half, and it is what makes the first mean anything: the name is
|
|
128
|
+
// the output's, so a request producing something else must not find it. Here
|
|
129
|
+
// the sound is to be re-encoded, which is a different output and a different
|
|
130
|
+
// set of bytes.
|
|
131
|
+
const manager = wireOutputs({
|
|
132
|
+
enabled: true,
|
|
133
|
+
ffmpegBin: "ffmpeg",
|
|
134
|
+
localBindHost: "127.0.0.1",
|
|
135
|
+
localPort: 9090,
|
|
136
|
+
// A session made for real here waits for a playlist that nothing is going
|
|
137
|
+
// to write. Zero, so this check spends no time on a deadline it is not
|
|
138
|
+
// about — the deadline has its own subject elsewhere.
|
|
139
|
+
startupWaitMs: 0
|
|
140
|
+
});
|
|
141
|
+
t.after(() => manager.lifecycle.disposeAll());
|
|
142
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
143
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
144
|
+
const seeded = seedOneSession(manager);
|
|
145
|
+
|
|
146
|
+
let answered = null;
|
|
147
|
+
try {
|
|
148
|
+
answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two", transcodeAudio: true }));
|
|
149
|
+
} catch {
|
|
150
|
+
// Making a session for real needs a probe and a disk, which this file does
|
|
151
|
+
// not give it. Failing there is the proof: it did not take the seeded one.
|
|
152
|
+
answered = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
assert.notEqual(answered, seeded);
|
|
156
|
+
assert.ok(!manager.viewers.forOutput(seeded).has("viewer-two"), "and nobody was added to somebody else's output");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("two screens that come to the same format share one output, and a different format does not", async (t) => {
|
|
160
|
+
const manager = wireOutputs({
|
|
161
|
+
enabled: true,
|
|
162
|
+
ffmpegBin: "ffmpeg",
|
|
163
|
+
localBindHost: "127.0.0.1",
|
|
164
|
+
localPort: 9090,
|
|
165
|
+
startupWaitMs: 0,
|
|
166
|
+
// A host with no ladder to choose from: the box asked for, fitted to the
|
|
167
|
+
// source, is what is produced.
|
|
168
|
+
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
169
|
+
});
|
|
170
|
+
t.after(() => manager.lifecycle.disposeAll());
|
|
171
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
172
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
173
|
+
knownFile(manager);
|
|
174
|
+
const spec = new OutputSpec({
|
|
175
|
+
sourceKey: TORRENT,
|
|
176
|
+
segmentFormatId: "fmp4",
|
|
177
|
+
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
178
|
+
video: new VideoOutput({
|
|
179
|
+
fileIndex: 0,
|
|
180
|
+
encode: { encoder: "h264_vaapi", width: 1920, height: 1080, fps: 24, preset: null, tonemap: false }
|
|
181
|
+
}),
|
|
182
|
+
audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: false })
|
|
183
|
+
});
|
|
184
|
+
const seeded = { ...seedOneSession(manager), id: spec.toName(), spec, timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "uniform" }) };
|
|
185
|
+
manager.outputs.delete(nameOfThatOutput());
|
|
186
|
+
manager.outputs.set(seeded.id, seeded);
|
|
187
|
+
|
|
188
|
+
// A taller window than the film: fitted to the source it is the same 1920x1080.
|
|
189
|
+
const joined = await manager.viewerRequests.createOrGetSession(
|
|
190
|
+
request({ consumerId: "tall-window", transcodeVideo: true, targetWidth: 1920, targetHeight: 1200 })
|
|
191
|
+
);
|
|
192
|
+
assert.equal(joined, seeded, "the key names the format produced, not the window it was asked for");
|
|
193
|
+
|
|
194
|
+
let other = null;
|
|
195
|
+
try {
|
|
196
|
+
other = await manager.viewerRequests.createOrGetSession(
|
|
197
|
+
request({ consumerId: "small-window", transcodeVideo: true, targetWidth: 1280, targetHeight: 720 })
|
|
198
|
+
);
|
|
199
|
+
} catch {
|
|
200
|
+
other = null;
|
|
201
|
+
}
|
|
202
|
+
assert.notEqual(other, seeded, "a smaller picture is another output");
|
|
203
|
+
assert.ok(!manager.viewers.forOutput(seeded).has("small-window"));
|
|
204
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import {
|
|
10
|
+
import { MachineBudget } from "../services/storage/MachineBudget.js";
|
|
11
11
|
|
|
12
12
|
const MEGABYTE = 1024 * 1024;
|
|
13
13
|
|
|
@@ -31,9 +31,29 @@ function consumerOf(name, held, wanted) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A budget over ONE resource, which is what this file is about: the division
|
|
36
|
+
* itself, with the several-resources question asked in `one-budget.test.js`.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ readFree: () => Promise<number | null>, logger?: object }} params
|
|
39
|
+
*/
|
|
40
|
+
function diskOf({ readFree, logger }) {
|
|
41
|
+
const budget = new MachineBudget({ logger });
|
|
42
|
+
budget.defineResource({ name: "disk", readFree });
|
|
43
|
+
return {
|
|
44
|
+
register: (consumer) => budget.register({ ...consumer, resource: "disk" }),
|
|
45
|
+
forget: (name) => budget.forget(name),
|
|
46
|
+
describe: () => budget.describe(),
|
|
47
|
+
revise: async () => {
|
|
48
|
+
const readings = await budget.revise();
|
|
49
|
+
return readings.get("disk") ?? { freeBytes: 0, allowanceBytes: 0, shares: [] };
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
test("what everyone may hold together never exceeds what the disk has", async () => {
|
|
35
55
|
const free = 100 * MEGABYTE;
|
|
36
|
-
const space =
|
|
56
|
+
const space = diskOf({ readFree: async () => free });
|
|
37
57
|
const segments = consumerOf("segments", 10 * MEGABYTE, 1000 * MEGABYTE);
|
|
38
58
|
const pieces = consumerOf("pieces", 20 * MEGABYTE, 1000 * MEGABYTE);
|
|
39
59
|
const diagnostics = consumerOf("diagnostics", 5 * MEGABYTE, 1000 * MEGABYTE);
|
|
@@ -55,7 +75,7 @@ test("what everyone may hold together never exceeds what the disk has", async ()
|
|
|
55
75
|
});
|
|
56
76
|
|
|
57
77
|
test("when everyone's ask fits, everyone gets it and the disk never binds", async () => {
|
|
58
|
-
const space =
|
|
78
|
+
const space = diskOf({ readFree: async () => 1000 * MEGABYTE });
|
|
59
79
|
const small = consumerOf("diagnostics", 0, 5 * MEGABYTE);
|
|
60
80
|
const large = consumerOf("segments", 0, 500 * MEGABYTE);
|
|
61
81
|
space.register(small.consumer);
|
|
@@ -70,7 +90,7 @@ test("when everyone's ask fits, everyone gets it and the disk never binds", asyn
|
|
|
70
90
|
test("when they do not fit, each is cut in proportion to what it asked", async () => {
|
|
71
91
|
// The same rule memory divides by, and the same reason: a share taken from
|
|
72
92
|
// whoever asked most would hand the disk to whoever grew fastest.
|
|
73
|
-
const space =
|
|
93
|
+
const space = diskOf({ readFree: async () => 200 * MEGABYTE });
|
|
74
94
|
const modest = consumerOf("diagnostics", 0, 100 * MEGABYTE);
|
|
75
95
|
const greedy = consumerOf("segments", 0, 300 * MEGABYTE);
|
|
76
96
|
space.register(modest.consumer);
|
|
@@ -85,7 +105,7 @@ test("when they do not fit, each is cut in proportion to what it asked", async (
|
|
|
85
105
|
|
|
86
106
|
test("a disk that fills lowers every share, rather than keeping one taken when it was empty", async () => {
|
|
87
107
|
let free = 1000 * MEGABYTE;
|
|
88
|
-
const space =
|
|
108
|
+
const space = diskOf({ readFree: async () => free });
|
|
89
109
|
const segments = consumerOf("segments", 0, 10_000 * MEGABYTE);
|
|
90
110
|
const pieces = consumerOf("pieces", 0, 10_000 * MEGABYTE);
|
|
91
111
|
space.register(segments.consumer);
|
|
@@ -106,7 +126,7 @@ test("what another process took is left for it, not spent", async () => {
|
|
|
106
126
|
// That is a measurement of somebody else's demand, and it is what the next
|
|
107
127
|
// allowance leaves alone.
|
|
108
128
|
let free = 1000 * MEGABYTE;
|
|
109
|
-
const space =
|
|
129
|
+
const space = diskOf({ readFree: async () => free });
|
|
110
130
|
const only = consumerOf("segments", 0, 10_000 * MEGABYTE);
|
|
111
131
|
space.register(only.consumer);
|
|
112
132
|
|
|
@@ -118,7 +138,7 @@ test("what another process took is left for it, not spent", async () => {
|
|
|
118
138
|
});
|
|
119
139
|
|
|
120
140
|
test("a disk that cannot be read allows nothing to grow", async () => {
|
|
121
|
-
const space =
|
|
141
|
+
const space = diskOf({ readFree: async () => null });
|
|
122
142
|
const segments = consumerOf("segments", 0, 500 * MEGABYTE);
|
|
123
143
|
space.register(segments.consumer);
|
|
124
144
|
|
|
@@ -130,7 +150,7 @@ test("a disk that cannot be read allows nothing to grow", async () => {
|
|
|
130
150
|
});
|
|
131
151
|
|
|
132
152
|
test("it says what the disk has and what each claimant may hold", async () => {
|
|
133
|
-
const space =
|
|
153
|
+
const space = diskOf({ readFree: async () => 100 * MEGABYTE });
|
|
134
154
|
assert.match(space.describe(), /nothing has claimed/);
|
|
135
155
|
space.register(consumerOf("segments", 4 * MEGABYTE, 8 * MEGABYTE).consumer);
|
|
136
156
|
await space.revise();
|
|
@@ -139,7 +159,7 @@ test("it says what the disk has and what each claimant may hold", async () => {
|
|
|
139
159
|
|
|
140
160
|
test("it says what it decided, every pass", async () => {
|
|
141
161
|
const lines = [];
|
|
142
|
-
const space =
|
|
162
|
+
const space = diskOf({
|
|
143
163
|
readFree: async () => 100 * MEGABYTE,
|
|
144
164
|
logger: { info: (line) => lines.push(line) }
|
|
145
165
|
});
|
|
@@ -154,7 +174,7 @@ test("the free space is read from the nearest directory that exists", async () =
|
|
|
154
174
|
// when the proxy stops, so at every start `statfs` on it fails. Field
|
|
155
175
|
// 2026-09-10: the first reading said `disk: 0MB free` on a host with 103 GB,
|
|
156
176
|
// and zero means "no room" to everything that reads it.
|
|
157
|
-
const { freeBytesFor } = await import("../services/
|
|
177
|
+
const { freeBytesFor } = await import("../services/storage/free.js");
|
|
158
178
|
const os = await import("node:os");
|
|
159
179
|
const path = await import("node:path");
|
|
160
180
|
const missing = path.join(os.tmpdir(), "no-such-directory-here", "nor-here");
|