@torrent-tv/proxy 2.87.21 → 2.88.0
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 +4 -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/browser-proxy-contract.md +133 -133
- 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/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +106 -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 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +173 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +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 +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +758 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +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 +1141 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +315 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +515 -515
- 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 +245 -245
- 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 +931 -931
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +696 -696
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -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/episode-naming.js +182 -0
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -0
- 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-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +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 +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +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 +12 -2
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +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,186 +1,186 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Uploading generously at the two moments a viewer is waiting.
|
|
3
|
-
*
|
|
4
|
-
* BitTorrent peers serve those who serve them: each re-ranks its takers about
|
|
5
|
-
* every 10 s and opens a few slots to whoever uploaded most, plus one at
|
|
6
|
-
* random. Uploading a token 8-50 KB/s means being picked at random, one slot
|
|
7
|
-
* per cycle. Measured 2026-08-04 on a session with 96 peers already connected:
|
|
8
|
-
* 64 KB/s after 2 s, 1.6 MB/s after 4 s, 4.8 MB/s after 8 s — and the 16 MB the
|
|
9
|
-
* codec probe needs took 8.36 s of the 11.46 s before playback began.
|
|
10
|
-
*
|
|
11
|
-
* The reciprocity boost could not help: it waits for the download to be all but
|
|
12
|
-
* dead (below 200 KB/s) with peers visibly choking us, which a ramp is not. In
|
|
13
|
-
* that session it first moved the limit 13.3 s after the torrent was added, and
|
|
14
|
-
* reached the generous rate at 43.7 s — both after the wait they were meant to
|
|
15
|
-
* shorten.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import { decideUploadLimit, torrentsForUploadPolicy } from "../services/torrent/torrent-pool.js";
|
|
21
|
-
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
22
|
-
import { Urgency } from "../services/torrent/demand/index.js";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* A torrent whose metadata has arrived and which nothing is wanted of.
|
|
26
|
-
*
|
|
27
|
-
* The files matter: a torrent with no list of files cannot be stated about at
|
|
28
|
-
* all, and is wanted for that reason alone — it is still being fetched.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} name
|
|
31
|
-
* @returns {object}
|
|
32
|
-
*/
|
|
33
|
-
function quiet(name) {
|
|
34
|
-
return { name, files: [{ offset: 0, length: 1_000, name }] };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The same, with something stated for it — which is what "somebody wants this"
|
|
39
|
-
* means now that nothing counts readers.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} name
|
|
42
|
-
* @returns {object}
|
|
43
|
-
*/
|
|
44
|
-
function wanted(name) {
|
|
45
|
-
const torrent = quiet(name);
|
|
46
|
-
demandFor(torrent).register.state({
|
|
47
|
-
claimant: "priority-map:0:0",
|
|
48
|
-
fileIndex: 0,
|
|
49
|
-
byteStart: 0,
|
|
50
|
-
byteEnd: 999,
|
|
51
|
-
urgency: Urgency.NEAR
|
|
52
|
-
});
|
|
53
|
-
return torrent;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const NOW = 1_000_000;
|
|
57
|
-
const healthy = (extra = {}) => ({
|
|
58
|
-
name: "film.mkv",
|
|
59
|
-
wires: [{ amInterested: true, peerChoking: false }],
|
|
60
|
-
downloadSpeed: 5 * 1024 * 1024,
|
|
61
|
-
done: false,
|
|
62
|
-
...extra
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("a torrent in a hurry gets the generous rate even while downloading well", () => {
|
|
66
|
-
const decision = decideUploadLimit([healthy({ hurryUntil: NOW + 10_000 })], { now: NOW });
|
|
67
|
-
assert.equal(decision.bytesPerSec, 512 * 1024);
|
|
68
|
-
assert.match(decision.reason, /in a hurry/);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("the hurry expires on its own", () => {
|
|
72
|
-
const decision = decideUploadLimit([healthy({ hurryUntil: NOW - 1 })], { now: NOW });
|
|
73
|
-
assert.equal(decision.bytesPerSec, 50 * 1024, "back to the token upload once the rush is over");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("a finished torrent is never in a hurry", () => {
|
|
77
|
-
// Nothing left to download, so there is nothing to buy with the upload — and
|
|
78
|
-
// seeding is what we deliberately avoid.
|
|
79
|
-
const decision = decideUploadLimit([healthy({ hurryUntil: NOW + 10_000, done: true })], { now: NOW });
|
|
80
|
-
assert.equal(decision.bytesPerSec, 50 * 1024);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("nothing being watched still means near-silence", () => {
|
|
84
|
-
assert.equal(decideUploadLimit([], { now: NOW }).bytesPerSec, 8 * 1024);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("the reciprocity boost still works when no hurry is on", () => {
|
|
88
|
-
const starving = {
|
|
89
|
-
name: "film.mkv",
|
|
90
|
-
wires: [
|
|
91
|
-
{ amInterested: true, peerChoking: true },
|
|
92
|
-
{ amInterested: true, peerChoking: true }
|
|
93
|
-
],
|
|
94
|
-
downloadSpeed: 10 * 1024,
|
|
95
|
-
done: false
|
|
96
|
-
};
|
|
97
|
-
const decision = decideUploadLimit([starving], { now: NOW });
|
|
98
|
-
assert.equal(decision.bytesPerSec, 512 * 1024);
|
|
99
|
-
assert.match(decision.reason, /earn unchoke/);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("a torrent nothing is wanted of still reaches the policy while it is in a hurry", () => {
|
|
103
|
-
// The moment that matters: a torrent has just been added and its head and
|
|
104
|
-
// tail are being fetched for the codec probe. Nothing is stated for it yet,
|
|
105
|
-
// and the selection only ever kept torrents something was — which is where
|
|
106
|
-
// the gap was.
|
|
107
|
-
const hurrying = quiet("film.mkv");
|
|
108
|
-
hurrying.hurryUntil = NOW + 20_000;
|
|
109
|
-
const idle = quiet("other.mkv");
|
|
110
|
-
const watched = wanted("watched.mkv");
|
|
111
|
-
try {
|
|
112
|
-
assert.deepEqual(
|
|
113
|
-
torrentsForUploadPolicy([hurrying, idle], NOW),
|
|
114
|
-
[hurrying],
|
|
115
|
-
"a torrent nothing is stated for yet was ignored"
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
assert.deepEqual(
|
|
119
|
-
torrentsForUploadPolicy([{ ...hurrying, hurryUntil: NOW - 1 }, idle], NOW),
|
|
120
|
-
[],
|
|
121
|
-
"and stops counting once the rush is over"
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
assert.deepEqual(
|
|
125
|
-
torrentsForUploadPolicy([watched], NOW),
|
|
126
|
-
[watched],
|
|
127
|
-
"a torrent somebody wants something of still counts, hurry or not"
|
|
128
|
-
);
|
|
129
|
-
} finally {
|
|
130
|
-
forgetTorrent(hurrying);
|
|
131
|
-
forgetTorrent(idle);
|
|
132
|
-
forgetTorrent(watched);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("a torrent nobody is reading is not treated as starving", () => {
|
|
137
|
-
// The encoder is suspended once it is far enough ahead, and while it is
|
|
138
|
-
// suspended nothing is requested — so the download reads zero without anyone
|
|
139
|
-
// waiting. Measured before this guard: four cycles of 512 -> 50 KB/s in three
|
|
140
|
-
// minutes, every one of them reported as `earn unchoke ... down=0KB/s`.
|
|
141
|
-
const idleButChoked = {
|
|
142
|
-
name: "film.mkv",
|
|
143
|
-
isWanted: false,
|
|
144
|
-
wires: [
|
|
145
|
-
{ amInterested: true, peerChoking: true },
|
|
146
|
-
{ amInterested: true, peerChoking: true }
|
|
147
|
-
],
|
|
148
|
-
downloadSpeed: 0,
|
|
149
|
-
done: false
|
|
150
|
-
};
|
|
151
|
-
assert.equal(decideUploadLimit([idleButChoked], { now: NOW }).bytesPerSec, 50 * 1024);
|
|
152
|
-
|
|
153
|
-
const waiting = { ...idleButChoked, isWanted: true };
|
|
154
|
-
assert.equal(
|
|
155
|
-
decideUploadLimit([waiting], { now: NOW }).bytesPerSec,
|
|
156
|
-
512 * 1024,
|
|
157
|
-
"a reader that IS waiting must still earn unchoke slots"
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("a torrent short of nothing is not worth uploading for", () => {
|
|
162
|
-
// Field 2026-09-11: the file being watched was complete, its windows all
|
|
163
|
-
// present, and the proxy went on offering 512 KB/s to 596 peers for
|
|
164
|
-
// forty-eight minutes — reading a 4 MB piece off the disk for every 16 KB it
|
|
165
|
-
// sent. Upload is bought with reciprocity, and reciprocity is only worth
|
|
166
|
-
// buying while somebody is still short of bytes.
|
|
167
|
-
const complete = {
|
|
168
|
-
name: "watched to the end",
|
|
169
|
-
isWanted: true,
|
|
170
|
-
hasUnmetDemand: false,
|
|
171
|
-
done: false,
|
|
172
|
-
downloadSpeed: 0,
|
|
173
|
-
wires: [
|
|
174
|
-
{ amInterested: true, peerChoking: true },
|
|
175
|
-
{ amInterested: true, peerChoking: true },
|
|
176
|
-
{ amInterested: true, peerChoking: true }
|
|
177
|
-
]
|
|
178
|
-
};
|
|
179
|
-
const decided = decideUploadLimit([complete]);
|
|
180
|
-
assert.equal(decided.bytesPerSec, 8 * 1024, "a complete torrent is given the idle floor");
|
|
181
|
-
assert.match(decided.reason, /buys nothing/);
|
|
182
|
-
|
|
183
|
-
// And a torrent that IS short of something still earns its unchoke.
|
|
184
|
-
const short = { ...complete, hasUnmetDemand: true };
|
|
185
|
-
assert.ok(decideUploadLimit([short]).bytesPerSec > 8 * 1024, "a starving torrent still buys reciprocity");
|
|
186
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Uploading generously at the two moments a viewer is waiting.
|
|
3
|
+
*
|
|
4
|
+
* BitTorrent peers serve those who serve them: each re-ranks its takers about
|
|
5
|
+
* every 10 s and opens a few slots to whoever uploaded most, plus one at
|
|
6
|
+
* random. Uploading a token 8-50 KB/s means being picked at random, one slot
|
|
7
|
+
* per cycle. Measured 2026-08-04 on a session with 96 peers already connected:
|
|
8
|
+
* 64 KB/s after 2 s, 1.6 MB/s after 4 s, 4.8 MB/s after 8 s — and the 16 MB the
|
|
9
|
+
* codec probe needs took 8.36 s of the 11.46 s before playback began.
|
|
10
|
+
*
|
|
11
|
+
* The reciprocity boost could not help: it waits for the download to be all but
|
|
12
|
+
* dead (below 200 KB/s) with peers visibly choking us, which a ramp is not. In
|
|
13
|
+
* that session it first moved the limit 13.3 s after the torrent was added, and
|
|
14
|
+
* reached the generous rate at 43.7 s — both after the wait they were meant to
|
|
15
|
+
* shorten.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { decideUploadLimit, torrentsForUploadPolicy } from "../services/torrent/torrent-pool.js";
|
|
21
|
+
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
22
|
+
import { Urgency } from "../services/torrent/demand/index.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A torrent whose metadata has arrived and which nothing is wanted of.
|
|
26
|
+
*
|
|
27
|
+
* The files matter: a torrent with no list of files cannot be stated about at
|
|
28
|
+
* all, and is wanted for that reason alone — it is still being fetched.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @returns {object}
|
|
32
|
+
*/
|
|
33
|
+
function quiet(name) {
|
|
34
|
+
return { name, files: [{ offset: 0, length: 1_000, name }] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The same, with something stated for it — which is what "somebody wants this"
|
|
39
|
+
* means now that nothing counts readers.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} name
|
|
42
|
+
* @returns {object}
|
|
43
|
+
*/
|
|
44
|
+
function wanted(name) {
|
|
45
|
+
const torrent = quiet(name);
|
|
46
|
+
demandFor(torrent).register.state({
|
|
47
|
+
claimant: "priority-map:0:0",
|
|
48
|
+
fileIndex: 0,
|
|
49
|
+
byteStart: 0,
|
|
50
|
+
byteEnd: 999,
|
|
51
|
+
urgency: Urgency.NEAR
|
|
52
|
+
});
|
|
53
|
+
return torrent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const NOW = 1_000_000;
|
|
57
|
+
const healthy = (extra = {}) => ({
|
|
58
|
+
name: "film.mkv",
|
|
59
|
+
wires: [{ amInterested: true, peerChoking: false }],
|
|
60
|
+
downloadSpeed: 5 * 1024 * 1024,
|
|
61
|
+
done: false,
|
|
62
|
+
...extra
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a torrent in a hurry gets the generous rate even while downloading well", () => {
|
|
66
|
+
const decision = decideUploadLimit([healthy({ hurryUntil: NOW + 10_000 })], { now: NOW });
|
|
67
|
+
assert.equal(decision.bytesPerSec, 512 * 1024);
|
|
68
|
+
assert.match(decision.reason, /in a hurry/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the hurry expires on its own", () => {
|
|
72
|
+
const decision = decideUploadLimit([healthy({ hurryUntil: NOW - 1 })], { now: NOW });
|
|
73
|
+
assert.equal(decision.bytesPerSec, 50 * 1024, "back to the token upload once the rush is over");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a finished torrent is never in a hurry", () => {
|
|
77
|
+
// Nothing left to download, so there is nothing to buy with the upload — and
|
|
78
|
+
// seeding is what we deliberately avoid.
|
|
79
|
+
const decision = decideUploadLimit([healthy({ hurryUntil: NOW + 10_000, done: true })], { now: NOW });
|
|
80
|
+
assert.equal(decision.bytesPerSec, 50 * 1024);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("nothing being watched still means near-silence", () => {
|
|
84
|
+
assert.equal(decideUploadLimit([], { now: NOW }).bytesPerSec, 8 * 1024);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the reciprocity boost still works when no hurry is on", () => {
|
|
88
|
+
const starving = {
|
|
89
|
+
name: "film.mkv",
|
|
90
|
+
wires: [
|
|
91
|
+
{ amInterested: true, peerChoking: true },
|
|
92
|
+
{ amInterested: true, peerChoking: true }
|
|
93
|
+
],
|
|
94
|
+
downloadSpeed: 10 * 1024,
|
|
95
|
+
done: false
|
|
96
|
+
};
|
|
97
|
+
const decision = decideUploadLimit([starving], { now: NOW });
|
|
98
|
+
assert.equal(decision.bytesPerSec, 512 * 1024);
|
|
99
|
+
assert.match(decision.reason, /earn unchoke/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a torrent nothing is wanted of still reaches the policy while it is in a hurry", () => {
|
|
103
|
+
// The moment that matters: a torrent has just been added and its head and
|
|
104
|
+
// tail are being fetched for the codec probe. Nothing is stated for it yet,
|
|
105
|
+
// and the selection only ever kept torrents something was — which is where
|
|
106
|
+
// the gap was.
|
|
107
|
+
const hurrying = quiet("film.mkv");
|
|
108
|
+
hurrying.hurryUntil = NOW + 20_000;
|
|
109
|
+
const idle = quiet("other.mkv");
|
|
110
|
+
const watched = wanted("watched.mkv");
|
|
111
|
+
try {
|
|
112
|
+
assert.deepEqual(
|
|
113
|
+
torrentsForUploadPolicy([hurrying, idle], NOW),
|
|
114
|
+
[hurrying],
|
|
115
|
+
"a torrent nothing is stated for yet was ignored"
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(
|
|
119
|
+
torrentsForUploadPolicy([{ ...hurrying, hurryUntil: NOW - 1 }, idle], NOW),
|
|
120
|
+
[],
|
|
121
|
+
"and stops counting once the rush is over"
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
torrentsForUploadPolicy([watched], NOW),
|
|
126
|
+
[watched],
|
|
127
|
+
"a torrent somebody wants something of still counts, hurry or not"
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
forgetTorrent(hurrying);
|
|
131
|
+
forgetTorrent(idle);
|
|
132
|
+
forgetTorrent(watched);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a torrent nobody is reading is not treated as starving", () => {
|
|
137
|
+
// The encoder is suspended once it is far enough ahead, and while it is
|
|
138
|
+
// suspended nothing is requested — so the download reads zero without anyone
|
|
139
|
+
// waiting. Measured before this guard: four cycles of 512 -> 50 KB/s in three
|
|
140
|
+
// minutes, every one of them reported as `earn unchoke ... down=0KB/s`.
|
|
141
|
+
const idleButChoked = {
|
|
142
|
+
name: "film.mkv",
|
|
143
|
+
isWanted: false,
|
|
144
|
+
wires: [
|
|
145
|
+
{ amInterested: true, peerChoking: true },
|
|
146
|
+
{ amInterested: true, peerChoking: true }
|
|
147
|
+
],
|
|
148
|
+
downloadSpeed: 0,
|
|
149
|
+
done: false
|
|
150
|
+
};
|
|
151
|
+
assert.equal(decideUploadLimit([idleButChoked], { now: NOW }).bytesPerSec, 50 * 1024);
|
|
152
|
+
|
|
153
|
+
const waiting = { ...idleButChoked, isWanted: true };
|
|
154
|
+
assert.equal(
|
|
155
|
+
decideUploadLimit([waiting], { now: NOW }).bytesPerSec,
|
|
156
|
+
512 * 1024,
|
|
157
|
+
"a reader that IS waiting must still earn unchoke slots"
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a torrent short of nothing is not worth uploading for", () => {
|
|
162
|
+
// Field 2026-09-11: the file being watched was complete, its windows all
|
|
163
|
+
// present, and the proxy went on offering 512 KB/s to 596 peers for
|
|
164
|
+
// forty-eight minutes — reading a 4 MB piece off the disk for every 16 KB it
|
|
165
|
+
// sent. Upload is bought with reciprocity, and reciprocity is only worth
|
|
166
|
+
// buying while somebody is still short of bytes.
|
|
167
|
+
const complete = {
|
|
168
|
+
name: "watched to the end",
|
|
169
|
+
isWanted: true,
|
|
170
|
+
hasUnmetDemand: false,
|
|
171
|
+
done: false,
|
|
172
|
+
downloadSpeed: 0,
|
|
173
|
+
wires: [
|
|
174
|
+
{ amInterested: true, peerChoking: true },
|
|
175
|
+
{ amInterested: true, peerChoking: true },
|
|
176
|
+
{ amInterested: true, peerChoking: true }
|
|
177
|
+
]
|
|
178
|
+
};
|
|
179
|
+
const decided = decideUploadLimit([complete]);
|
|
180
|
+
assert.equal(decided.bytesPerSec, 8 * 1024, "a complete torrent is given the idle floor");
|
|
181
|
+
assert.match(decided.reason, /buys nothing/);
|
|
182
|
+
|
|
183
|
+
// And a torrent that IS short of something still earns its unchoke.
|
|
184
|
+
const short = { ...complete, hasUnmetDemand: true };
|
|
185
|
+
assert.ok(decideUploadLimit([short]).bytesPerSec > 8 * 1024, "a starving torrent still buys reciprocity");
|
|
186
|
+
});
|
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The usrsctp state reader's gating and command construction.
|
|
3
|
-
*
|
|
4
|
-
* Roadmap item 11: reads usrsctp's live association state via gdb the moment
|
|
5
|
-
* a wedge is declared. Everything here is the part that decides WHEN and WITH
|
|
6
|
-
* WHAT ARGUMENTS — the spawning itself is thin glue around these, the same
|
|
7
|
-
* shape as the packet witness (test/packet-witness.test.js).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { EventEmitter } from "node:events";
|
|
13
|
-
|
|
14
|
-
import { createUsrsctpStateReader, SCTPSTATE_SCRIPT_PATH } from "../services/transport/usrsctp-state.js";
|
|
15
|
-
|
|
16
|
-
class FakeChild extends EventEmitter {
|
|
17
|
-
constructor(command, args) {
|
|
18
|
-
super();
|
|
19
|
-
this.command = command;
|
|
20
|
-
this.args = args;
|
|
21
|
-
this.stdout = new EventEmitter();
|
|
22
|
-
this.stderr = new EventEmitter();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
kill() {
|
|
26
|
-
// Not exercised by these tests — every fake run finishes on its own.
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {string} output - What the fake gdb writes to stdout before closing.
|
|
33
|
-
* @returns {{ spawnProcess: Function, calls: Array<{ command: string, args: string[] }> }}
|
|
34
|
-
*/
|
|
35
|
-
function makeSpawn(output) {
|
|
36
|
-
const calls = [];
|
|
37
|
-
const spawnProcess = (command, args) => {
|
|
38
|
-
calls.push({ command, args });
|
|
39
|
-
const child = new FakeChild(command, args);
|
|
40
|
-
setImmediate(() => {
|
|
41
|
-
if (output) {
|
|
42
|
-
child.stdout.emit("data", Buffer.from(output));
|
|
43
|
-
}
|
|
44
|
-
child.emit("close", 0);
|
|
45
|
-
});
|
|
46
|
-
return child;
|
|
47
|
-
};
|
|
48
|
-
return { spawnProcess, calls };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Wait until `check` holds, rather than for a chosen interval. */
|
|
52
|
-
async function waitFor(check) {
|
|
53
|
-
const deadline = Date.now() + 5_000;
|
|
54
|
-
for (;;) {
|
|
55
|
-
if (check()) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (Date.now() > deadline) {
|
|
59
|
-
throw new Error("timed out waiting for the reading to be logged");
|
|
60
|
-
}
|
|
61
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
test("the deadline is counted outside the process gdb stops", async () => {
|
|
66
|
-
const { spawnProcess, calls } = makeSpawn("state=8 rwnd=95890\n");
|
|
67
|
-
const lines = [];
|
|
68
|
-
const reader = createUsrsctpStateReader({
|
|
69
|
-
log: (message) => lines.push(message),
|
|
70
|
-
spawnProcess,
|
|
71
|
-
pid: 4242
|
|
72
|
-
});
|
|
73
|
-
const started = reader.maybeRead("test wedge");
|
|
74
|
-
assert.equal(started, true);
|
|
75
|
-
await waitFor(() => lines.length > 0);
|
|
76
|
-
assert.equal(calls.length, 1);
|
|
77
|
-
// `timeout` is a separate process, so it keeps counting while gdb holds every
|
|
78
|
-
// thread of this one. A `setTimeout` here cannot fire — measured in the field
|
|
79
|
-
// on 2026-09-05, where gdb held the proxy for four minutes and the guard set
|
|
80
|
-
// for fifteen seconds never ran.
|
|
81
|
-
assert.equal(calls[0].command, "timeout");
|
|
82
|
-
assert.deepEqual(calls[0].args, [
|
|
83
|
-
"-s",
|
|
84
|
-
"KILL",
|
|
85
|
-
"15",
|
|
86
|
-
"gdb",
|
|
87
|
-
"-q",
|
|
88
|
-
"-batch",
|
|
89
|
-
"-p",
|
|
90
|
-
"4242",
|
|
91
|
-
"-x",
|
|
92
|
-
SCTPSTATE_SCRIPT_PATH
|
|
93
|
-
]);
|
|
94
|
-
assert.match(lines[0], /state=8 rwnd=95890/);
|
|
95
|
-
assert.match(lines[0], /test wedge/);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("no output is reported as no reading, not silence", async () => {
|
|
99
|
-
const { spawnProcess } = makeSpawn("");
|
|
100
|
-
const lines = [];
|
|
101
|
-
const reader = createUsrsctpStateReader({ log: (message) => lines.push(message), spawnProcess, pid: 1 });
|
|
102
|
-
reader.maybeRead("empty case");
|
|
103
|
-
await waitFor(() => lines.length > 0);
|
|
104
|
-
assert.match(lines[0], /no reading/);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a second read is refused within the cooldown, like the packet witness", async () => {
|
|
108
|
-
const { spawnProcess, calls } = makeSpawn("state=8\n");
|
|
109
|
-
const lines = [];
|
|
110
|
-
const reader = createUsrsctpStateReader({
|
|
111
|
-
log: (message) => lines.push(message),
|
|
112
|
-
spawnProcess,
|
|
113
|
-
pid: 1,
|
|
114
|
-
cooldownMs: 60_000
|
|
115
|
-
});
|
|
116
|
-
assert.equal(reader.maybeRead("first"), true);
|
|
117
|
-
await waitFor(() => lines.length > 0);
|
|
118
|
-
assert.equal(reader.maybeRead("second, too soon"), false);
|
|
119
|
-
assert.equal(calls.length, 1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("a missing gdb is reported, not thrown", async () => {
|
|
123
|
-
const lines = [];
|
|
124
|
-
const spawnProcess = () => {
|
|
125
|
-
throw new Error("spawn gdb ENOENT");
|
|
126
|
-
};
|
|
127
|
-
const reader = createUsrsctpStateReader({ log: (message) => lines.push(message), spawnProcess, pid: 1 });
|
|
128
|
-
reader.maybeRead("no gdb on this host");
|
|
129
|
-
await waitFor(() => lines.length > 0);
|
|
130
|
-
assert.match(lines[0], /could not start gdb/);
|
|
131
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The usrsctp state reader's gating and command construction.
|
|
3
|
+
*
|
|
4
|
+
* Roadmap item 11: reads usrsctp's live association state via gdb the moment
|
|
5
|
+
* a wedge is declared. Everything here is the part that decides WHEN and WITH
|
|
6
|
+
* WHAT ARGUMENTS — the spawning itself is thin glue around these, the same
|
|
7
|
+
* shape as the packet witness (test/packet-witness.test.js).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
|
|
14
|
+
import { createUsrsctpStateReader, SCTPSTATE_SCRIPT_PATH } from "../services/transport/usrsctp-state.js";
|
|
15
|
+
|
|
16
|
+
class FakeChild extends EventEmitter {
|
|
17
|
+
constructor(command, args) {
|
|
18
|
+
super();
|
|
19
|
+
this.command = command;
|
|
20
|
+
this.args = args;
|
|
21
|
+
this.stdout = new EventEmitter();
|
|
22
|
+
this.stderr = new EventEmitter();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
kill() {
|
|
26
|
+
// Not exercised by these tests — every fake run finishes on its own.
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} output - What the fake gdb writes to stdout before closing.
|
|
33
|
+
* @returns {{ spawnProcess: Function, calls: Array<{ command: string, args: string[] }> }}
|
|
34
|
+
*/
|
|
35
|
+
function makeSpawn(output) {
|
|
36
|
+
const calls = [];
|
|
37
|
+
const spawnProcess = (command, args) => {
|
|
38
|
+
calls.push({ command, args });
|
|
39
|
+
const child = new FakeChild(command, args);
|
|
40
|
+
setImmediate(() => {
|
|
41
|
+
if (output) {
|
|
42
|
+
child.stdout.emit("data", Buffer.from(output));
|
|
43
|
+
}
|
|
44
|
+
child.emit("close", 0);
|
|
45
|
+
});
|
|
46
|
+
return child;
|
|
47
|
+
};
|
|
48
|
+
return { spawnProcess, calls };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Wait until `check` holds, rather than for a chosen interval. */
|
|
52
|
+
async function waitFor(check) {
|
|
53
|
+
const deadline = Date.now() + 5_000;
|
|
54
|
+
for (;;) {
|
|
55
|
+
if (check()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (Date.now() > deadline) {
|
|
59
|
+
throw new Error("timed out waiting for the reading to be logged");
|
|
60
|
+
}
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test("the deadline is counted outside the process gdb stops", async () => {
|
|
66
|
+
const { spawnProcess, calls } = makeSpawn("state=8 rwnd=95890\n");
|
|
67
|
+
const lines = [];
|
|
68
|
+
const reader = createUsrsctpStateReader({
|
|
69
|
+
log: (message) => lines.push(message),
|
|
70
|
+
spawnProcess,
|
|
71
|
+
pid: 4242
|
|
72
|
+
});
|
|
73
|
+
const started = reader.maybeRead("test wedge");
|
|
74
|
+
assert.equal(started, true);
|
|
75
|
+
await waitFor(() => lines.length > 0);
|
|
76
|
+
assert.equal(calls.length, 1);
|
|
77
|
+
// `timeout` is a separate process, so it keeps counting while gdb holds every
|
|
78
|
+
// thread of this one. A `setTimeout` here cannot fire — measured in the field
|
|
79
|
+
// on 2026-09-05, where gdb held the proxy for four minutes and the guard set
|
|
80
|
+
// for fifteen seconds never ran.
|
|
81
|
+
assert.equal(calls[0].command, "timeout");
|
|
82
|
+
assert.deepEqual(calls[0].args, [
|
|
83
|
+
"-s",
|
|
84
|
+
"KILL",
|
|
85
|
+
"15",
|
|
86
|
+
"gdb",
|
|
87
|
+
"-q",
|
|
88
|
+
"-batch",
|
|
89
|
+
"-p",
|
|
90
|
+
"4242",
|
|
91
|
+
"-x",
|
|
92
|
+
SCTPSTATE_SCRIPT_PATH
|
|
93
|
+
]);
|
|
94
|
+
assert.match(lines[0], /state=8 rwnd=95890/);
|
|
95
|
+
assert.match(lines[0], /test wedge/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("no output is reported as no reading, not silence", async () => {
|
|
99
|
+
const { spawnProcess } = makeSpawn("");
|
|
100
|
+
const lines = [];
|
|
101
|
+
const reader = createUsrsctpStateReader({ log: (message) => lines.push(message), spawnProcess, pid: 1 });
|
|
102
|
+
reader.maybeRead("empty case");
|
|
103
|
+
await waitFor(() => lines.length > 0);
|
|
104
|
+
assert.match(lines[0], /no reading/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a second read is refused within the cooldown, like the packet witness", async () => {
|
|
108
|
+
const { spawnProcess, calls } = makeSpawn("state=8\n");
|
|
109
|
+
const lines = [];
|
|
110
|
+
const reader = createUsrsctpStateReader({
|
|
111
|
+
log: (message) => lines.push(message),
|
|
112
|
+
spawnProcess,
|
|
113
|
+
pid: 1,
|
|
114
|
+
cooldownMs: 60_000
|
|
115
|
+
});
|
|
116
|
+
assert.equal(reader.maybeRead("first"), true);
|
|
117
|
+
await waitFor(() => lines.length > 0);
|
|
118
|
+
assert.equal(reader.maybeRead("second, too soon"), false);
|
|
119
|
+
assert.equal(calls.length, 1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a missing gdb is reported, not thrown", async () => {
|
|
123
|
+
const lines = [];
|
|
124
|
+
const spawnProcess = () => {
|
|
125
|
+
throw new Error("spawn gdb ENOENT");
|
|
126
|
+
};
|
|
127
|
+
const reader = createUsrsctpStateReader({ log: (message) => lines.push(message), spawnProcess, pid: 1 });
|
|
128
|
+
reader.maybeRead("no gdb on this host");
|
|
129
|
+
await waitFor(() => lines.length > 0);
|
|
130
|
+
assert.match(lines[0], /could not start gdb/);
|
|
131
|
+
});
|