@torrent-tv/proxy 2.87.2 → 2.87.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 +2157 -2148
- 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 +94 -94
- 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 +50 -50
- 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/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- 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 -181
- 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 +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- 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 +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -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 +721 -720
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- 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 +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- 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 +416 -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 +1083 -1083
- 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 +254 -254
- package/services/encode/quality/output-format.js +266 -266
- 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 +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- 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 +212 -212
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -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 +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- 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 +856 -848
- package/services/media/tracks/AudioTrack.js +131 -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/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 +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/{viewer → server}/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +36 -36
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- 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 +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- 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/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 -2635
- package/services/torrent/torrent-source-key.js +38 -38
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -760
- 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 +365 -350
- package/services/torrent/worker/protocol.js +184 -182
- 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 -1116
- package/services/transport/data-channel-handler.js +1356 -1356
- 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 +657 -657
- 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 +73 -73
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +176 -176
- 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/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- 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 +161 -161
- 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/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/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 +111 -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 -294
- 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 +204 -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 +381 -381
- 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 +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- 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/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 +1229 -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 +102 -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/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- 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 +182 -182
- 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 +407 -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
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Room for a piece is an owned reservation, and it comes back every time.
|
|
3
|
-
*
|
|
4
|
-
* Each case here is a defect read out of the field failure of 2026-08-31
|
|
5
|
-
* (`research/worker-heap-oom-2026-08-31.md`, §5), where the torrent worker was
|
|
6
|
-
* found holding reservations nobody could return. They all need the disk tier
|
|
7
|
-
* to be slow, or to fail, at a moment the caller chooses — which is what
|
|
8
|
-
* `options.disk` is for.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
14
|
-
|
|
15
|
-
const CHUNK = 1024;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A disk tier the test drives: writes can be held open and either side can be
|
|
19
|
-
* made to fail.
|
|
20
|
-
*
|
|
21
|
-
* @param {{ failWrite?: boolean, failRead?: boolean, holdWrites?: boolean }} [behaviour]
|
|
22
|
-
*/
|
|
23
|
-
function makeDisk({ failWrite = false, failRead = false, holdWrites = false } = {}) {
|
|
24
|
-
const stored = new Map();
|
|
25
|
-
/** @type {Array<() => void>} */
|
|
26
|
-
const held = [];
|
|
27
|
-
return {
|
|
28
|
-
stored,
|
|
29
|
-
/** Let every held write finish. */
|
|
30
|
-
releaseWrites() {
|
|
31
|
-
const waiting = held.splice(0, held.length);
|
|
32
|
-
for (const resume of waiting) {
|
|
33
|
-
resume();
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
get heldCount() {
|
|
37
|
-
return held.length;
|
|
38
|
-
},
|
|
39
|
-
get size() {
|
|
40
|
-
return stored.size;
|
|
41
|
-
},
|
|
42
|
-
get bytes() {
|
|
43
|
-
let total = 0;
|
|
44
|
-
for (const value of stored.values()) {
|
|
45
|
-
total += value.length;
|
|
46
|
-
}
|
|
47
|
-
return total;
|
|
48
|
-
},
|
|
49
|
-
get path() {
|
|
50
|
-
return ".";
|
|
51
|
-
},
|
|
52
|
-
get allowanceBytes() {
|
|
53
|
-
return null;
|
|
54
|
-
},
|
|
55
|
-
reviseAllowance() {
|
|
56
|
-
return null;
|
|
57
|
-
},
|
|
58
|
-
stats() {
|
|
59
|
-
return { pieces: stored.size, bytes: this.bytes, allowanceBytes: null, evictions: 0 };
|
|
60
|
-
},
|
|
61
|
-
has(index) {
|
|
62
|
-
return stored.has(index);
|
|
63
|
-
},
|
|
64
|
-
async write(index, bytes) {
|
|
65
|
-
if (holdWrites) {
|
|
66
|
-
await new Promise((resolve) => held.push(resolve));
|
|
67
|
-
}
|
|
68
|
-
if (failWrite) {
|
|
69
|
-
throw new Error("the disk refused the write");
|
|
70
|
-
}
|
|
71
|
-
stored.set(index, Buffer.from(bytes));
|
|
72
|
-
},
|
|
73
|
-
async read(index, target) {
|
|
74
|
-
if (failRead) {
|
|
75
|
-
throw new Error("the disk refused the read");
|
|
76
|
-
}
|
|
77
|
-
const bytes = stored.get(index);
|
|
78
|
-
if (!bytes) {
|
|
79
|
-
throw new Error(`Piece ${index} is not on disk.`);
|
|
80
|
-
}
|
|
81
|
-
bytes.copy(target);
|
|
82
|
-
return bytes.length;
|
|
83
|
-
},
|
|
84
|
-
forget(index) {
|
|
85
|
-
stored.delete(index);
|
|
86
|
-
},
|
|
87
|
-
async close() {},
|
|
88
|
-
async destroy() {
|
|
89
|
-
stored.clear();
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @param {object} [options]
|
|
96
|
-
* @returns {{ store: SharedPieceStore, disk: ReturnType<typeof makeDisk> }}
|
|
97
|
-
*/
|
|
98
|
-
function makeStore({ pieces = 2, totalPieces = 16, disk = makeDisk() } = {}) {
|
|
99
|
-
const store = new SharedPieceStore(CHUNK, {
|
|
100
|
-
length: CHUNK * totalPieces,
|
|
101
|
-
memoryBytes: CHUNK * pieces,
|
|
102
|
-
disk,
|
|
103
|
-
name: "test"
|
|
104
|
-
});
|
|
105
|
-
return { store, disk };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {number} index
|
|
110
|
-
* @returns {Buffer}
|
|
111
|
-
*/
|
|
112
|
-
function piece(index) {
|
|
113
|
-
const bytes = Buffer.allocUnsafeSlow(CHUNK);
|
|
114
|
-
bytes.fill(index % 256);
|
|
115
|
-
bytes.writeUInt32BE(index, 0);
|
|
116
|
-
return bytes;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const put = (store, index) =>
|
|
120
|
-
new Promise((resolve, reject) => store.put(index, piece(index), (error) => (error ? reject(error) : resolve())));
|
|
121
|
-
const get = (store, index) =>
|
|
122
|
-
new Promise((resolve, reject) =>
|
|
123
|
-
store.get(index, undefined, (error, bytes) => (error ? reject(error) : resolve(bytes)))
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Wait until a condition holds, rather than for a chosen interval — a test that
|
|
128
|
-
* sleeps samples, it does not check (roadmap item 54).
|
|
129
|
-
*
|
|
130
|
-
* @param {() => boolean} holds
|
|
131
|
-
* @param {string} what
|
|
132
|
-
* @param {number} [limit] - A backstop, never the measurement: one of the
|
|
133
|
-
* conditions here is the store's own patience, which is itself five seconds.
|
|
134
|
-
* @returns {Promise<void>}
|
|
135
|
-
*/
|
|
136
|
-
async function until(holds, what, limit = 5_000) {
|
|
137
|
-
const deadline = Date.now() + limit;
|
|
138
|
-
while (!holds()) {
|
|
139
|
-
if (Date.now() > deadline) {
|
|
140
|
-
throw new Error(`timed out waiting until ${what}`);
|
|
141
|
-
}
|
|
142
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
test("a revival that fails on the disk gives its slot back", async () => {
|
|
147
|
-
const disk = makeDisk({ failRead: true });
|
|
148
|
-
const { store } = makeStore({ pieces: 2, disk });
|
|
149
|
-
try {
|
|
150
|
-
await put(store, 0);
|
|
151
|
-
await put(store, 1);
|
|
152
|
-
await put(store, 2); // piece 0 is the least recently used, so it spills
|
|
153
|
-
|
|
154
|
-
await assert.rejects(() => get(store, 0), /refused the read/);
|
|
155
|
-
|
|
156
|
-
assert.equal(
|
|
157
|
-
store.stats().outstanding,
|
|
158
|
-
0,
|
|
159
|
-
"the slot claimed for the revival was never returned"
|
|
160
|
-
);
|
|
161
|
-
} finally {
|
|
162
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("a spill that fails gives back the slot the eviction claimed", async () => {
|
|
167
|
-
const disk = makeDisk({ failWrite: true });
|
|
168
|
-
const { store } = makeStore({ pieces: 2, disk });
|
|
169
|
-
try {
|
|
170
|
-
await put(store, 0);
|
|
171
|
-
await put(store, 1);
|
|
172
|
-
await assert.rejects(() => put(store, 2), /refused the write/);
|
|
173
|
-
|
|
174
|
-
assert.equal(store.stats().outstanding, 0, "the eviction kept its reservation");
|
|
175
|
-
} finally {
|
|
176
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// A bound, because the failure this catches is a claim that never ends: without
|
|
181
|
-
// it the check does not fail, it HANGS, and `node --test` then cannot finish at
|
|
182
|
-
// all — which is what happened between 2026-08-31 and 2026-09-03 (roadmap item
|
|
183
|
-
// 54). The store gives up after PINNED_WAIT_MS, so 30 s is ample for the answer
|
|
184
|
-
// and short enough to be a failure rather than a stoppage.
|
|
185
|
-
test("a claim that cannot be met ends in an error, not in waiting for ever", { timeout: 30_000 }, async () => {
|
|
186
|
-
const disk = makeDisk({ holdWrites: true });
|
|
187
|
-
const { store } = makeStore({ pieces: 2, disk });
|
|
188
|
-
try {
|
|
189
|
-
await put(store, 0);
|
|
190
|
-
await put(store, 1);
|
|
191
|
-
|
|
192
|
-
// Evicts one piece; its write is held, so the store has a spill in flight
|
|
193
|
-
// for as long as this test wants.
|
|
194
|
-
const spilling = put(store, 2);
|
|
195
|
-
await until(() => disk.heldCount > 0, "a write is in flight");
|
|
196
|
-
|
|
197
|
-
// Nothing left that may be evicted: one piece is being written out, the
|
|
198
|
-
// other is pinned. The old rule waited while anything was nominally in
|
|
199
|
-
// flight, which here is for ever.
|
|
200
|
-
store.pin(1);
|
|
201
|
-
// The claim gives up within its own patience — that is the property. What
|
|
202
|
-
// the caller does with the refusal is its own business, and since
|
|
203
|
-
// 2026-09-11 an arriving piece answers it by taking the disk instead of
|
|
204
|
-
// failing the torrent client's write.
|
|
205
|
-
const arriving = put(store, 3);
|
|
206
|
-
await until(
|
|
207
|
-
() => store.stats().blockedByPins > 0,
|
|
208
|
-
"the claim gave up rather than waiting for ever",
|
|
209
|
-
20_000
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
store.unpin(1);
|
|
213
|
-
disk.releaseWrites();
|
|
214
|
-
await arriving;
|
|
215
|
-
disk.releaseWrites();
|
|
216
|
-
await spilling;
|
|
217
|
-
} finally {
|
|
218
|
-
disk.releaseWrites();
|
|
219
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("closing the store fails whoever is waiting for room", { timeout: 30_000 }, async () => {
|
|
224
|
-
const disk = makeDisk({ holdWrites: true });
|
|
225
|
-
const { store } = makeStore({ pieces: 2, disk });
|
|
226
|
-
try {
|
|
227
|
-
await put(store, 0);
|
|
228
|
-
await put(store, 1);
|
|
229
|
-
const spilling = put(store, 2);
|
|
230
|
-
await until(() => disk.heldCount > 0, "a write is in flight");
|
|
231
|
-
store.pin(1);
|
|
232
|
-
|
|
233
|
-
const waiting = put(store, 3);
|
|
234
|
-
// Either counter: the claim waits for the disk first — a block is in flight
|
|
235
|
-
// and the store is full, so evicting another piece would only raise the
|
|
236
|
-
// memory in use — and reaches the pinned wait five seconds later. Asking for
|
|
237
|
-
// `waitedForPins` alone named one of the two ways of waiting and timed out
|
|
238
|
-
// while the store was demonstrably doing the other.
|
|
239
|
-
await until(
|
|
240
|
-
() => store.stats().waitedForPins + store.stats().waitedForDisk > 0,
|
|
241
|
-
"the claim is waiting"
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
await new Promise((resolve) => store.close(resolve));
|
|
245
|
-
await assert.rejects(() => waiting, /closed/);
|
|
246
|
-
|
|
247
|
-
store.unpin(1);
|
|
248
|
-
disk.releaseWrites();
|
|
249
|
-
await spilling.catch(() => undefined);
|
|
250
|
-
} finally {
|
|
251
|
-
disk.releaseWrites();
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test("a piece written back to memory is not resurrected on disk by its own spill", { timeout: 30_000 }, async () => {
|
|
256
|
-
const disk = makeDisk({ holdWrites: true });
|
|
257
|
-
const { store } = makeStore({ pieces: 4, disk });
|
|
258
|
-
try {
|
|
259
|
-
await put(store, 0);
|
|
260
|
-
await put(store, 1);
|
|
261
|
-
await put(store, 2);
|
|
262
|
-
|
|
263
|
-
// Piece 0 leaves memory because the machine's allowance fell; its write is
|
|
264
|
-
// still in flight. The allowance then recovers to MORE than it was, so the
|
|
265
|
-
// piece coming back needs no eviction and no wait for that write.
|
|
266
|
-
//
|
|
267
|
-
// Four blocks, not three, and the reason is what this check is about. A
|
|
268
|
-
// block being written out is still memory in use, so with a ceiling of three
|
|
269
|
-
// and one block in flight the store is full, and the claim correctly waits
|
|
270
|
-
// for the disk — which the earlier setup did not allow for, so the check
|
|
271
|
-
// timed out on an admission policy it never meant to measure. Whether a
|
|
272
|
-
// piece arriving while its OWN spill is in flight should be admitted without
|
|
273
|
-
// taking a second block is a real question about the accounting and is
|
|
274
|
-
// roadmap item 9, not this check's subject: the subject is that the spill,
|
|
275
|
-
// when it completes, must not put the stale copy back on disk.
|
|
276
|
-
store.reviseGrowthCeiling(CHUNK * 2);
|
|
277
|
-
await until(() => disk.heldCount > 0, "the spill of piece 0 is in flight");
|
|
278
|
-
store.reviseGrowthCeiling(CHUNK * 4);
|
|
279
|
-
|
|
280
|
-
// The swarm hands piece 0 back while that write is still going. The store
|
|
281
|
-
// must drop the disk copy AFTER the write has recorded it, not before —
|
|
282
|
-
// `PieceDiskStore.write` adds the index on completion, so an early forget is
|
|
283
|
-
// undone and the next read of piece 0 comes back from before the rewrite.
|
|
284
|
-
let settled = false;
|
|
285
|
-
const rewritten = put(store, 0);
|
|
286
|
-
void rewritten.then(() => {
|
|
287
|
-
settled = true;
|
|
288
|
-
});
|
|
289
|
-
await until(() => store.stats().resident === 3, "piece 0 is back in memory");
|
|
290
|
-
assert.equal(settled, false, "the write finished without waiting for the piece's own spill");
|
|
291
|
-
|
|
292
|
-
disk.releaseWrites();
|
|
293
|
-
await rewritten;
|
|
294
|
-
|
|
295
|
-
assert.equal(disk.has(0), false, "the completing spill put the stale copy back");
|
|
296
|
-
} finally {
|
|
297
|
-
disk.releaseWrites();
|
|
298
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("a spill that fails while the allowance is lowered is counted, not thrown", async () => {
|
|
303
|
-
const disk = makeDisk({ failWrite: true });
|
|
304
|
-
const { store } = makeStore({ pieces: 4, disk });
|
|
305
|
-
try {
|
|
306
|
-
await put(store, 0);
|
|
307
|
-
await put(store, 1);
|
|
308
|
-
await put(store, 2);
|
|
309
|
-
await put(store, 3);
|
|
310
|
-
|
|
311
|
-
store.reviseGrowthCeiling(CHUNK * 2);
|
|
312
|
-
await until(() => store.stats().spillFailures > 0, "the failed spills are counted");
|
|
313
|
-
|
|
314
|
-
assert.equal(store.stats().outstanding, 0, "lowering the allowance held a reservation");
|
|
315
|
-
} finally {
|
|
316
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
317
|
-
}
|
|
318
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Room for a piece is an owned reservation, and it comes back every time.
|
|
3
|
+
*
|
|
4
|
+
* Each case here is a defect read out of the field failure of 2026-08-31
|
|
5
|
+
* (`research/worker-heap-oom-2026-08-31.md`, §5), where the torrent worker was
|
|
6
|
+
* found holding reservations nobody could return. They all need the disk tier
|
|
7
|
+
* to be slow, or to fail, at a moment the caller chooses — which is what
|
|
8
|
+
* `options.disk` is for.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
14
|
+
|
|
15
|
+
const CHUNK = 1024;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A disk tier the test drives: writes can be held open and either side can be
|
|
19
|
+
* made to fail.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ failWrite?: boolean, failRead?: boolean, holdWrites?: boolean }} [behaviour]
|
|
22
|
+
*/
|
|
23
|
+
function makeDisk({ failWrite = false, failRead = false, holdWrites = false } = {}) {
|
|
24
|
+
const stored = new Map();
|
|
25
|
+
/** @type {Array<() => void>} */
|
|
26
|
+
const held = [];
|
|
27
|
+
return {
|
|
28
|
+
stored,
|
|
29
|
+
/** Let every held write finish. */
|
|
30
|
+
releaseWrites() {
|
|
31
|
+
const waiting = held.splice(0, held.length);
|
|
32
|
+
for (const resume of waiting) {
|
|
33
|
+
resume();
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
get heldCount() {
|
|
37
|
+
return held.length;
|
|
38
|
+
},
|
|
39
|
+
get size() {
|
|
40
|
+
return stored.size;
|
|
41
|
+
},
|
|
42
|
+
get bytes() {
|
|
43
|
+
let total = 0;
|
|
44
|
+
for (const value of stored.values()) {
|
|
45
|
+
total += value.length;
|
|
46
|
+
}
|
|
47
|
+
return total;
|
|
48
|
+
},
|
|
49
|
+
get path() {
|
|
50
|
+
return ".";
|
|
51
|
+
},
|
|
52
|
+
get allowanceBytes() {
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
reviseAllowance() {
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
stats() {
|
|
59
|
+
return { pieces: stored.size, bytes: this.bytes, allowanceBytes: null, evictions: 0 };
|
|
60
|
+
},
|
|
61
|
+
has(index) {
|
|
62
|
+
return stored.has(index);
|
|
63
|
+
},
|
|
64
|
+
async write(index, bytes) {
|
|
65
|
+
if (holdWrites) {
|
|
66
|
+
await new Promise((resolve) => held.push(resolve));
|
|
67
|
+
}
|
|
68
|
+
if (failWrite) {
|
|
69
|
+
throw new Error("the disk refused the write");
|
|
70
|
+
}
|
|
71
|
+
stored.set(index, Buffer.from(bytes));
|
|
72
|
+
},
|
|
73
|
+
async read(index, target) {
|
|
74
|
+
if (failRead) {
|
|
75
|
+
throw new Error("the disk refused the read");
|
|
76
|
+
}
|
|
77
|
+
const bytes = stored.get(index);
|
|
78
|
+
if (!bytes) {
|
|
79
|
+
throw new Error(`Piece ${index} is not on disk.`);
|
|
80
|
+
}
|
|
81
|
+
bytes.copy(target);
|
|
82
|
+
return bytes.length;
|
|
83
|
+
},
|
|
84
|
+
forget(index) {
|
|
85
|
+
stored.delete(index);
|
|
86
|
+
},
|
|
87
|
+
async close() {},
|
|
88
|
+
async destroy() {
|
|
89
|
+
stored.clear();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @returns {{ store: SharedPieceStore, disk: ReturnType<typeof makeDisk> }}
|
|
97
|
+
*/
|
|
98
|
+
function makeStore({ pieces = 2, totalPieces = 16, disk = makeDisk() } = {}) {
|
|
99
|
+
const store = new SharedPieceStore(CHUNK, {
|
|
100
|
+
length: CHUNK * totalPieces,
|
|
101
|
+
memoryBytes: CHUNK * pieces,
|
|
102
|
+
disk,
|
|
103
|
+
name: "test"
|
|
104
|
+
});
|
|
105
|
+
return { store, disk };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {number} index
|
|
110
|
+
* @returns {Buffer}
|
|
111
|
+
*/
|
|
112
|
+
function piece(index) {
|
|
113
|
+
const bytes = Buffer.allocUnsafeSlow(CHUNK);
|
|
114
|
+
bytes.fill(index % 256);
|
|
115
|
+
bytes.writeUInt32BE(index, 0);
|
|
116
|
+
return bytes;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const put = (store, index) =>
|
|
120
|
+
new Promise((resolve, reject) => store.put(index, piece(index), (error) => (error ? reject(error) : resolve())));
|
|
121
|
+
const get = (store, index) =>
|
|
122
|
+
new Promise((resolve, reject) =>
|
|
123
|
+
store.get(index, undefined, (error, bytes) => (error ? reject(error) : resolve(bytes)))
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Wait until a condition holds, rather than for a chosen interval — a test that
|
|
128
|
+
* sleeps samples, it does not check (roadmap item 54).
|
|
129
|
+
*
|
|
130
|
+
* @param {() => boolean} holds
|
|
131
|
+
* @param {string} what
|
|
132
|
+
* @param {number} [limit] - A backstop, never the measurement: one of the
|
|
133
|
+
* conditions here is the store's own patience, which is itself five seconds.
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
async function until(holds, what, limit = 5_000) {
|
|
137
|
+
const deadline = Date.now() + limit;
|
|
138
|
+
while (!holds()) {
|
|
139
|
+
if (Date.now() > deadline) {
|
|
140
|
+
throw new Error(`timed out waiting until ${what}`);
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("a revival that fails on the disk gives its slot back", async () => {
|
|
147
|
+
const disk = makeDisk({ failRead: true });
|
|
148
|
+
const { store } = makeStore({ pieces: 2, disk });
|
|
149
|
+
try {
|
|
150
|
+
await put(store, 0);
|
|
151
|
+
await put(store, 1);
|
|
152
|
+
await put(store, 2); // piece 0 is the least recently used, so it spills
|
|
153
|
+
|
|
154
|
+
await assert.rejects(() => get(store, 0), /refused the read/);
|
|
155
|
+
|
|
156
|
+
assert.equal(
|
|
157
|
+
store.stats().outstanding,
|
|
158
|
+
0,
|
|
159
|
+
"the slot claimed for the revival was never returned"
|
|
160
|
+
);
|
|
161
|
+
} finally {
|
|
162
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a spill that fails gives back the slot the eviction claimed", async () => {
|
|
167
|
+
const disk = makeDisk({ failWrite: true });
|
|
168
|
+
const { store } = makeStore({ pieces: 2, disk });
|
|
169
|
+
try {
|
|
170
|
+
await put(store, 0);
|
|
171
|
+
await put(store, 1);
|
|
172
|
+
await assert.rejects(() => put(store, 2), /refused the write/);
|
|
173
|
+
|
|
174
|
+
assert.equal(store.stats().outstanding, 0, "the eviction kept its reservation");
|
|
175
|
+
} finally {
|
|
176
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// A bound, because the failure this catches is a claim that never ends: without
|
|
181
|
+
// it the check does not fail, it HANGS, and `node --test` then cannot finish at
|
|
182
|
+
// all — which is what happened between 2026-08-31 and 2026-09-03 (roadmap item
|
|
183
|
+
// 54). The store gives up after PINNED_WAIT_MS, so 30 s is ample for the answer
|
|
184
|
+
// and short enough to be a failure rather than a stoppage.
|
|
185
|
+
test("a claim that cannot be met ends in an error, not in waiting for ever", { timeout: 30_000 }, async () => {
|
|
186
|
+
const disk = makeDisk({ holdWrites: true });
|
|
187
|
+
const { store } = makeStore({ pieces: 2, disk });
|
|
188
|
+
try {
|
|
189
|
+
await put(store, 0);
|
|
190
|
+
await put(store, 1);
|
|
191
|
+
|
|
192
|
+
// Evicts one piece; its write is held, so the store has a spill in flight
|
|
193
|
+
// for as long as this test wants.
|
|
194
|
+
const spilling = put(store, 2);
|
|
195
|
+
await until(() => disk.heldCount > 0, "a write is in flight");
|
|
196
|
+
|
|
197
|
+
// Nothing left that may be evicted: one piece is being written out, the
|
|
198
|
+
// other is pinned. The old rule waited while anything was nominally in
|
|
199
|
+
// flight, which here is for ever.
|
|
200
|
+
store.pin(1);
|
|
201
|
+
// The claim gives up within its own patience — that is the property. What
|
|
202
|
+
// the caller does with the refusal is its own business, and since
|
|
203
|
+
// 2026-09-11 an arriving piece answers it by taking the disk instead of
|
|
204
|
+
// failing the torrent client's write.
|
|
205
|
+
const arriving = put(store, 3);
|
|
206
|
+
await until(
|
|
207
|
+
() => store.stats().blockedByPins > 0,
|
|
208
|
+
"the claim gave up rather than waiting for ever",
|
|
209
|
+
20_000
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
store.unpin(1);
|
|
213
|
+
disk.releaseWrites();
|
|
214
|
+
await arriving;
|
|
215
|
+
disk.releaseWrites();
|
|
216
|
+
await spilling;
|
|
217
|
+
} finally {
|
|
218
|
+
disk.releaseWrites();
|
|
219
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("closing the store fails whoever is waiting for room", { timeout: 30_000 }, async () => {
|
|
224
|
+
const disk = makeDisk({ holdWrites: true });
|
|
225
|
+
const { store } = makeStore({ pieces: 2, disk });
|
|
226
|
+
try {
|
|
227
|
+
await put(store, 0);
|
|
228
|
+
await put(store, 1);
|
|
229
|
+
const spilling = put(store, 2);
|
|
230
|
+
await until(() => disk.heldCount > 0, "a write is in flight");
|
|
231
|
+
store.pin(1);
|
|
232
|
+
|
|
233
|
+
const waiting = put(store, 3);
|
|
234
|
+
// Either counter: the claim waits for the disk first — a block is in flight
|
|
235
|
+
// and the store is full, so evicting another piece would only raise the
|
|
236
|
+
// memory in use — and reaches the pinned wait five seconds later. Asking for
|
|
237
|
+
// `waitedForPins` alone named one of the two ways of waiting and timed out
|
|
238
|
+
// while the store was demonstrably doing the other.
|
|
239
|
+
await until(
|
|
240
|
+
() => store.stats().waitedForPins + store.stats().waitedForDisk > 0,
|
|
241
|
+
"the claim is waiting"
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await new Promise((resolve) => store.close(resolve));
|
|
245
|
+
await assert.rejects(() => waiting, /closed/);
|
|
246
|
+
|
|
247
|
+
store.unpin(1);
|
|
248
|
+
disk.releaseWrites();
|
|
249
|
+
await spilling.catch(() => undefined);
|
|
250
|
+
} finally {
|
|
251
|
+
disk.releaseWrites();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("a piece written back to memory is not resurrected on disk by its own spill", { timeout: 30_000 }, async () => {
|
|
256
|
+
const disk = makeDisk({ holdWrites: true });
|
|
257
|
+
const { store } = makeStore({ pieces: 4, disk });
|
|
258
|
+
try {
|
|
259
|
+
await put(store, 0);
|
|
260
|
+
await put(store, 1);
|
|
261
|
+
await put(store, 2);
|
|
262
|
+
|
|
263
|
+
// Piece 0 leaves memory because the machine's allowance fell; its write is
|
|
264
|
+
// still in flight. The allowance then recovers to MORE than it was, so the
|
|
265
|
+
// piece coming back needs no eviction and no wait for that write.
|
|
266
|
+
//
|
|
267
|
+
// Four blocks, not three, and the reason is what this check is about. A
|
|
268
|
+
// block being written out is still memory in use, so with a ceiling of three
|
|
269
|
+
// and one block in flight the store is full, and the claim correctly waits
|
|
270
|
+
// for the disk — which the earlier setup did not allow for, so the check
|
|
271
|
+
// timed out on an admission policy it never meant to measure. Whether a
|
|
272
|
+
// piece arriving while its OWN spill is in flight should be admitted without
|
|
273
|
+
// taking a second block is a real question about the accounting and is
|
|
274
|
+
// roadmap item 9, not this check's subject: the subject is that the spill,
|
|
275
|
+
// when it completes, must not put the stale copy back on disk.
|
|
276
|
+
store.reviseGrowthCeiling(CHUNK * 2);
|
|
277
|
+
await until(() => disk.heldCount > 0, "the spill of piece 0 is in flight");
|
|
278
|
+
store.reviseGrowthCeiling(CHUNK * 4);
|
|
279
|
+
|
|
280
|
+
// The swarm hands piece 0 back while that write is still going. The store
|
|
281
|
+
// must drop the disk copy AFTER the write has recorded it, not before —
|
|
282
|
+
// `PieceDiskStore.write` adds the index on completion, so an early forget is
|
|
283
|
+
// undone and the next read of piece 0 comes back from before the rewrite.
|
|
284
|
+
let settled = false;
|
|
285
|
+
const rewritten = put(store, 0);
|
|
286
|
+
void rewritten.then(() => {
|
|
287
|
+
settled = true;
|
|
288
|
+
});
|
|
289
|
+
await until(() => store.stats().resident === 3, "piece 0 is back in memory");
|
|
290
|
+
assert.equal(settled, false, "the write finished without waiting for the piece's own spill");
|
|
291
|
+
|
|
292
|
+
disk.releaseWrites();
|
|
293
|
+
await rewritten;
|
|
294
|
+
|
|
295
|
+
assert.equal(disk.has(0), false, "the completing spill put the stale copy back");
|
|
296
|
+
} finally {
|
|
297
|
+
disk.releaseWrites();
|
|
298
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("a spill that fails while the allowance is lowered is counted, not thrown", async () => {
|
|
303
|
+
const disk = makeDisk({ failWrite: true });
|
|
304
|
+
const { store } = makeStore({ pieces: 4, disk });
|
|
305
|
+
try {
|
|
306
|
+
await put(store, 0);
|
|
307
|
+
await put(store, 1);
|
|
308
|
+
await put(store, 2);
|
|
309
|
+
await put(store, 3);
|
|
310
|
+
|
|
311
|
+
store.reviseGrowthCeiling(CHUNK * 2);
|
|
312
|
+
await until(() => store.stats().spillFailures > 0, "the failed spills are counted");
|
|
313
|
+
|
|
314
|
+
assert.equal(store.stats().outstanding, 0, "lowering the allowance held a reservation");
|
|
315
|
+
} finally {
|
|
316
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
317
|
+
}
|
|
318
|
+
});
|