@torrent-tv/proxy 2.88.2 → 2.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2259 -2246
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +23 -4
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1150 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -254
- package/services/encode/quality/output-format.js +319 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +21 -6
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1244
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -0
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +13 -8
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -0
- package/test/fmp4-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +270 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -0
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
# Disk architecture — one owner, and what goes first when it is short
|
|
2
|
-
|
|
3
|
-
Three things on this proxy write to the same disk. Until 2026-09-10 each read
|
|
4
|
-
the free space as though it were the only claimant, so there were three
|
|
5
|
-
ceilings, each standing for the whole disk.
|
|
6
|
-
|
|
7
|
-
| what | where | what bounded it before |
|
|
8
|
-
|---|---|---|
|
|
9
|
-
| segments an encoder produced | `services/encode/SegmentStore.js` | a quarter of what was free, plus a 2 GB floor — both numbers chosen by hand |
|
|
10
|
-
| pieces the memory store spilled | `services/storage/piece-store/piece-disk-store.js` | **nothing at all** |
|
|
11
|
-
| diagnostics kept on purpose | `/data` — core dumps, heap snapshots, packet captures | a count of files, never a size |
|
|
12
|
-
|
|
13
|
-
The middle row is what it cost: one fifty-minute viewing wrote **14 400 MB** to
|
|
14
|
-
the spill file (field 2026-08-31) while the store held 312-424 MB, and free
|
|
15
|
-
space on the host fell by every megabyte of it. The bottom row is 2.9 GB of one
|
|
16
|
-
core dump on the addon host, inside a budget of "two dumps".
|
|
17
|
-
|
|
18
|
-
## One owner
|
|
19
|
-
|
|
20
|
-
`services/disk/DiskSpace.js` reads the free space once and divides it. The rule
|
|
21
|
-
is the one memory already uses, on the other reading:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
allowance = free now + what we already hold - what everything that is not us has been seen to need
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Nothing in it is a fraction chosen by hand. The reserve is measured: between two
|
|
28
|
-
readings, how much free space went away beyond what our own consumers took.
|
|
29
|
-
|
|
30
|
-
`services/disk/wire.js` says who the claimants are. The segments are told their
|
|
31
|
-
share by a call; the spilled pieces live on the torrent thread, so the share
|
|
32
|
-
travels the channel that already carries everything else and the reply says what
|
|
33
|
-
they hold — one exchange, both directions.
|
|
34
|
-
|
|
35
|
-
The reading is said every pass, in the series beside the memory one:
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
disk: 104584MB free; segments 0MB of 52292MB, spilled pieces 0MB of 52292MB
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Two rules, answering two questions
|
|
42
|
-
|
|
43
|
-
They are kept apart because they were briefly proposed as one, and that was
|
|
44
|
-
wrong.
|
|
45
|
-
|
|
46
|
-
**Time.** Material nobody needs should not sit on the owner's disk merely
|
|
47
|
-
because there is room for it. An output nobody has read for long enough goes
|
|
48
|
-
whole, whatever the free space is.
|
|
49
|
-
|
|
50
|
-
**Space.** Material everyone needs must still go when there is no room.
|
|
51
|
-
|
|
52
|
-
A ceiling alone would keep 5 GB on a household disk for six hours because the
|
|
53
|
-
disk is large. An idle rule alone would keep everything until its clock ran out,
|
|
54
|
-
however tight the disk had become.
|
|
55
|
-
|
|
56
|
-
## What goes first, and it is the viewers who decide
|
|
57
|
-
|
|
58
|
-
For the segments, the order is the priority map's own read from the other end:
|
|
59
|
-
|
|
60
|
-
1. outputs nobody is watching at all;
|
|
61
|
-
2. what lies behind the earliest viewer — furthest behind first;
|
|
62
|
-
3. what lies ahead of the furthest viewer — furthest ahead first.
|
|
63
|
-
|
|
64
|
-
A segment a viewer is standing on is never a victim. It used to be whole outputs
|
|
65
|
-
by when their directory was last read, which says nothing about what anybody is
|
|
66
|
-
about to watch.
|
|
67
|
-
|
|
68
|
-
## A piece is a file
|
|
69
|
-
|
|
70
|
-
`DiskTier` wrote every spilled piece into ONE sparse file at
|
|
71
|
-
`index * chunkLength` and answered `forget(index)` by dropping the number from a
|
|
72
|
-
set. The bytes stayed: a sparse file's blocks come back only by hole punching,
|
|
73
|
-
which Node exposes no binding for, so nothing this process could do returned a
|
|
74
|
-
single block before the whole file was removed.
|
|
75
|
-
|
|
76
|
-
`PieceDiskStore` gives each piece its own file. Removing one returns exactly its
|
|
77
|
-
blocks, needs no binding this runtime lacks, and makes the unit of eviction the
|
|
78
|
-
same as the unit of storage — so the order pieces leave in is the order we
|
|
79
|
-
choose. Over its allowance the least recently used goes, and a piece being read
|
|
80
|
-
is never the victim. A piece thrown away is answered as absent, so the torrent
|
|
81
|
-
fetches it again, which is the same bargain the memory tier makes when it
|
|
82
|
-
spills.
|
|
83
|
-
|
|
84
|
-
The read this replaced was chosen for its cost — 22.08 ms via `readFile` against
|
|
85
|
-
**7.63 ms** into a buffer we already hold, measured on the field host — and that
|
|
86
|
-
is unchanged: it is still one `read` into the caller's buffer. What it adds is
|
|
87
|
-
an `open` per read, tens of microseconds against those milliseconds.
|
|
88
|
-
|
|
89
|
-
## A clean exit leaves nothing of ours
|
|
90
|
-
|
|
91
|
-
The segments' root was removed only when it happened to be empty — from the
|
|
92
|
-
first commit of this repository, never a decision. What it protected against is
|
|
93
|
-
a second proxy sharing the root; what it did was leave every directory alone,
|
|
94
|
-
including this process's own orphans. A directory adopted at startup, owned by
|
|
95
|
-
no session, therefore survived the exit and was adopted again at the next start.
|
|
96
|
-
|
|
97
|
-
That loop is what made an orphan permanent, and it is why 5.0 GB of segments
|
|
98
|
-
from sessions that had ended hours before were on the addon host on 2026-09-10.
|
|
99
|
-
`SegmentStore.dropAll` now removes everything this process owns, root included,
|
|
100
|
-
which also gives the startup sweep its meaning back: whatever is found then is
|
|
101
|
-
from a kill.
|
|
102
|
-
|
|
103
|
-
Both kinds have both rules. For the spilled pieces the time rule is the same
|
|
104
|
-
statement one layer down: a piece behind every read head has been read and will
|
|
105
|
-
not be read again unless somebody seeks back, and a seek back re-downloads it —
|
|
106
|
-
the bargain this tier already makes whenever it drops a piece for room.
|
|
107
|
-
`PieceLru.readHeads` says where the readers stand, `PieceDiskStore.forgetBehind`
|
|
108
|
-
acts on it, and the eviction order under pressure is the same: behind the
|
|
109
|
-
earliest reader first, furthest behind first of all. With no reader at all
|
|
110
|
-
nothing is removed — a store between reads is not a store nobody wants, and the
|
|
111
|
-
torrent going idle is what empties it whole.
|
|
112
|
-
|
|
113
|
-
## How long material nobody is using is kept
|
|
114
|
-
|
|
115
|
-
One number, in one place — `services/disk/keep.js`, one hour — because
|
|
116
|
-
everything it governs stands for the same unmeasured thing: whether the viewer
|
|
117
|
-
comes back.
|
|
118
|
-
|
|
119
|
-
It was three, and they contradicted each other:
|
|
120
|
-
|
|
121
|
-
| what | was | now |
|
|
122
|
-
|---|---|---|
|
|
123
|
-
| a torrent and its downloaded bytes | 15 minutes | one hour |
|
|
124
|
-
| a session | 30 minutes | unchanged — a session is a record, not material |
|
|
125
|
-
| the segments an encoder produced | 6 hours | one hour |
|
|
126
|
-
|
|
127
|
-
The torrent went at fifteen minutes while the session it feeds lived to thirty,
|
|
128
|
-
so between them there was a session with no source: a viewer returning at the
|
|
129
|
-
twentieth minute got a session that could not make a single new segment. With
|
|
130
|
-
one hour for both kinds of material the session dies first, which is the right
|
|
131
|
-
order and needs no rule of its own to enforce.
|
|
132
|
-
|
|
133
|
-
Nothing derives the hour. Everything else in the decision is measured — a piece
|
|
134
|
-
comes back from the swarm in ~1430 ms, a segment in its own encode time, and the
|
|
135
|
-
disk has an owner that prices holding it — and only the return is unknown.
|
|
136
|
-
|
|
137
|
-
**And the return is measurable here.** A session opened on an output whose
|
|
138
|
-
segments are still on disk IS a return, and its age is exactly what the store
|
|
139
|
-
recorded. `services/disk/returns.js` keeps them and says so beside the disk
|
|
140
|
-
figures:
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
returns: 14 session(s) opened on material still held, 3 on material gone;
|
|
144
|
-
median 12min after the last read, longest 51min — kept for 60min
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
The median and the longest are the two the period has to sit between: shorter
|
|
148
|
-
than the median throws away material half the returns wanted, longer than the
|
|
149
|
-
longest keeps material no return has ever reached. A week of ordinary use and
|
|
150
|
-
the hour is replaced by what viewers actually do — and then the two kinds can
|
|
151
|
-
have different numbers, since their costs of coming back differ.
|
|
152
|
-
|
|
153
|
-
## What is NOT solved
|
|
154
|
-
|
|
155
|
-
**The measurement that would replace the hour has not been taken yet.** The
|
|
156
|
-
counter is in place and says nothing until a week of ordinary use has produced a
|
|
157
|
-
distribution.
|
|
158
|
-
|
|
159
|
-
**The diagnostics are not a claimant yet.** They are bounded by a count of files
|
|
160
|
-
and never by a size, and they cannot simply be thrown away when space is short: a
|
|
161
|
-
dump is the only evidence of the death it records. That needs a rule of its own.
|
|
162
|
-
|
|
163
|
-
## One layer, four directories, and under the import rule
|
|
164
|
-
|
|
165
|
-
`SegmentStore` moved out of `services/encode/` on 2026-09-14 — a store of
|
|
166
|
-
finished pieces inside the ENCODING layer was the encoder owning its own disk.
|
|
167
|
-
It did not move into the room's own directory either: it is a CLAIMANT, and a
|
|
168
|
-
claimant inside the owner of the resource is the same fault one floor down.
|
|
169
|
-
|
|
170
|
-
**Four directories, deliberately.** They share one property — bytes on a medium
|
|
171
|
-
with a limit — and that is what makes them one layer. What they are is three
|
|
172
|
-
different things: a torrent piece is born when it is downloaded and addressed by
|
|
173
|
-
`(infohash, number)`; a produced segment is born when an encoder closes it and
|
|
174
|
-
addressed by `(output key, number)`; a whole file is born when its last piece
|
|
175
|
-
lands. Different birth, different death, different name, and two of the three
|
|
176
|
-
live in the torrent WORKER thread while the others are on the main one. Flatten
|
|
177
|
-
them and that thread boundary becomes invisible: code that cannot call code
|
|
178
|
-
looks like a sibling, and no rule catches it.
|
|
179
|
-
|
|
180
|
-
**The rule is written as an exception, not as a list.** `group: ["../**",
|
|
181
|
-
"!../storage/**", "!../piece-store/**", "!../segment-store/**", "!../files/**"]`
|
|
182
|
-
— everything outside the layer, minus the layer. A list of forbidden layers was
|
|
183
|
-
tried first and let `../../utils/logger.js` straight through, because a list
|
|
184
|
-
only catches what somebody remembered to write in it. Checked by breaking it,
|
|
185
|
-
in both directions, in all four directories.
|
|
186
|
-
|
|
187
|
-
**One reader of how much memory is free** (`machine-memory.js`). It had been
|
|
188
|
-
written three times — the health collector, the memory report and the piece
|
|
189
|
-
store — and the copies had already drifted: the piece store was corrected from
|
|
190
|
-
`os.freemem()` to the kernel's `MemAvailable` on 2026-08-27 and the health
|
|
191
|
-
collector went on publishing the wrong quantity until 2026-09-02.
|
|
192
|
-
|
|
193
|
-
## Every claimant is told its share
|
|
194
|
-
|
|
195
|
-
A CLAIMANT IS WHOEVER HOLDS BYTES, and by 2026-09-14 all of them are registered
|
|
196
|
-
with the owner (`wire.js`): the segments an encoder produced, the pieces the
|
|
197
|
-
memory store spilled, the files downloaded whole, and the evidence. Two of the
|
|
198
|
-
four live on the torrent thread and arrive as a pair of closures over its
|
|
199
|
-
channel.
|
|
200
|
-
|
|
201
|
-
**Whole files had no bound of any kind** until that day — a 2.8 GB film kept
|
|
202
|
-
whole on a host whose disk is often a 32 GB card. They now take a share, and
|
|
203
|
-
over it the whole file nobody has asked for in longest goes. Losing one is not
|
|
204
|
-
losing data: the torrent can fetch it again, and until it does the read falls
|
|
205
|
-
back to the pieces, which is what `piece-from-whole-file.js` exists for. A file
|
|
206
|
-
is also not ASSEMBLED when there is no room for it — writing it and then
|
|
207
|
-
removing it is the same bytes written for nothing.
|
|
208
|
-
|
|
209
|
-
**The torrent pool's own "disk cap" is gone, and it owned nothing.** It capped
|
|
210
|
-
`torrentDownloadedBytes` — a sum over WebTorrent's bitfield — so a piece held
|
|
211
|
-
purely in MEMORY told against a ceiling called disk, while the bytes it meant to
|
|
212
|
-
bound belong to the spill and to the whole files, each of which has an owner.
|
|
213
|
-
Under it, eviction removed whole idle TORRENTS to free bytes that were not
|
|
214
|
-
necessarily on the disk at all.
|
|
215
|
-
|
|
216
|
-
**The evidence is a claimant with a rule of its own** (`Diagnostics.js`), and the
|
|
217
|
-
rule is what makes a bound on it safe:
|
|
218
|
-
|
|
219
|
-
1. over its share, collection STOPS. Nothing already recorded is deleted to make
|
|
220
|
-
room — a dump is the only evidence of the death it records — and a refusal is
|
|
221
|
-
a LINE with the figures, so an investigation that finds nothing can tell "it
|
|
222
|
-
did not happen" from "there was nowhere to put it";
|
|
223
|
-
2. except what is superseded, which is not evidence twice. A capture of a
|
|
224
|
-
connection already captured adds nothing: the wedge does not un-wedge. Field
|
|
225
|
-
2026-09-06, thirteen captures of one wedge in six hours, 74 MB, every one
|
|
226
|
-
saying what the first said;
|
|
227
|
-
3. what it asks for is measured — what it holds plus one more of the largest
|
|
228
|
-
kind seen — so the claim grows with the evidence and a proxy that has never
|
|
229
|
-
faulted asks for nothing.
|
|
230
|
-
|
|
231
|
-
**Core dumps are not ours to refuse, and that is stated rather than papered
|
|
232
|
-
over.** The kernel writes them, whole address space at a time, and no gate of
|
|
233
|
-
ours is consulted. They are counted, so they take room from what the product may
|
|
234
|
-
hold and the figure is visible; the pruning that keeps the newest two is left
|
|
235
|
-
where it is, because changing it is a decision about evidence and not about
|
|
236
|
-
disk.
|
|
237
|
-
|
|
238
|
-
## One budget
|
|
239
|
-
|
|
240
|
-
There is one owner, one policy and one place that reads the machine —
|
|
241
|
-
`MachineBudget.js`. What it divides is divided PER RESOURCE, because memory
|
|
242
|
-
cannot be paid for with disk, and "disk" is one resource per DEVICE: measured on
|
|
243
|
-
the addon host 2026-09-05, `/tmp` (the segments and the spill) is the overlay
|
|
244
|
-
and `/data` (the evidence) is ext4 on the nvme. Dividing one figure between
|
|
245
|
-
claimants on both gave each a share of a disk it does not write to, which is
|
|
246
|
-
what the old `DiskSpace` did — it read the free space of the segment root alone.
|
|
247
|
-
The device is read with `statSync(dir).dev`, from the nearest ancestor that
|
|
248
|
-
exists, for the same reason `freeBytesFor` walks up.
|
|
249
|
-
|
|
250
|
-
**Why one owner and not two.** The claimants trade across resources: pieces that
|
|
251
|
-
do not fit in memory are spilled to disk, whole files exist so that pieces need
|
|
252
|
-
not be held, segments exist because making them again is dear. Field 2026-08-31:
|
|
253
|
-
14 400 MB spilled in fifty minutes while the memory store held 312-424 MB — give
|
|
254
|
-
it memory and there is no spill to bound. Two owners cannot make that trade,
|
|
255
|
-
because neither sees both sides of it. Memory was divided inside the torrent
|
|
256
|
-
thread and disk on the main one until 2026-09-14; the memory share now travels
|
|
257
|
-
the same channel the spill share already did.
|
|
258
|
-
|
|
259
|
-
**The policy is the operator's, and it is the only chosen number here.**
|
|
260
|
-
`--budget adaptive|share|fixed` with `--budget-share` / `--budget-bytes`, and
|
|
261
|
-
floors with `--min-memory-bytes` / `--min-disk-bytes`. The default is
|
|
262
|
-
`adaptive`, which is the measured rule of `allowance.js` and chooses nothing. A
|
|
263
|
-
floor lifts what the policy allows and can never invent room the machine does
|
|
264
|
-
not have; a claimant left below its own minimum says so in a line, because
|
|
265
|
-
working below what it needs is a fault of the machine and invisible otherwise.
|
|
266
|
-
|
|
267
|
-
|
|
1
|
+
# Disk architecture — one owner, and what goes first when it is short
|
|
2
|
+
|
|
3
|
+
Three things on this proxy write to the same disk. Until 2026-09-10 each read
|
|
4
|
+
the free space as though it were the only claimant, so there were three
|
|
5
|
+
ceilings, each standing for the whole disk.
|
|
6
|
+
|
|
7
|
+
| what | where | what bounded it before |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| segments an encoder produced | `services/encode/SegmentStore.js` | a quarter of what was free, plus a 2 GB floor — both numbers chosen by hand |
|
|
10
|
+
| pieces the memory store spilled | `services/storage/piece-store/piece-disk-store.js` | **nothing at all** |
|
|
11
|
+
| diagnostics kept on purpose | `/data` — core dumps, heap snapshots, packet captures | a count of files, never a size |
|
|
12
|
+
|
|
13
|
+
The middle row is what it cost: one fifty-minute viewing wrote **14 400 MB** to
|
|
14
|
+
the spill file (field 2026-08-31) while the store held 312-424 MB, and free
|
|
15
|
+
space on the host fell by every megabyte of it. The bottom row is 2.9 GB of one
|
|
16
|
+
core dump on the addon host, inside a budget of "two dumps".
|
|
17
|
+
|
|
18
|
+
## One owner
|
|
19
|
+
|
|
20
|
+
`services/disk/DiskSpace.js` reads the free space once and divides it. The rule
|
|
21
|
+
is the one memory already uses, on the other reading:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
allowance = free now + what we already hold - what everything that is not us has been seen to need
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Nothing in it is a fraction chosen by hand. The reserve is measured: between two
|
|
28
|
+
readings, how much free space went away beyond what our own consumers took.
|
|
29
|
+
|
|
30
|
+
`services/disk/wire.js` says who the claimants are. The segments are told their
|
|
31
|
+
share by a call; the spilled pieces live on the torrent thread, so the share
|
|
32
|
+
travels the channel that already carries everything else and the reply says what
|
|
33
|
+
they hold — one exchange, both directions.
|
|
34
|
+
|
|
35
|
+
The reading is said every pass, in the series beside the memory one:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
disk: 104584MB free; segments 0MB of 52292MB, spilled pieces 0MB of 52292MB
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Two rules, answering two questions
|
|
42
|
+
|
|
43
|
+
They are kept apart because they were briefly proposed as one, and that was
|
|
44
|
+
wrong.
|
|
45
|
+
|
|
46
|
+
**Time.** Material nobody needs should not sit on the owner's disk merely
|
|
47
|
+
because there is room for it. An output nobody has read for long enough goes
|
|
48
|
+
whole, whatever the free space is.
|
|
49
|
+
|
|
50
|
+
**Space.** Material everyone needs must still go when there is no room.
|
|
51
|
+
|
|
52
|
+
A ceiling alone would keep 5 GB on a household disk for six hours because the
|
|
53
|
+
disk is large. An idle rule alone would keep everything until its clock ran out,
|
|
54
|
+
however tight the disk had become.
|
|
55
|
+
|
|
56
|
+
## What goes first, and it is the viewers who decide
|
|
57
|
+
|
|
58
|
+
For the segments, the order is the priority map's own read from the other end:
|
|
59
|
+
|
|
60
|
+
1. outputs nobody is watching at all;
|
|
61
|
+
2. what lies behind the earliest viewer — furthest behind first;
|
|
62
|
+
3. what lies ahead of the furthest viewer — furthest ahead first.
|
|
63
|
+
|
|
64
|
+
A segment a viewer is standing on is never a victim. It used to be whole outputs
|
|
65
|
+
by when their directory was last read, which says nothing about what anybody is
|
|
66
|
+
about to watch.
|
|
67
|
+
|
|
68
|
+
## A piece is a file
|
|
69
|
+
|
|
70
|
+
`DiskTier` wrote every spilled piece into ONE sparse file at
|
|
71
|
+
`index * chunkLength` and answered `forget(index)` by dropping the number from a
|
|
72
|
+
set. The bytes stayed: a sparse file's blocks come back only by hole punching,
|
|
73
|
+
which Node exposes no binding for, so nothing this process could do returned a
|
|
74
|
+
single block before the whole file was removed.
|
|
75
|
+
|
|
76
|
+
`PieceDiskStore` gives each piece its own file. Removing one returns exactly its
|
|
77
|
+
blocks, needs no binding this runtime lacks, and makes the unit of eviction the
|
|
78
|
+
same as the unit of storage — so the order pieces leave in is the order we
|
|
79
|
+
choose. Over its allowance the least recently used goes, and a piece being read
|
|
80
|
+
is never the victim. A piece thrown away is answered as absent, so the torrent
|
|
81
|
+
fetches it again, which is the same bargain the memory tier makes when it
|
|
82
|
+
spills.
|
|
83
|
+
|
|
84
|
+
The read this replaced was chosen for its cost — 22.08 ms via `readFile` against
|
|
85
|
+
**7.63 ms** into a buffer we already hold, measured on the field host — and that
|
|
86
|
+
is unchanged: it is still one `read` into the caller's buffer. What it adds is
|
|
87
|
+
an `open` per read, tens of microseconds against those milliseconds.
|
|
88
|
+
|
|
89
|
+
## A clean exit leaves nothing of ours
|
|
90
|
+
|
|
91
|
+
The segments' root was removed only when it happened to be empty — from the
|
|
92
|
+
first commit of this repository, never a decision. What it protected against is
|
|
93
|
+
a second proxy sharing the root; what it did was leave every directory alone,
|
|
94
|
+
including this process's own orphans. A directory adopted at startup, owned by
|
|
95
|
+
no session, therefore survived the exit and was adopted again at the next start.
|
|
96
|
+
|
|
97
|
+
That loop is what made an orphan permanent, and it is why 5.0 GB of segments
|
|
98
|
+
from sessions that had ended hours before were on the addon host on 2026-09-10.
|
|
99
|
+
`SegmentStore.dropAll` now removes everything this process owns, root included,
|
|
100
|
+
which also gives the startup sweep its meaning back: whatever is found then is
|
|
101
|
+
from a kill.
|
|
102
|
+
|
|
103
|
+
Both kinds have both rules. For the spilled pieces the time rule is the same
|
|
104
|
+
statement one layer down: a piece behind every read head has been read and will
|
|
105
|
+
not be read again unless somebody seeks back, and a seek back re-downloads it —
|
|
106
|
+
the bargain this tier already makes whenever it drops a piece for room.
|
|
107
|
+
`PieceLru.readHeads` says where the readers stand, `PieceDiskStore.forgetBehind`
|
|
108
|
+
acts on it, and the eviction order under pressure is the same: behind the
|
|
109
|
+
earliest reader first, furthest behind first of all. With no reader at all
|
|
110
|
+
nothing is removed — a store between reads is not a store nobody wants, and the
|
|
111
|
+
torrent going idle is what empties it whole.
|
|
112
|
+
|
|
113
|
+
## How long material nobody is using is kept
|
|
114
|
+
|
|
115
|
+
One number, in one place — `services/disk/keep.js`, one hour — because
|
|
116
|
+
everything it governs stands for the same unmeasured thing: whether the viewer
|
|
117
|
+
comes back.
|
|
118
|
+
|
|
119
|
+
It was three, and they contradicted each other:
|
|
120
|
+
|
|
121
|
+
| what | was | now |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| a torrent and its downloaded bytes | 15 minutes | one hour |
|
|
124
|
+
| a session | 30 minutes | unchanged — a session is a record, not material |
|
|
125
|
+
| the segments an encoder produced | 6 hours | one hour |
|
|
126
|
+
|
|
127
|
+
The torrent went at fifteen minutes while the session it feeds lived to thirty,
|
|
128
|
+
so between them there was a session with no source: a viewer returning at the
|
|
129
|
+
twentieth minute got a session that could not make a single new segment. With
|
|
130
|
+
one hour for both kinds of material the session dies first, which is the right
|
|
131
|
+
order and needs no rule of its own to enforce.
|
|
132
|
+
|
|
133
|
+
Nothing derives the hour. Everything else in the decision is measured — a piece
|
|
134
|
+
comes back from the swarm in ~1430 ms, a segment in its own encode time, and the
|
|
135
|
+
disk has an owner that prices holding it — and only the return is unknown.
|
|
136
|
+
|
|
137
|
+
**And the return is measurable here.** A session opened on an output whose
|
|
138
|
+
segments are still on disk IS a return, and its age is exactly what the store
|
|
139
|
+
recorded. `services/disk/returns.js` keeps them and says so beside the disk
|
|
140
|
+
figures:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
returns: 14 session(s) opened on material still held, 3 on material gone;
|
|
144
|
+
median 12min after the last read, longest 51min — kept for 60min
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The median and the longest are the two the period has to sit between: shorter
|
|
148
|
+
than the median throws away material half the returns wanted, longer than the
|
|
149
|
+
longest keeps material no return has ever reached. A week of ordinary use and
|
|
150
|
+
the hour is replaced by what viewers actually do — and then the two kinds can
|
|
151
|
+
have different numbers, since their costs of coming back differ.
|
|
152
|
+
|
|
153
|
+
## What is NOT solved
|
|
154
|
+
|
|
155
|
+
**The measurement that would replace the hour has not been taken yet.** The
|
|
156
|
+
counter is in place and says nothing until a week of ordinary use has produced a
|
|
157
|
+
distribution.
|
|
158
|
+
|
|
159
|
+
**The diagnostics are not a claimant yet.** They are bounded by a count of files
|
|
160
|
+
and never by a size, and they cannot simply be thrown away when space is short: a
|
|
161
|
+
dump is the only evidence of the death it records. That needs a rule of its own.
|
|
162
|
+
|
|
163
|
+
## One layer, four directories, and under the import rule
|
|
164
|
+
|
|
165
|
+
`SegmentStore` moved out of `services/encode/` on 2026-09-14 — a store of
|
|
166
|
+
finished pieces inside the ENCODING layer was the encoder owning its own disk.
|
|
167
|
+
It did not move into the room's own directory either: it is a CLAIMANT, and a
|
|
168
|
+
claimant inside the owner of the resource is the same fault one floor down.
|
|
169
|
+
|
|
170
|
+
**Four directories, deliberately.** They share one property — bytes on a medium
|
|
171
|
+
with a limit — and that is what makes them one layer. What they are is three
|
|
172
|
+
different things: a torrent piece is born when it is downloaded and addressed by
|
|
173
|
+
`(infohash, number)`; a produced segment is born when an encoder closes it and
|
|
174
|
+
addressed by `(output key, number)`; a whole file is born when its last piece
|
|
175
|
+
lands. Different birth, different death, different name, and two of the three
|
|
176
|
+
live in the torrent WORKER thread while the others are on the main one. Flatten
|
|
177
|
+
them and that thread boundary becomes invisible: code that cannot call code
|
|
178
|
+
looks like a sibling, and no rule catches it.
|
|
179
|
+
|
|
180
|
+
**The rule is written as an exception, not as a list.** `group: ["../**",
|
|
181
|
+
"!../storage/**", "!../piece-store/**", "!../segment-store/**", "!../files/**"]`
|
|
182
|
+
— everything outside the layer, minus the layer. A list of forbidden layers was
|
|
183
|
+
tried first and let `../../utils/logger.js` straight through, because a list
|
|
184
|
+
only catches what somebody remembered to write in it. Checked by breaking it,
|
|
185
|
+
in both directions, in all four directories.
|
|
186
|
+
|
|
187
|
+
**One reader of how much memory is free** (`machine-memory.js`). It had been
|
|
188
|
+
written three times — the health collector, the memory report and the piece
|
|
189
|
+
store — and the copies had already drifted: the piece store was corrected from
|
|
190
|
+
`os.freemem()` to the kernel's `MemAvailable` on 2026-08-27 and the health
|
|
191
|
+
collector went on publishing the wrong quantity until 2026-09-02.
|
|
192
|
+
|
|
193
|
+
## Every claimant is told its share
|
|
194
|
+
|
|
195
|
+
A CLAIMANT IS WHOEVER HOLDS BYTES, and by 2026-09-14 all of them are registered
|
|
196
|
+
with the owner (`wire.js`): the segments an encoder produced, the pieces the
|
|
197
|
+
memory store spilled, the files downloaded whole, and the evidence. Two of the
|
|
198
|
+
four live on the torrent thread and arrive as a pair of closures over its
|
|
199
|
+
channel.
|
|
200
|
+
|
|
201
|
+
**Whole files had no bound of any kind** until that day — a 2.8 GB film kept
|
|
202
|
+
whole on a host whose disk is often a 32 GB card. They now take a share, and
|
|
203
|
+
over it the whole file nobody has asked for in longest goes. Losing one is not
|
|
204
|
+
losing data: the torrent can fetch it again, and until it does the read falls
|
|
205
|
+
back to the pieces, which is what `piece-from-whole-file.js` exists for. A file
|
|
206
|
+
is also not ASSEMBLED when there is no room for it — writing it and then
|
|
207
|
+
removing it is the same bytes written for nothing.
|
|
208
|
+
|
|
209
|
+
**The torrent pool's own "disk cap" is gone, and it owned nothing.** It capped
|
|
210
|
+
`torrentDownloadedBytes` — a sum over WebTorrent's bitfield — so a piece held
|
|
211
|
+
purely in MEMORY told against a ceiling called disk, while the bytes it meant to
|
|
212
|
+
bound belong to the spill and to the whole files, each of which has an owner.
|
|
213
|
+
Under it, eviction removed whole idle TORRENTS to free bytes that were not
|
|
214
|
+
necessarily on the disk at all.
|
|
215
|
+
|
|
216
|
+
**The evidence is a claimant with a rule of its own** (`Diagnostics.js`), and the
|
|
217
|
+
rule is what makes a bound on it safe:
|
|
218
|
+
|
|
219
|
+
1. over its share, collection STOPS. Nothing already recorded is deleted to make
|
|
220
|
+
room — a dump is the only evidence of the death it records — and a refusal is
|
|
221
|
+
a LINE with the figures, so an investigation that finds nothing can tell "it
|
|
222
|
+
did not happen" from "there was nowhere to put it";
|
|
223
|
+
2. except what is superseded, which is not evidence twice. A capture of a
|
|
224
|
+
connection already captured adds nothing: the wedge does not un-wedge. Field
|
|
225
|
+
2026-09-06, thirteen captures of one wedge in six hours, 74 MB, every one
|
|
226
|
+
saying what the first said;
|
|
227
|
+
3. what it asks for is measured — what it holds plus one more of the largest
|
|
228
|
+
kind seen — so the claim grows with the evidence and a proxy that has never
|
|
229
|
+
faulted asks for nothing.
|
|
230
|
+
|
|
231
|
+
**Core dumps are not ours to refuse, and that is stated rather than papered
|
|
232
|
+
over.** The kernel writes them, whole address space at a time, and no gate of
|
|
233
|
+
ours is consulted. They are counted, so they take room from what the product may
|
|
234
|
+
hold and the figure is visible; the pruning that keeps the newest two is left
|
|
235
|
+
where it is, because changing it is a decision about evidence and not about
|
|
236
|
+
disk.
|
|
237
|
+
|
|
238
|
+
## One budget
|
|
239
|
+
|
|
240
|
+
There is one owner, one policy and one place that reads the machine —
|
|
241
|
+
`MachineBudget.js`. What it divides is divided PER RESOURCE, because memory
|
|
242
|
+
cannot be paid for with disk, and "disk" is one resource per DEVICE: measured on
|
|
243
|
+
the addon host 2026-09-05, `/tmp` (the segments and the spill) is the overlay
|
|
244
|
+
and `/data` (the evidence) is ext4 on the nvme. Dividing one figure between
|
|
245
|
+
claimants on both gave each a share of a disk it does not write to, which is
|
|
246
|
+
what the old `DiskSpace` did — it read the free space of the segment root alone.
|
|
247
|
+
The device is read with `statSync(dir).dev`, from the nearest ancestor that
|
|
248
|
+
exists, for the same reason `freeBytesFor` walks up.
|
|
249
|
+
|
|
250
|
+
**Why one owner and not two.** The claimants trade across resources: pieces that
|
|
251
|
+
do not fit in memory are spilled to disk, whole files exist so that pieces need
|
|
252
|
+
not be held, segments exist because making them again is dear. Field 2026-08-31:
|
|
253
|
+
14 400 MB spilled in fifty minutes while the memory store held 312-424 MB — give
|
|
254
|
+
it memory and there is no spill to bound. Two owners cannot make that trade,
|
|
255
|
+
because neither sees both sides of it. Memory was divided inside the torrent
|
|
256
|
+
thread and disk on the main one until 2026-09-14; the memory share now travels
|
|
257
|
+
the same channel the spill share already did.
|
|
258
|
+
|
|
259
|
+
**The policy is the operator's, and it is the only chosen number here.**
|
|
260
|
+
`--budget adaptive|share|fixed` with `--budget-share` / `--budget-bytes`, and
|
|
261
|
+
floors with `--min-memory-bytes` / `--min-disk-bytes`. The default is
|
|
262
|
+
`adaptive`, which is the measured rule of `allowance.js` and chooses nothing. A
|
|
263
|
+
floor lifts what the policy allows and can never invent room the machine does
|
|
264
|
+
not have; a claimant left below its own minimum says so in a line, because
|
|
265
|
+
working below what it needs is a fault of the machine and invisible otherwise.
|
|
266
|
+
|
|
267
|
+
|
package/knip.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://unpkg.com/knip@6/schema.json",
|
|
3
|
-
"entry": [
|
|
4
|
-
"test/*.test.js"
|
|
5
|
-
],
|
|
6
|
-
"project": [
|
|
7
|
-
"bin/**/*.js",
|
|
8
|
-
"routes/**/*.js",
|
|
9
|
-
"services/**/*.js",
|
|
10
|
-
"utils/**/*.js",
|
|
11
|
-
"server.js",
|
|
12
|
-
"test/**/*.js"
|
|
13
|
-
]
|
|
14
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/knip@6/schema.json",
|
|
3
|
+
"entry": [
|
|
4
|
+
"test/*.test.js"
|
|
5
|
+
],
|
|
6
|
+
"project": [
|
|
7
|
+
"bin/**/*.js",
|
|
8
|
+
"routes/**/*.js",
|
|
9
|
+
"services/**/*.js",
|
|
10
|
+
"utils/**/*.js",
|
|
11
|
+
"server.js",
|
|
12
|
+
"test/**/*.js"
|
|
13
|
+
]
|
|
14
|
+
}
|