@torrent-tv/proxy 2.89.17 → 2.89.18
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/README.md +14 -3
- package/package.json +10 -2
- package/server.js +0 -11
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2377
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -151
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/client-logs-route.test.js +0 -34
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
package/test/one-budget.test.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,273 +0,0 @@
|
|
|
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({ trackIndex = 0, transcode = false } = {}) {
|
|
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, transcode })
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** @returns {string} */
|
|
64
|
-
function nameOfThatOutput(sound) {
|
|
65
|
-
return specOfThatOutput(sound).toName();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @param {object} manager
|
|
70
|
-
* @param {{ trackIndex?: number, transcode?: boolean }} [sound] - The soundtrack
|
|
71
|
-
* the seeded output carries; the file's own, copied, when left out.
|
|
72
|
-
* @returns {object} The session it is seeded with.
|
|
73
|
-
*/
|
|
74
|
-
function seedOneSession(manager, sound = {}) {
|
|
75
|
-
const session = {
|
|
76
|
-
id: nameOfThatOutput(sound),
|
|
77
|
-
spec: specOfThatOutput(sound),
|
|
78
|
-
// Its own address, so two seeded outputs are two places for viewers.
|
|
79
|
-
outputKey: specOfThatOutput(sound).toKey(),
|
|
80
|
-
state: "ready",
|
|
81
|
-
file: new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }),
|
|
82
|
-
timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
|
|
83
|
-
runs: new Set(),
|
|
84
|
-
progress: { processedSeconds: 0, startPositionSeconds: 0, updatedAt: Date.now() },
|
|
85
|
-
lastAccessedAt: Date.now(),
|
|
86
|
-
// READY, because a joining viewer waits for the session to be able to
|
|
87
|
-
// answer them, and this file is about which session answers rather than
|
|
88
|
-
// about that wait. A session publishing its own playlist from the probed
|
|
89
|
-
// duration has nothing to wait for; left out, every check here would sit
|
|
90
|
-
// out the warm-up deadline and would be measuring that timer.
|
|
91
|
-
useSyntheticPlaylist: true
|
|
92
|
-
};
|
|
93
|
-
manager.outputs.set(session.id, session);
|
|
94
|
-
return session;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* What the proxy already knows about the file when a second viewer arrives:
|
|
99
|
-
* what a probe said, and where its keyframes are.
|
|
100
|
-
*
|
|
101
|
-
* @param {object} manager
|
|
102
|
-
*/
|
|
103
|
-
function knownFile(manager) {
|
|
104
|
-
// This check exercises output identity. Machine admission has its own tests.
|
|
105
|
-
manager.admission.admitsWatching = () => ({ admitted: true, reason: "", speedX: 8 });
|
|
106
|
-
manager.admission.previewCandidate = () => ({ admitted: true, reason: "", speedX: 8 });
|
|
107
|
-
manager.getCachedMediaInfo = () => ({ durationSeconds: 8, width: 1920, height: 1080, fps: 24 });
|
|
108
|
-
// The soundtracks the plan listed: the file's own, at the rate it states, and
|
|
109
|
-
// a dub beside it that states nothing — which no link can be asked about as a
|
|
110
|
-
// copy, so it is sent re-encoded.
|
|
111
|
-
manager.getCachedAudioTracks = () => [
|
|
112
|
-
{ index: 0, fileIndex: 0, sourceTrackIndex: 0, bitrateKbps: 128 },
|
|
113
|
-
{ index: 1, fileIndex: 0, sourceTrackIndex: 1, bitrateKbps: null, peakKbps: null }
|
|
114
|
-
];
|
|
115
|
-
manager.keyframeTables.learn({ sourceKey: TORRENT, fileIndex: 0 }, { times: [0, 4], format: "test" });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
test("a second viewer of one output is served by the session that exists", async (t) => {
|
|
119
|
-
const manager = wireOutputs({
|
|
120
|
-
enabled: true,
|
|
121
|
-
ffmpegBin: "ffmpeg",
|
|
122
|
-
localBindHost: "127.0.0.1",
|
|
123
|
-
localPort: 9090
|
|
124
|
-
});
|
|
125
|
-
t.after(() => manager.lifecycle.disposeAll());
|
|
126
|
-
// No plan runs here: this file is about which session answers a request, and
|
|
127
|
-
// placing encoders would spawn real processes.
|
|
128
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
129
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
130
|
-
knownFile(manager);
|
|
131
|
-
const seeded = seedOneSession(manager);
|
|
132
|
-
|
|
133
|
-
const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
|
|
134
|
-
|
|
135
|
-
assert.equal(answered, seeded, "one output, one session");
|
|
136
|
-
assert.ok(manager.viewers.forOutput(seeded).has("viewer-two"), "and the second viewer is on it");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("a request whose parameters differ is not that session", async (t) => {
|
|
140
|
-
// The other half, and it is what makes the first mean anything: the name is
|
|
141
|
-
// the output's, so a request producing something else must not find it. Here
|
|
142
|
-
// the sound is to be re-encoded, which is a different output and a different
|
|
143
|
-
// set of bytes.
|
|
144
|
-
const manager = wireOutputs({
|
|
145
|
-
enabled: true,
|
|
146
|
-
ffmpegBin: "ffmpeg",
|
|
147
|
-
localBindHost: "127.0.0.1",
|
|
148
|
-
localPort: 9090,
|
|
149
|
-
// A session made for real here waits for a playlist that nothing is going
|
|
150
|
-
// to write. Zero, so this check spends no time on a deadline it is not
|
|
151
|
-
// about — the deadline has its own subject elsewhere.
|
|
152
|
-
startupWaitMs: 0
|
|
153
|
-
});
|
|
154
|
-
t.after(() => manager.lifecycle.disposeAll());
|
|
155
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
156
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
157
|
-
const seeded = seedOneSession(manager);
|
|
158
|
-
|
|
159
|
-
let answered = null;
|
|
160
|
-
try {
|
|
161
|
-
answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two", transcodeAudio: true }));
|
|
162
|
-
} catch {
|
|
163
|
-
// Making a session for real needs a probe and a disk, which this file does
|
|
164
|
-
// not give it. Failing there is the proof: it did not take the seeded one.
|
|
165
|
-
answered = null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
assert.notEqual(answered, seeded);
|
|
169
|
-
assert.ok(!manager.viewers.forOutput(seeded).has("viewer-two"), "and nobody was added to somebody else's output");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("two screens that come to the same format share one output, and a different format does not", async (t) => {
|
|
173
|
-
const manager = wireOutputs({
|
|
174
|
-
enabled: true,
|
|
175
|
-
ffmpegBin: "ffmpeg",
|
|
176
|
-
localBindHost: "127.0.0.1",
|
|
177
|
-
localPort: 9090,
|
|
178
|
-
startupWaitMs: 0,
|
|
179
|
-
// A host with no ladder to choose from: the box asked for, fitted to the
|
|
180
|
-
// source, is what is produced.
|
|
181
|
-
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
182
|
-
});
|
|
183
|
-
t.after(() => manager.lifecycle.disposeAll());
|
|
184
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
185
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
186
|
-
knownFile(manager);
|
|
187
|
-
const spec = new OutputSpec({
|
|
188
|
-
sourceKey: TORRENT,
|
|
189
|
-
segmentFormatId: "fmp4",
|
|
190
|
-
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
191
|
-
video: new VideoOutput({
|
|
192
|
-
fileIndex: 0,
|
|
193
|
-
encode: { encoder: "h264_vaapi", width: 1920, height: 1080, fps: 24, preset: null, tonemap: false }
|
|
194
|
-
}),
|
|
195
|
-
audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: false })
|
|
196
|
-
});
|
|
197
|
-
const seeded = { ...seedOneSession(manager), id: spec.toName(), spec, timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "uniform" }) };
|
|
198
|
-
manager.outputs.delete(nameOfThatOutput());
|
|
199
|
-
manager.outputs.set(seeded.id, seeded);
|
|
200
|
-
|
|
201
|
-
// A taller window than the film: fitted to the source it is the same 1920x1080.
|
|
202
|
-
const joined = await manager.viewerRequests.createOrGetSession(
|
|
203
|
-
request({ consumerId: "tall-window", transcodeVideo: true, targetWidth: 1920, targetHeight: 1200 })
|
|
204
|
-
);
|
|
205
|
-
assert.equal(joined, seeded, "the key names the format produced, not the window it was asked for");
|
|
206
|
-
|
|
207
|
-
let other = null;
|
|
208
|
-
try {
|
|
209
|
-
other = await manager.viewerRequests.createOrGetSession(
|
|
210
|
-
request({ consumerId: "small-window", transcodeVideo: true, targetWidth: 1280, targetHeight: 720 })
|
|
211
|
-
);
|
|
212
|
-
} catch {
|
|
213
|
-
other = null;
|
|
214
|
-
}
|
|
215
|
-
assert.notEqual(other, seeded, "a smaller picture is another output");
|
|
216
|
-
assert.ok(!manager.viewers.forOutput(seeded).has("small-window"));
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("a remembered dub with no stated rate opens on the output that sends it as AAC, not on its copy", async (t) => {
|
|
220
|
-
// The field case of 2026-10-01: the next episode opened on a dub the page
|
|
221
|
-
// remembered, which its browser plays as it stands and nothing states a rate
|
|
222
|
-
// for. Opened as a copy, no link could be asked about it and the episode was
|
|
223
|
-
// refused; it is the AAC output that answers.
|
|
224
|
-
const manager = wireOutputs({
|
|
225
|
-
enabled: true,
|
|
226
|
-
ffmpegBin: "ffmpeg",
|
|
227
|
-
localBindHost: "127.0.0.1",
|
|
228
|
-
localPort: 9090,
|
|
229
|
-
// A measured copy speed, so this machine can price the output and the
|
|
230
|
-
// admission answers on room rather than on knowing nothing.
|
|
231
|
-
copySpeedX: 20
|
|
232
|
-
});
|
|
233
|
-
t.after(() => manager.lifecycle.disposeAll());
|
|
234
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
235
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
236
|
-
knownFile(manager);
|
|
237
|
-
const copied = seedOneSession(manager, { trackIndex: 1, transcode: false });
|
|
238
|
-
const encoded = seedOneSession(manager, { trackIndex: 1, transcode: true });
|
|
239
|
-
|
|
240
|
-
const answered = await manager.viewerRequests.createOrGetSession(
|
|
241
|
-
request({ consumerId: "viewer-two", audioTrackIndex: 1, transcodeAudio: false })
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
assert.equal(answered, encoded, "the dub is re-encoded");
|
|
245
|
-
assert.notEqual(answered, copied);
|
|
246
|
-
assert.deepEqual(
|
|
247
|
-
manager.renditions.soundtrackOf(answered, "viewer-two"),
|
|
248
|
-
{ trackIndex: 1, transcode: true },
|
|
249
|
-
"and the viewer is recorded as receiving AAC"
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("a track whose rate is stated is still copied for a browser that plays it", async (t) => {
|
|
254
|
-
const manager = wireOutputs({
|
|
255
|
-
enabled: true,
|
|
256
|
-
ffmpegBin: "ffmpeg",
|
|
257
|
-
localBindHost: "127.0.0.1",
|
|
258
|
-
localPort: 9090,
|
|
259
|
-
// A measured copy speed, so this machine can price the output and the
|
|
260
|
-
// admission answers on room rather than on knowing nothing.
|
|
261
|
-
copySpeedX: 20
|
|
262
|
-
});
|
|
263
|
-
t.after(() => manager.lifecycle.disposeAll());
|
|
264
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
265
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
266
|
-
knownFile(manager);
|
|
267
|
-
const copied = seedOneSession(manager);
|
|
268
|
-
seedOneSession(manager, { trackIndex: 0, transcode: true });
|
|
269
|
-
|
|
270
|
-
const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
|
|
271
|
-
|
|
272
|
-
assert.equal(answered, copied);
|
|
273
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One disk, one owner, three claimants.
|
|
3
|
-
*
|
|
4
|
-
* What these pin is the thing that was wrong: each consumer read the free space
|
|
5
|
-
* as though it were alone, so three ceilings each stood for the whole disk.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import test from "node:test";
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
|
-
import { MachineBudget } from "../services/storage/MachineBudget.js";
|
|
11
|
-
|
|
12
|
-
const MEGABYTE = 1024 * 1024;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {string} name
|
|
16
|
-
* @param {number} held
|
|
17
|
-
* @param {number} wanted
|
|
18
|
-
*/
|
|
19
|
-
function consumerOf(name, held, wanted) {
|
|
20
|
-
const state = { allowed: -1 };
|
|
21
|
-
return {
|
|
22
|
-
state,
|
|
23
|
-
consumer: {
|
|
24
|
-
name,
|
|
25
|
-
held: () => held,
|
|
26
|
-
wanted: () => wanted,
|
|
27
|
-
allow: (bytes) => {
|
|
28
|
-
state.allowed = bytes;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
}
|
|
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
|
-
|
|
54
|
-
test("what everyone may hold together never exceeds what the disk has", async () => {
|
|
55
|
-
const free = 100 * MEGABYTE;
|
|
56
|
-
const space = diskOf({ readFree: async () => free });
|
|
57
|
-
const segments = consumerOf("segments", 10 * MEGABYTE, 1000 * MEGABYTE);
|
|
58
|
-
const pieces = consumerOf("pieces", 20 * MEGABYTE, 1000 * MEGABYTE);
|
|
59
|
-
const diagnostics = consumerOf("diagnostics", 5 * MEGABYTE, 1000 * MEGABYTE);
|
|
60
|
-
space.register(segments.consumer);
|
|
61
|
-
space.register(pieces.consumer);
|
|
62
|
-
space.register(diagnostics.consumer);
|
|
63
|
-
|
|
64
|
-
const { allowanceBytes } = await space.revise();
|
|
65
|
-
|
|
66
|
-
const together = segments.state.allowed + pieces.state.allowed + diagnostics.state.allowed;
|
|
67
|
-
assert.ok(together <= allowanceBytes, "the shares add up to more than the allowance");
|
|
68
|
-
// Free space plus what they already hold: that pair is the ceiling they could
|
|
69
|
-
// reach, which is the same statement the memory allowance makes.
|
|
70
|
-
assert.equal(allowanceBytes, free + 35 * MEGABYTE);
|
|
71
|
-
assert.ok(
|
|
72
|
-
together <= free + 35 * MEGABYTE,
|
|
73
|
-
"three consumers were between them allowed more than the disk has"
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("when everyone's ask fits, everyone gets it and the disk never binds", async () => {
|
|
78
|
-
const space = diskOf({ readFree: async () => 1000 * MEGABYTE });
|
|
79
|
-
const small = consumerOf("diagnostics", 0, 5 * MEGABYTE);
|
|
80
|
-
const large = consumerOf("segments", 0, 500 * MEGABYTE);
|
|
81
|
-
space.register(small.consumer);
|
|
82
|
-
space.register(large.consumer);
|
|
83
|
-
|
|
84
|
-
await space.revise();
|
|
85
|
-
|
|
86
|
-
assert.equal(small.state.allowed, 5 * MEGABYTE);
|
|
87
|
-
assert.equal(large.state.allowed, 500 * MEGABYTE);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("when they do not fit, each is cut in proportion to what it asked", async () => {
|
|
91
|
-
// The same rule memory divides by, and the same reason: a share taken from
|
|
92
|
-
// whoever asked most would hand the disk to whoever grew fastest.
|
|
93
|
-
const space = diskOf({ readFree: async () => 200 * MEGABYTE });
|
|
94
|
-
const modest = consumerOf("diagnostics", 0, 100 * MEGABYTE);
|
|
95
|
-
const greedy = consumerOf("segments", 0, 300 * MEGABYTE);
|
|
96
|
-
space.register(modest.consumer);
|
|
97
|
-
space.register(greedy.consumer);
|
|
98
|
-
|
|
99
|
-
await space.revise();
|
|
100
|
-
|
|
101
|
-
assert.equal(modest.state.allowed, 50 * MEGABYTE);
|
|
102
|
-
assert.equal(greedy.state.allowed, 150 * MEGABYTE);
|
|
103
|
-
assert.equal(modest.state.allowed + greedy.state.allowed, 200 * MEGABYTE);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a disk that fills lowers every share, rather than keeping one taken when it was empty", async () => {
|
|
107
|
-
let free = 1000 * MEGABYTE;
|
|
108
|
-
const space = diskOf({ readFree: async () => free });
|
|
109
|
-
const segments = consumerOf("segments", 0, 10_000 * MEGABYTE);
|
|
110
|
-
const pieces = consumerOf("pieces", 0, 10_000 * MEGABYTE);
|
|
111
|
-
space.register(segments.consumer);
|
|
112
|
-
space.register(pieces.consumer);
|
|
113
|
-
|
|
114
|
-
await space.revise();
|
|
115
|
-
const roomy = segments.state.allowed;
|
|
116
|
-
|
|
117
|
-
free = 40 * MEGABYTE;
|
|
118
|
-
await space.revise();
|
|
119
|
-
|
|
120
|
-
assert.ok(segments.state.allowed < roomy, "the share did not follow the disk down");
|
|
121
|
-
assert.ok(pieces.state.allowed < roomy, "the other share did not follow either");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("what another process took is left for it, not spent", async () => {
|
|
125
|
-
// Between two readings the disk lost 400 MB that none of our consumers took.
|
|
126
|
-
// That is a measurement of somebody else's demand, and it is what the next
|
|
127
|
-
// allowance leaves alone.
|
|
128
|
-
let free = 1000 * MEGABYTE;
|
|
129
|
-
const space = diskOf({ readFree: async () => free });
|
|
130
|
-
const only = consumerOf("segments", 0, 10_000 * MEGABYTE);
|
|
131
|
-
space.register(only.consumer);
|
|
132
|
-
|
|
133
|
-
await space.revise();
|
|
134
|
-
free = 600 * MEGABYTE;
|
|
135
|
-
const { allowanceBytes } = await space.revise();
|
|
136
|
-
|
|
137
|
-
assert.equal(allowanceBytes, 200 * MEGABYTE, "the fall we did not cause was spent anyway");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("a disk that cannot be read allows nothing to grow", async () => {
|
|
141
|
-
const space = diskOf({ readFree: async () => null });
|
|
142
|
-
const segments = consumerOf("segments", 0, 500 * MEGABYTE);
|
|
143
|
-
space.register(segments.consumer);
|
|
144
|
-
|
|
145
|
-
const { freeBytes, allowanceBytes } = await space.revise();
|
|
146
|
-
|
|
147
|
-
assert.equal(freeBytes, 0);
|
|
148
|
-
assert.equal(allowanceBytes, 0);
|
|
149
|
-
assert.equal(segments.state.allowed, 0, "an unreadable disk was treated as an empty one");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("it says what the disk has and what each claimant may hold", async () => {
|
|
153
|
-
const space = diskOf({ readFree: async () => 100 * MEGABYTE });
|
|
154
|
-
assert.match(space.describe(), /nothing has claimed/);
|
|
155
|
-
space.register(consumerOf("segments", 4 * MEGABYTE, 8 * MEGABYTE).consumer);
|
|
156
|
-
await space.revise();
|
|
157
|
-
assert.match(space.describe(), /disk: 100MB free; segments 4MB of 8MB/);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("it says what it decided, every pass", async () => {
|
|
161
|
-
const lines = [];
|
|
162
|
-
const space = diskOf({
|
|
163
|
-
readFree: async () => 100 * MEGABYTE,
|
|
164
|
-
logger: { info: (line) => lines.push(line) }
|
|
165
|
-
});
|
|
166
|
-
space.register(consumerOf("segments", 4 * MEGABYTE, 8 * MEGABYTE).consumer);
|
|
167
|
-
await space.revise();
|
|
168
|
-
assert.equal(lines.length, 1, "a pass that decided the shares said nothing about them");
|
|
169
|
-
assert.match(lines[0], /disk: 100MB free; segments 4MB of 8MB/);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("the free space is read from the nearest directory that exists", async () => {
|
|
173
|
-
// The segments' directory is made when the first session starts and removed
|
|
174
|
-
// when the proxy stops, so at every start `statfs` on it fails. Field
|
|
175
|
-
// 2026-09-10: the first reading said `disk: 0MB free` on a host with 103 GB,
|
|
176
|
-
// and zero means "no room" to everything that reads it.
|
|
177
|
-
const { freeBytesFor } = await import("../services/storage/free.js");
|
|
178
|
-
const os = await import("node:os");
|
|
179
|
-
const path = await import("node:path");
|
|
180
|
-
const missing = path.join(os.tmpdir(), "no-such-directory-here", "nor-here");
|
|
181
|
-
|
|
182
|
-
const bytes = await freeBytesFor(missing);
|
|
183
|
-
|
|
184
|
-
assert.ok(Number.isFinite(bytes) && bytes > 0, "a directory that does not exist read as no disk");
|
|
185
|
-
});
|