@torrent-tv/proxy 2.87.8 → 2.87.10
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 +8 -0
- 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/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/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/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/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/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 +3 -0
- 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/server/WaitLedger.js +142 -142
- 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/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- 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/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- 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 +71 -71
- package/test/asset-paths.test.js +28 -28
- 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 +43 -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 -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 +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/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 +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
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,310 +1,310 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Pieces in shared memory, spilling to disk.
|
|
3
|
-
*
|
|
4
|
-
* The cases worth having are the ones that describe past failures: a buffer
|
|
5
|
-
* handed out must not be invalidated by later activity (2.9.71 transferred
|
|
6
|
-
* memory it did not own), and a piece being read must not be evicted (2.9.71
|
|
7
|
-
* again, at the torrent level).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import fs from "node:fs/promises";
|
|
13
|
-
import os from "node:os";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
16
|
-
|
|
17
|
-
const CHUNK = 64 * 1024;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param {object} [options]
|
|
21
|
-
* @returns {Promise<{ store: SharedPieceStore, directory: string }>}
|
|
22
|
-
*/
|
|
23
|
-
async function makeStore({ pieces = 4, totalPieces = 16 } = {}) {
|
|
24
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "piece-store-"));
|
|
25
|
-
const store = new SharedPieceStore(CHUNK, {
|
|
26
|
-
length: CHUNK * totalPieces,
|
|
27
|
-
memoryBytes: CHUNK * pieces,
|
|
28
|
-
path: directory,
|
|
29
|
-
name: "test"
|
|
30
|
-
});
|
|
31
|
-
return { store, directory };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param {number} index
|
|
36
|
-
* @param {number} [length]
|
|
37
|
-
* @returns {Buffer}
|
|
38
|
-
*/
|
|
39
|
-
function piece(index, length = CHUNK) {
|
|
40
|
-
const bytes = Buffer.allocUnsafeSlow(length);
|
|
41
|
-
bytes.fill(index % 256);
|
|
42
|
-
bytes.writeUInt32BE(index, 0);
|
|
43
|
-
return bytes;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Promise-shaped wrappers, so the tests read as the sequence they describe. */
|
|
47
|
-
const put = (store, index, bytes) =>
|
|
48
|
-
new Promise((resolve, reject) => store.put(index, bytes, (error) => (error ? reject(error) : resolve())));
|
|
49
|
-
const get = (store, index, options) =>
|
|
50
|
-
new Promise((resolve, reject) =>
|
|
51
|
-
store.get(index, options, (error, bytes) => (error ? reject(error) : resolve(bytes)))
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
test("a stored piece comes back byte for byte", async () => {
|
|
55
|
-
const { store, directory } = await makeStore();
|
|
56
|
-
try {
|
|
57
|
-
await put(store, 0, piece(0));
|
|
58
|
-
const read = await get(store, 0);
|
|
59
|
-
assert.deepEqual(read, piece(0));
|
|
60
|
-
} finally {
|
|
61
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
62
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("pieces past the memory budget spill to disk and read back intact", async () => {
|
|
67
|
-
const { store, directory } = await makeStore({ pieces: 4, totalPieces: 16 });
|
|
68
|
-
try {
|
|
69
|
-
for (let index = 0; index < 10; index += 1) {
|
|
70
|
-
await put(store, index, piece(index));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
assert.equal(store.residentCount, 4, "more pieces stayed resident than the budget allows");
|
|
74
|
-
assert.ok(store.spilledCount >= 6, "pieces over budget were not written out");
|
|
75
|
-
|
|
76
|
-
// The earliest pieces can only come from disk now.
|
|
77
|
-
for (const index of [0, 1, 2, 5]) {
|
|
78
|
-
assert.deepEqual(await get(store, index), piece(index), `piece ${index} came back wrong`);
|
|
79
|
-
}
|
|
80
|
-
} finally {
|
|
81
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
82
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("the short last piece keeps its own length", async () => {
|
|
87
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "piece-store-"));
|
|
88
|
-
const store = new SharedPieceStore(CHUNK, {
|
|
89
|
-
length: CHUNK * 3 + 1234,
|
|
90
|
-
memoryBytes: CHUNK * 4,
|
|
91
|
-
path: directory,
|
|
92
|
-
name: "tail"
|
|
93
|
-
});
|
|
94
|
-
try {
|
|
95
|
-
const tail = piece(3, 1234);
|
|
96
|
-
await put(store, 3, tail);
|
|
97
|
-
const read = await get(store, 3);
|
|
98
|
-
assert.equal(read.length, 1234);
|
|
99
|
-
assert.deepEqual(read, tail);
|
|
100
|
-
} finally {
|
|
101
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
102
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a buffer handed out survives later writes to the same slot", async () => {
|
|
107
|
-
// This is the shape of the shipped defect: the caller keeps what it was
|
|
108
|
-
// given while the store carries on working.
|
|
109
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
110
|
-
try {
|
|
111
|
-
await put(store, 0, piece(0));
|
|
112
|
-
const held = await get(store, 0);
|
|
113
|
-
|
|
114
|
-
for (let index = 1; index < 6; index += 1) {
|
|
115
|
-
await put(store, index, piece(index));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
assert.deepEqual(held, piece(0), "the buffer changed under its holder");
|
|
119
|
-
} finally {
|
|
120
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
121
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("a pinned piece is not evicted to make room", async () => {
|
|
126
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
127
|
-
try {
|
|
128
|
-
await put(store, 0, piece(0));
|
|
129
|
-
await put(store, 1, piece(1));
|
|
130
|
-
|
|
131
|
-
store.pin(0);
|
|
132
|
-
await put(store, 2, piece(2));
|
|
133
|
-
|
|
134
|
-
const located = store.locate(0);
|
|
135
|
-
assert.ok(located, "the pinned piece was evicted while held");
|
|
136
|
-
const view = Buffer.from(located.buffer, located.offset, located.length);
|
|
137
|
-
assert.deepEqual(view, piece(0), "the pinned piece was overwritten in place");
|
|
138
|
-
|
|
139
|
-
store.unpin(0);
|
|
140
|
-
} finally {
|
|
141
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
142
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("refuses to make room when every resident piece is being read", async () => {
|
|
147
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
148
|
-
try {
|
|
149
|
-
await put(store, 0, piece(0));
|
|
150
|
-
await put(store, 1, piece(1));
|
|
151
|
-
store.pin(0);
|
|
152
|
-
store.pin(1);
|
|
153
|
-
|
|
154
|
-
// The pinned pieces stay where they are, and the arrival is kept anyway —
|
|
155
|
-
// on the disk, which is what it has. Refusing it was how a store short of
|
|
156
|
-
// memory ended a torrent: the refusal travelled out through the torrent
|
|
157
|
-
// client's own write callback and the client destroyed the torrent
|
|
158
|
-
// (field 2026-09-11).
|
|
159
|
-
await put(store, 2, piece(2));
|
|
160
|
-
assert.ok(store.locate(0), "the pinned piece was taken from under its reader");
|
|
161
|
-
assert.ok(store.locate(1), "the pinned piece was taken from under its reader");
|
|
162
|
-
assert.equal(store.locate(2), null, "and the arrival did not displace either of them");
|
|
163
|
-
assert.ok((await get(store, 2)).equals(piece(2)), "the arrival is kept, and reads back");
|
|
164
|
-
} finally {
|
|
165
|
-
store.unpin(0);
|
|
166
|
-
store.unpin(1);
|
|
167
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
168
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("a piece revived from disk is readable by offset again", async () => {
|
|
173
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
174
|
-
try {
|
|
175
|
-
await put(store, 0, piece(0));
|
|
176
|
-
await put(store, 1, piece(1));
|
|
177
|
-
await put(store, 2, piece(2)); // pushes piece 0 out to disk
|
|
178
|
-
assert.equal(store.locate(0), null, "piece 0 should have left memory");
|
|
179
|
-
|
|
180
|
-
await store.reside(0); // brings it back, which is what a playback read does
|
|
181
|
-
const located = store.locate(0);
|
|
182
|
-
assert.ok(located, "piece 0 was not brought back into memory");
|
|
183
|
-
const view = Buffer.from(located.buffer, located.offset, located.length);
|
|
184
|
-
assert.deepEqual(view, piece(0));
|
|
185
|
-
} finally {
|
|
186
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
187
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("a range within a piece is served correctly from memory and from disk", async () => {
|
|
192
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
193
|
-
try {
|
|
194
|
-
const source = piece(4);
|
|
195
|
-
await put(store, 4, source);
|
|
196
|
-
|
|
197
|
-
assert.deepEqual(
|
|
198
|
-
await get(store, 4, { offset: 100, length: 256 }),
|
|
199
|
-
source.subarray(100, 356),
|
|
200
|
-
"range from memory is wrong"
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
await put(store, 5, piece(5));
|
|
204
|
-
await put(store, 6, piece(6)); // piece 4 spills
|
|
205
|
-
|
|
206
|
-
assert.deepEqual(
|
|
207
|
-
await get(store, 4, { offset: 100, length: 256 }),
|
|
208
|
-
source.subarray(100, 356),
|
|
209
|
-
"range after revival is wrong"
|
|
210
|
-
);
|
|
211
|
-
} finally {
|
|
212
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
213
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("takes memory as it needs it, not the whole budget up front", async () => {
|
|
218
|
-
// The budget is per torrent, so a torrent that is merely open — or one being
|
|
219
|
-
// probed for its codecs — must not charge the host for the ceiling.
|
|
220
|
-
const { store, directory } = await makeStore({ pieces: 16, totalPieces: 64 });
|
|
221
|
-
try {
|
|
222
|
-
assert.equal(store.capacity, 16);
|
|
223
|
-
const initial = store.stats().committedBytes;
|
|
224
|
-
assert.ok(initial === 0, `claimed ${initial} bytes before holding anything`);
|
|
225
|
-
|
|
226
|
-
for (let index = 0; index < 5; index += 1) {
|
|
227
|
-
await put(store, index, piece(index));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
assert.equal(store.residentCount, 5);
|
|
231
|
-
assert.equal(store.stats().committedBytes, CHUNK * 5, "committed should equal resident");
|
|
232
|
-
assert.equal(store.stats().residentBytes, CHUNK * 5);
|
|
233
|
-
|
|
234
|
-
// Per-piece buffers must not disturb early pieces.
|
|
235
|
-
assert.deepEqual(await get(store, 0), piece(0), "an early piece was disturbed");
|
|
236
|
-
} finally {
|
|
237
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
238
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("counts where reads were served from, so the budget can be judged", async () => {
|
|
243
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
244
|
-
try {
|
|
245
|
-
await put(store, 0, piece(0));
|
|
246
|
-
await get(store, 0); // memory
|
|
247
|
-
await get(store, 0); // memory
|
|
248
|
-
|
|
249
|
-
await put(store, 1, piece(1));
|
|
250
|
-
await put(store, 2, piece(2)); // piece 0 spills
|
|
251
|
-
await get(store, 0); // disk
|
|
252
|
-
|
|
253
|
-
const stats = store.stats();
|
|
254
|
-
assert.equal(stats.fromMemory, 2, "memory reads miscounted");
|
|
255
|
-
assert.equal(stats.fromDisk, 1, "disk reads miscounted");
|
|
256
|
-
// One: piece 0 goes out to make room for piece 2. The read that follows is
|
|
257
|
-
// the torrent client's, and since 2026-09-11 it is answered from the disk
|
|
258
|
-
// copy without taking a block — so nothing has to be written out for it and
|
|
259
|
-
// nothing is revived. A read for PLAYBACK still populates memory, and pays
|
|
260
|
-
// for it with a spill; that is the next assertion.
|
|
261
|
-
assert.equal(stats.spills, 1, "spills miscounted");
|
|
262
|
-
assert.equal(stats.revivals, 0, "the client's own read does not revive");
|
|
263
|
-
|
|
264
|
-
await store.reside(0);
|
|
265
|
-
const afterPlayback = store.stats();
|
|
266
|
-
assert.equal(afterPlayback.revivals, 1, "a read for playback revives");
|
|
267
|
-
assert.equal(afterPlayback.spills, 2, "and pays for its block with a spill");
|
|
268
|
-
assert.equal(stats.blockedByPins, 0);
|
|
269
|
-
assert.equal(stats.capacity, 2);
|
|
270
|
-
} finally {
|
|
271
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
272
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("counts a refusal caused by pinned pieces", async () => {
|
|
277
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
278
|
-
try {
|
|
279
|
-
await put(store, 0, piece(0));
|
|
280
|
-
await put(store, 1, piece(1));
|
|
281
|
-
store.pin(0);
|
|
282
|
-
store.pin(1);
|
|
283
|
-
await put(store, 2, piece(2));
|
|
284
|
-
|
|
285
|
-
const stats = store.stats();
|
|
286
|
-
assert.equal(stats.blockedByPins, 1, "the store not being able to give a block went unrecorded");
|
|
287
|
-
assert.equal(stats.admittedWithoutSlot, 1, "and what it did instead went unrecorded");
|
|
288
|
-
} finally {
|
|
289
|
-
store.unpin(0);
|
|
290
|
-
store.unpin(1);
|
|
291
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
292
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test("destroy removes the spill file", async () => {
|
|
297
|
-
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
298
|
-
await put(store, 0, piece(0));
|
|
299
|
-
await put(store, 1, piece(1));
|
|
300
|
-
await put(store, 2, piece(2)); // forces a spill
|
|
301
|
-
|
|
302
|
-
const before = await fs.readdir(directory);
|
|
303
|
-
assert.ok(before.length > 0, "nothing was written to spill");
|
|
304
|
-
|
|
305
|
-
await new Promise((resolve) => store.destroy(resolve));
|
|
306
|
-
const after = await fs.readdir(directory);
|
|
307
|
-
assert.equal(after.length, 0, "the spill file outlived the store");
|
|
308
|
-
|
|
309
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
310
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Pieces in shared memory, spilling to disk.
|
|
3
|
+
*
|
|
4
|
+
* The cases worth having are the ones that describe past failures: a buffer
|
|
5
|
+
* handed out must not be invalidated by later activity (2.9.71 transferred
|
|
6
|
+
* memory it did not own), and a piece being read must not be evicted (2.9.71
|
|
7
|
+
* again, at the torrent level).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
16
|
+
|
|
17
|
+
const CHUNK = 64 * 1024;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @returns {Promise<{ store: SharedPieceStore, directory: string }>}
|
|
22
|
+
*/
|
|
23
|
+
async function makeStore({ pieces = 4, totalPieces = 16 } = {}) {
|
|
24
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "piece-store-"));
|
|
25
|
+
const store = new SharedPieceStore(CHUNK, {
|
|
26
|
+
length: CHUNK * totalPieces,
|
|
27
|
+
memoryBytes: CHUNK * pieces,
|
|
28
|
+
path: directory,
|
|
29
|
+
name: "test"
|
|
30
|
+
});
|
|
31
|
+
return { store, directory };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {number} index
|
|
36
|
+
* @param {number} [length]
|
|
37
|
+
* @returns {Buffer}
|
|
38
|
+
*/
|
|
39
|
+
function piece(index, length = CHUNK) {
|
|
40
|
+
const bytes = Buffer.allocUnsafeSlow(length);
|
|
41
|
+
bytes.fill(index % 256);
|
|
42
|
+
bytes.writeUInt32BE(index, 0);
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Promise-shaped wrappers, so the tests read as the sequence they describe. */
|
|
47
|
+
const put = (store, index, bytes) =>
|
|
48
|
+
new Promise((resolve, reject) => store.put(index, bytes, (error) => (error ? reject(error) : resolve())));
|
|
49
|
+
const get = (store, index, options) =>
|
|
50
|
+
new Promise((resolve, reject) =>
|
|
51
|
+
store.get(index, options, (error, bytes) => (error ? reject(error) : resolve(bytes)))
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
test("a stored piece comes back byte for byte", async () => {
|
|
55
|
+
const { store, directory } = await makeStore();
|
|
56
|
+
try {
|
|
57
|
+
await put(store, 0, piece(0));
|
|
58
|
+
const read = await get(store, 0);
|
|
59
|
+
assert.deepEqual(read, piece(0));
|
|
60
|
+
} finally {
|
|
61
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
62
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("pieces past the memory budget spill to disk and read back intact", async () => {
|
|
67
|
+
const { store, directory } = await makeStore({ pieces: 4, totalPieces: 16 });
|
|
68
|
+
try {
|
|
69
|
+
for (let index = 0; index < 10; index += 1) {
|
|
70
|
+
await put(store, index, piece(index));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
assert.equal(store.residentCount, 4, "more pieces stayed resident than the budget allows");
|
|
74
|
+
assert.ok(store.spilledCount >= 6, "pieces over budget were not written out");
|
|
75
|
+
|
|
76
|
+
// The earliest pieces can only come from disk now.
|
|
77
|
+
for (const index of [0, 1, 2, 5]) {
|
|
78
|
+
assert.deepEqual(await get(store, index), piece(index), `piece ${index} came back wrong`);
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
82
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("the short last piece keeps its own length", async () => {
|
|
87
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "piece-store-"));
|
|
88
|
+
const store = new SharedPieceStore(CHUNK, {
|
|
89
|
+
length: CHUNK * 3 + 1234,
|
|
90
|
+
memoryBytes: CHUNK * 4,
|
|
91
|
+
path: directory,
|
|
92
|
+
name: "tail"
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const tail = piece(3, 1234);
|
|
96
|
+
await put(store, 3, tail);
|
|
97
|
+
const read = await get(store, 3);
|
|
98
|
+
assert.equal(read.length, 1234);
|
|
99
|
+
assert.deepEqual(read, tail);
|
|
100
|
+
} finally {
|
|
101
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
102
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a buffer handed out survives later writes to the same slot", async () => {
|
|
107
|
+
// This is the shape of the shipped defect: the caller keeps what it was
|
|
108
|
+
// given while the store carries on working.
|
|
109
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
110
|
+
try {
|
|
111
|
+
await put(store, 0, piece(0));
|
|
112
|
+
const held = await get(store, 0);
|
|
113
|
+
|
|
114
|
+
for (let index = 1; index < 6; index += 1) {
|
|
115
|
+
await put(store, index, piece(index));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(held, piece(0), "the buffer changed under its holder");
|
|
119
|
+
} finally {
|
|
120
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
121
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a pinned piece is not evicted to make room", async () => {
|
|
126
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
127
|
+
try {
|
|
128
|
+
await put(store, 0, piece(0));
|
|
129
|
+
await put(store, 1, piece(1));
|
|
130
|
+
|
|
131
|
+
store.pin(0);
|
|
132
|
+
await put(store, 2, piece(2));
|
|
133
|
+
|
|
134
|
+
const located = store.locate(0);
|
|
135
|
+
assert.ok(located, "the pinned piece was evicted while held");
|
|
136
|
+
const view = Buffer.from(located.buffer, located.offset, located.length);
|
|
137
|
+
assert.deepEqual(view, piece(0), "the pinned piece was overwritten in place");
|
|
138
|
+
|
|
139
|
+
store.unpin(0);
|
|
140
|
+
} finally {
|
|
141
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
142
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("refuses to make room when every resident piece is being read", async () => {
|
|
147
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
148
|
+
try {
|
|
149
|
+
await put(store, 0, piece(0));
|
|
150
|
+
await put(store, 1, piece(1));
|
|
151
|
+
store.pin(0);
|
|
152
|
+
store.pin(1);
|
|
153
|
+
|
|
154
|
+
// The pinned pieces stay where they are, and the arrival is kept anyway —
|
|
155
|
+
// on the disk, which is what it has. Refusing it was how a store short of
|
|
156
|
+
// memory ended a torrent: the refusal travelled out through the torrent
|
|
157
|
+
// client's own write callback and the client destroyed the torrent
|
|
158
|
+
// (field 2026-09-11).
|
|
159
|
+
await put(store, 2, piece(2));
|
|
160
|
+
assert.ok(store.locate(0), "the pinned piece was taken from under its reader");
|
|
161
|
+
assert.ok(store.locate(1), "the pinned piece was taken from under its reader");
|
|
162
|
+
assert.equal(store.locate(2), null, "and the arrival did not displace either of them");
|
|
163
|
+
assert.ok((await get(store, 2)).equals(piece(2)), "the arrival is kept, and reads back");
|
|
164
|
+
} finally {
|
|
165
|
+
store.unpin(0);
|
|
166
|
+
store.unpin(1);
|
|
167
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
168
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a piece revived from disk is readable by offset again", async () => {
|
|
173
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
174
|
+
try {
|
|
175
|
+
await put(store, 0, piece(0));
|
|
176
|
+
await put(store, 1, piece(1));
|
|
177
|
+
await put(store, 2, piece(2)); // pushes piece 0 out to disk
|
|
178
|
+
assert.equal(store.locate(0), null, "piece 0 should have left memory");
|
|
179
|
+
|
|
180
|
+
await store.reside(0); // brings it back, which is what a playback read does
|
|
181
|
+
const located = store.locate(0);
|
|
182
|
+
assert.ok(located, "piece 0 was not brought back into memory");
|
|
183
|
+
const view = Buffer.from(located.buffer, located.offset, located.length);
|
|
184
|
+
assert.deepEqual(view, piece(0));
|
|
185
|
+
} finally {
|
|
186
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
187
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a range within a piece is served correctly from memory and from disk", async () => {
|
|
192
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
193
|
+
try {
|
|
194
|
+
const source = piece(4);
|
|
195
|
+
await put(store, 4, source);
|
|
196
|
+
|
|
197
|
+
assert.deepEqual(
|
|
198
|
+
await get(store, 4, { offset: 100, length: 256 }),
|
|
199
|
+
source.subarray(100, 356),
|
|
200
|
+
"range from memory is wrong"
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await put(store, 5, piece(5));
|
|
204
|
+
await put(store, 6, piece(6)); // piece 4 spills
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(
|
|
207
|
+
await get(store, 4, { offset: 100, length: 256 }),
|
|
208
|
+
source.subarray(100, 356),
|
|
209
|
+
"range after revival is wrong"
|
|
210
|
+
);
|
|
211
|
+
} finally {
|
|
212
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
213
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("takes memory as it needs it, not the whole budget up front", async () => {
|
|
218
|
+
// The budget is per torrent, so a torrent that is merely open — or one being
|
|
219
|
+
// probed for its codecs — must not charge the host for the ceiling.
|
|
220
|
+
const { store, directory } = await makeStore({ pieces: 16, totalPieces: 64 });
|
|
221
|
+
try {
|
|
222
|
+
assert.equal(store.capacity, 16);
|
|
223
|
+
const initial = store.stats().committedBytes;
|
|
224
|
+
assert.ok(initial === 0, `claimed ${initial} bytes before holding anything`);
|
|
225
|
+
|
|
226
|
+
for (let index = 0; index < 5; index += 1) {
|
|
227
|
+
await put(store, index, piece(index));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
assert.equal(store.residentCount, 5);
|
|
231
|
+
assert.equal(store.stats().committedBytes, CHUNK * 5, "committed should equal resident");
|
|
232
|
+
assert.equal(store.stats().residentBytes, CHUNK * 5);
|
|
233
|
+
|
|
234
|
+
// Per-piece buffers must not disturb early pieces.
|
|
235
|
+
assert.deepEqual(await get(store, 0), piece(0), "an early piece was disturbed");
|
|
236
|
+
} finally {
|
|
237
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
238
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("counts where reads were served from, so the budget can be judged", async () => {
|
|
243
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
244
|
+
try {
|
|
245
|
+
await put(store, 0, piece(0));
|
|
246
|
+
await get(store, 0); // memory
|
|
247
|
+
await get(store, 0); // memory
|
|
248
|
+
|
|
249
|
+
await put(store, 1, piece(1));
|
|
250
|
+
await put(store, 2, piece(2)); // piece 0 spills
|
|
251
|
+
await get(store, 0); // disk
|
|
252
|
+
|
|
253
|
+
const stats = store.stats();
|
|
254
|
+
assert.equal(stats.fromMemory, 2, "memory reads miscounted");
|
|
255
|
+
assert.equal(stats.fromDisk, 1, "disk reads miscounted");
|
|
256
|
+
// One: piece 0 goes out to make room for piece 2. The read that follows is
|
|
257
|
+
// the torrent client's, and since 2026-09-11 it is answered from the disk
|
|
258
|
+
// copy without taking a block — so nothing has to be written out for it and
|
|
259
|
+
// nothing is revived. A read for PLAYBACK still populates memory, and pays
|
|
260
|
+
// for it with a spill; that is the next assertion.
|
|
261
|
+
assert.equal(stats.spills, 1, "spills miscounted");
|
|
262
|
+
assert.equal(stats.revivals, 0, "the client's own read does not revive");
|
|
263
|
+
|
|
264
|
+
await store.reside(0);
|
|
265
|
+
const afterPlayback = store.stats();
|
|
266
|
+
assert.equal(afterPlayback.revivals, 1, "a read for playback revives");
|
|
267
|
+
assert.equal(afterPlayback.spills, 2, "and pays for its block with a spill");
|
|
268
|
+
assert.equal(stats.blockedByPins, 0);
|
|
269
|
+
assert.equal(stats.capacity, 2);
|
|
270
|
+
} finally {
|
|
271
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
272
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("counts a refusal caused by pinned pieces", async () => {
|
|
277
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
278
|
+
try {
|
|
279
|
+
await put(store, 0, piece(0));
|
|
280
|
+
await put(store, 1, piece(1));
|
|
281
|
+
store.pin(0);
|
|
282
|
+
store.pin(1);
|
|
283
|
+
await put(store, 2, piece(2));
|
|
284
|
+
|
|
285
|
+
const stats = store.stats();
|
|
286
|
+
assert.equal(stats.blockedByPins, 1, "the store not being able to give a block went unrecorded");
|
|
287
|
+
assert.equal(stats.admittedWithoutSlot, 1, "and what it did instead went unrecorded");
|
|
288
|
+
} finally {
|
|
289
|
+
store.unpin(0);
|
|
290
|
+
store.unpin(1);
|
|
291
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
292
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("destroy removes the spill file", async () => {
|
|
297
|
+
const { store, directory } = await makeStore({ pieces: 2, totalPieces: 8 });
|
|
298
|
+
await put(store, 0, piece(0));
|
|
299
|
+
await put(store, 1, piece(1));
|
|
300
|
+
await put(store, 2, piece(2)); // forces a spill
|
|
301
|
+
|
|
302
|
+
const before = await fs.readdir(directory);
|
|
303
|
+
assert.ok(before.length > 0, "nothing was written to spill");
|
|
304
|
+
|
|
305
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
306
|
+
const after = await fs.readdir(directory);
|
|
307
|
+
assert.equal(after.length, 0, "the spill file outlived the store");
|
|
308
|
+
|
|
309
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
310
|
+
});
|