@torrent-tv/proxy 2.89.5 → 2.89.7
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 +338 -338
- package/CHANGELOG.md +16 -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/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- 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 +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- 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 +748 -748
- 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 +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- 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/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- 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 +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- 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/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/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- 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 +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- 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/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- 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 +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- 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/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- 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/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/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 +264 -264
- 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/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/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/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/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -753
- 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 +475 -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 +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- 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 +68 -68
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- 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 +240 -240
- package/test/helpers/manager.js +62 -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/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 +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- 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-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- 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 +87 -87
- 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-completeness.test.js +37 -1
- 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 +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- 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 +160 -160
- package/test/quality-variants.test.js +1270 -1257
- 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 +64 -64
- package/test/same-height-switch.test.js +663 -663
- 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-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 +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- 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 +111 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- 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 +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- 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/supply-margin.test.js +190 -190
- 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 +508 -504
- 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 +92 -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,174 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A proxy that needs nothing from a swarm is not in that swarm.
|
|
3
|
-
*
|
|
4
|
-
* WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
|
|
5
|
-
* `_addIncomingPeer` checks that the torrent is neither destroyed nor paused
|
|
6
|
-
* and registers the peer. A proxy with its port mapped is reachable, so on a
|
|
7
|
-
* popular torrent connections arrive and are never turned away — field
|
|
8
|
-
* 2026-09-11: 249 connected at the start of one viewing, 596 at the end, 15 862
|
|
9
|
-
* more queued, on a file complete for three quarters of an hour.
|
|
10
|
-
*
|
|
11
|
-
* The rule is not a limit on connections. While anybody is reading, every one is
|
|
12
|
-
* worth keeping: the one that has delivered nothing yet may deliver next.
|
|
13
|
-
*
|
|
14
|
-
* AND IT IS ASKED OF WHAT HAS BEEN STATED, never of a count of readers. The
|
|
15
|
-
* first version asked every five seconds whether anybody was READING, and a
|
|
16
|
-
* torrent added three seconds earlier answered no — its edges still being read,
|
|
17
|
-
* its plan still being built. It left the swarm with 741 connections let go,
|
|
18
|
-
* and nothing rejoined it: rejoining waited for a reader, and the reader was
|
|
19
|
-
* waiting for the header the swarm had been fetching. Playback did not start at
|
|
20
|
-
* all.
|
|
21
|
-
*
|
|
22
|
-
* The question now is whether anything is WANTED of it, which the priority map,
|
|
23
|
-
* the ends of an open file and a stopped read all answer — and which is true
|
|
24
|
-
* from the moment a torrent is opened.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import test from "node:test";
|
|
28
|
-
import assert from "node:assert/strict";
|
|
29
|
-
import {
|
|
30
|
-
isWanted,
|
|
31
|
-
leaveSwarm,
|
|
32
|
-
rejoinSwarm,
|
|
33
|
-
stateFileEdges,
|
|
34
|
-
swarmDecisionFor
|
|
35
|
-
} from "../services/torrent/torrent-pool.js";
|
|
36
|
-
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
37
|
-
import { Urgency } from "../services/torrent/demand/index.js";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* A torrent that records what was done to it. Only the surface the rule
|
|
41
|
-
* touches: `paused` is the library's own flag, and the peers are what the pause
|
|
42
|
-
* does not close by itself.
|
|
43
|
-
*
|
|
44
|
-
* @param {number} peerCount
|
|
45
|
-
* @returns {object}
|
|
46
|
-
*/
|
|
47
|
-
function torrentWith(peerCount) {
|
|
48
|
-
const peers = new Map();
|
|
49
|
-
for (let index = 0; index < peerCount; index += 1) {
|
|
50
|
-
peers.set(String(index), {
|
|
51
|
-
wire: { downloaded: 0 },
|
|
52
|
-
destroy() {
|
|
53
|
-
peers.delete(String(index));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
|
|
59
|
-
files: [{ length: 1024 }, { length: 2048 }],
|
|
60
|
-
paused: false,
|
|
61
|
-
wires: [...peers.values()].map((peer) => peer.wire),
|
|
62
|
-
_peers: peers,
|
|
63
|
-
pause() {
|
|
64
|
-
this.paused = true;
|
|
65
|
-
},
|
|
66
|
-
resume() {
|
|
67
|
-
this.paused = false;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
test("letting a swarm go pauses the torrent and closes what the pause does not", () => {
|
|
73
|
-
const torrent = torrentWith(6);
|
|
74
|
-
|
|
75
|
-
const closed = leaveSwarm(torrent);
|
|
76
|
-
|
|
77
|
-
assert.equal(torrent.paused, true, "the library's own word for this was not used");
|
|
78
|
-
assert.equal(closed, 6);
|
|
79
|
-
assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("a swarm already let go is not let go twice", () => {
|
|
83
|
-
const torrent = torrentWith(4);
|
|
84
|
-
leaveSwarm(torrent);
|
|
85
|
-
assert.equal(leaveSwarm(torrent), 0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("the reader that comes back takes the swarm with it", () => {
|
|
89
|
-
const torrent = torrentWith(3);
|
|
90
|
-
leaveSwarm(torrent);
|
|
91
|
-
|
|
92
|
-
assert.equal(rejoinSwarm(torrent), true);
|
|
93
|
-
assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
|
|
94
|
-
assert.equal(rejoinSwarm(torrent), false, "a torrent already in its swarm was resumed again");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("a torrent whose metadata has not arrived is wanted, whatever is stated", () => {
|
|
98
|
-
// THE FIELD FAILURE OF 2026-09-11, as a question rather than as a mechanism.
|
|
99
|
-
// A torrent being added has no list of files, so nothing can name a byte of
|
|
100
|
-
// it — and it is being fetched precisely because somebody asked for it.
|
|
101
|
-
const torrent = torrentWith(103);
|
|
102
|
-
torrent.files = [];
|
|
103
|
-
assert.equal(isWanted(torrent), true);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a torrent with files and nothing stated for them is wanted by nobody", () => {
|
|
107
|
-
const torrent = torrentWith(4);
|
|
108
|
-
try {
|
|
109
|
-
assert.equal(isWanted(torrent), false);
|
|
110
|
-
} finally {
|
|
111
|
-
forgetTorrent(torrent);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("anything stated makes it wanted, and the last withdrawal ends that", () => {
|
|
116
|
-
const torrent = torrentWith(4);
|
|
117
|
-
try {
|
|
118
|
-
// The ends of a file being opened, which is the first thing said about a
|
|
119
|
-
// torrent anybody has picked and the reason it stays in its swarm through
|
|
120
|
-
// the seconds when nothing else can say anything about it.
|
|
121
|
-
stateFileEdges(torrent, 0, Urgency.TAIL);
|
|
122
|
-
assert.equal(isWanted(torrent), true);
|
|
123
|
-
|
|
124
|
-
const { register } = demandFor(torrent);
|
|
125
|
-
register.withdraw("file-edges:0:head");
|
|
126
|
-
assert.equal(isWanted(torrent), true, "one end of it is still wanted");
|
|
127
|
-
register.withdraw("file-edges:0:tail");
|
|
128
|
-
assert.equal(isWanted(torrent), false);
|
|
129
|
-
} finally {
|
|
130
|
-
forgetTorrent(torrent);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("a torrent that has been destroyed is wanted by nobody", () => {
|
|
135
|
-
const torrent = torrentWith(1);
|
|
136
|
-
torrent.destroyed = true;
|
|
137
|
-
assert.equal(isWanted(torrent), false);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("a torrent something is wanted of keeps its swarm and is off the clock", () => {
|
|
141
|
-
assert.deepEqual(
|
|
142
|
-
swarmDecisionFor({ wanted: true, everWanted: true }),
|
|
143
|
-
{ swarm: "take", onTheClock: false }
|
|
144
|
-
);
|
|
145
|
-
assert.deepEqual(
|
|
146
|
-
swarmDecisionFor({ wanted: true, everWanted: false }),
|
|
147
|
-
{ swarm: "take", onTheClock: false }
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("a swarm is let go on a DEPARTURE, never on a beginning", () => {
|
|
152
|
-
// The whole of the 2.83.1 failure in one line: a torrent that has never been
|
|
153
|
-
// wanted is one being opened — its metadata has just landed, the file list is
|
|
154
|
-
// on its way to the person choosing, and nothing has had a chance to state
|
|
155
|
-
// anything about it yet. Taking its swarm away there destroyed 741
|
|
156
|
-
// connections it needed a minute later.
|
|
157
|
-
assert.deepEqual(
|
|
158
|
-
swarmDecisionFor({ wanted: false, everWanted: false }),
|
|
159
|
-
{ swarm: "leave alone", onTheClock: true }
|
|
160
|
-
);
|
|
161
|
-
assert.deepEqual(
|
|
162
|
-
swarmDecisionFor({ wanted: false, everWanted: true }),
|
|
163
|
-
{ swarm: "let go", onTheClock: true }
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("whatever happens to the swarm, an unwanted torrent is on the idle clock", () => {
|
|
168
|
-
// Including the one nobody has ever wanted: a file list fetched and never
|
|
169
|
-
// played used to be held for the life of the process, because the only thing
|
|
170
|
-
// that started the clock was a reader letting go.
|
|
171
|
-
for (const everWanted of [true, false]) {
|
|
172
|
-
assert.equal(swarmDecisionFor({ wanted: false, everWanted }).onTheClock, true);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file A proxy that needs nothing from a swarm is not in that swarm.
|
|
3
|
+
*
|
|
4
|
+
* WebTorrent enforces `maxConns` only on peers it dials (`_drain`), while
|
|
5
|
+
* `_addIncomingPeer` checks that the torrent is neither destroyed nor paused
|
|
6
|
+
* and registers the peer. A proxy with its port mapped is reachable, so on a
|
|
7
|
+
* popular torrent connections arrive and are never turned away — field
|
|
8
|
+
* 2026-09-11: 249 connected at the start of one viewing, 596 at the end, 15 862
|
|
9
|
+
* more queued, on a file complete for three quarters of an hour.
|
|
10
|
+
*
|
|
11
|
+
* The rule is not a limit on connections. While anybody is reading, every one is
|
|
12
|
+
* worth keeping: the one that has delivered nothing yet may deliver next.
|
|
13
|
+
*
|
|
14
|
+
* AND IT IS ASKED OF WHAT HAS BEEN STATED, never of a count of readers. The
|
|
15
|
+
* first version asked every five seconds whether anybody was READING, and a
|
|
16
|
+
* torrent added three seconds earlier answered no — its edges still being read,
|
|
17
|
+
* its plan still being built. It left the swarm with 741 connections let go,
|
|
18
|
+
* and nothing rejoined it: rejoining waited for a reader, and the reader was
|
|
19
|
+
* waiting for the header the swarm had been fetching. Playback did not start at
|
|
20
|
+
* all.
|
|
21
|
+
*
|
|
22
|
+
* The question now is whether anything is WANTED of it, which the priority map,
|
|
23
|
+
* the ends of an open file and a stopped read all answer — and which is true
|
|
24
|
+
* from the moment a torrent is opened.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import test from "node:test";
|
|
28
|
+
import assert from "node:assert/strict";
|
|
29
|
+
import {
|
|
30
|
+
isWanted,
|
|
31
|
+
leaveSwarm,
|
|
32
|
+
rejoinSwarm,
|
|
33
|
+
stateFileEdges,
|
|
34
|
+
swarmDecisionFor
|
|
35
|
+
} from "../services/torrent/torrent-pool.js";
|
|
36
|
+
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
37
|
+
import { Urgency } from "../services/torrent/demand/index.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A torrent that records what was done to it. Only the surface the rule
|
|
41
|
+
* touches: `paused` is the library's own flag, and the peers are what the pause
|
|
42
|
+
* does not close by itself.
|
|
43
|
+
*
|
|
44
|
+
* @param {number} peerCount
|
|
45
|
+
* @returns {object}
|
|
46
|
+
*/
|
|
47
|
+
function torrentWith(peerCount) {
|
|
48
|
+
const peers = new Map();
|
|
49
|
+
for (let index = 0; index < peerCount; index += 1) {
|
|
50
|
+
peers.set(String(index), {
|
|
51
|
+
wire: { downloaded: 0 },
|
|
52
|
+
destroy() {
|
|
53
|
+
peers.delete(String(index));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
infoHash: "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
|
|
59
|
+
files: [{ length: 1024 }, { length: 2048 }],
|
|
60
|
+
paused: false,
|
|
61
|
+
wires: [...peers.values()].map((peer) => peer.wire),
|
|
62
|
+
_peers: peers,
|
|
63
|
+
pause() {
|
|
64
|
+
this.paused = true;
|
|
65
|
+
},
|
|
66
|
+
resume() {
|
|
67
|
+
this.paused = false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("letting a swarm go pauses the torrent and closes what the pause does not", () => {
|
|
73
|
+
const torrent = torrentWith(6);
|
|
74
|
+
|
|
75
|
+
const closed = leaveSwarm(torrent);
|
|
76
|
+
|
|
77
|
+
assert.equal(torrent.paused, true, "the library's own word for this was not used");
|
|
78
|
+
assert.equal(closed, 6);
|
|
79
|
+
assert.equal(torrent._peers.size, 0, "the connections the pause does not close were not let go");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a swarm already let go is not let go twice", () => {
|
|
83
|
+
const torrent = torrentWith(4);
|
|
84
|
+
leaveSwarm(torrent);
|
|
85
|
+
assert.equal(leaveSwarm(torrent), 0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("the reader that comes back takes the swarm with it", () => {
|
|
89
|
+
const torrent = torrentWith(3);
|
|
90
|
+
leaveSwarm(torrent);
|
|
91
|
+
|
|
92
|
+
assert.equal(rejoinSwarm(torrent), true);
|
|
93
|
+
assert.equal(torrent.paused, false, "the next reader was left with a torrent that fetches nothing");
|
|
94
|
+
assert.equal(rejoinSwarm(torrent), false, "a torrent already in its swarm was resumed again");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a torrent whose metadata has not arrived is wanted, whatever is stated", () => {
|
|
98
|
+
// THE FIELD FAILURE OF 2026-09-11, as a question rather than as a mechanism.
|
|
99
|
+
// A torrent being added has no list of files, so nothing can name a byte of
|
|
100
|
+
// it — and it is being fetched precisely because somebody asked for it.
|
|
101
|
+
const torrent = torrentWith(103);
|
|
102
|
+
torrent.files = [];
|
|
103
|
+
assert.equal(isWanted(torrent), true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a torrent with files and nothing stated for them is wanted by nobody", () => {
|
|
107
|
+
const torrent = torrentWith(4);
|
|
108
|
+
try {
|
|
109
|
+
assert.equal(isWanted(torrent), false);
|
|
110
|
+
} finally {
|
|
111
|
+
forgetTorrent(torrent);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("anything stated makes it wanted, and the last withdrawal ends that", () => {
|
|
116
|
+
const torrent = torrentWith(4);
|
|
117
|
+
try {
|
|
118
|
+
// The ends of a file being opened, which is the first thing said about a
|
|
119
|
+
// torrent anybody has picked and the reason it stays in its swarm through
|
|
120
|
+
// the seconds when nothing else can say anything about it.
|
|
121
|
+
stateFileEdges(torrent, 0, Urgency.TAIL);
|
|
122
|
+
assert.equal(isWanted(torrent), true);
|
|
123
|
+
|
|
124
|
+
const { register } = demandFor(torrent);
|
|
125
|
+
register.withdraw("file-edges:0:head");
|
|
126
|
+
assert.equal(isWanted(torrent), true, "one end of it is still wanted");
|
|
127
|
+
register.withdraw("file-edges:0:tail");
|
|
128
|
+
assert.equal(isWanted(torrent), false);
|
|
129
|
+
} finally {
|
|
130
|
+
forgetTorrent(torrent);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a torrent that has been destroyed is wanted by nobody", () => {
|
|
135
|
+
const torrent = torrentWith(1);
|
|
136
|
+
torrent.destroyed = true;
|
|
137
|
+
assert.equal(isWanted(torrent), false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a torrent something is wanted of keeps its swarm and is off the clock", () => {
|
|
141
|
+
assert.deepEqual(
|
|
142
|
+
swarmDecisionFor({ wanted: true, everWanted: true }),
|
|
143
|
+
{ swarm: "take", onTheClock: false }
|
|
144
|
+
);
|
|
145
|
+
assert.deepEqual(
|
|
146
|
+
swarmDecisionFor({ wanted: true, everWanted: false }),
|
|
147
|
+
{ swarm: "take", onTheClock: false }
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a swarm is let go on a DEPARTURE, never on a beginning", () => {
|
|
152
|
+
// The whole of the 2.83.1 failure in one line: a torrent that has never been
|
|
153
|
+
// wanted is one being opened — its metadata has just landed, the file list is
|
|
154
|
+
// on its way to the person choosing, and nothing has had a chance to state
|
|
155
|
+
// anything about it yet. Taking its swarm away there destroyed 741
|
|
156
|
+
// connections it needed a minute later.
|
|
157
|
+
assert.deepEqual(
|
|
158
|
+
swarmDecisionFor({ wanted: false, everWanted: false }),
|
|
159
|
+
{ swarm: "leave alone", onTheClock: true }
|
|
160
|
+
);
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
swarmDecisionFor({ wanted: false, everWanted: true }),
|
|
163
|
+
{ swarm: "let go", onTheClock: true }
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("whatever happens to the swarm, an unwanted torrent is on the idle clock", () => {
|
|
168
|
+
// Including the one nobody has ever wanted: a file list fetched and never
|
|
169
|
+
// played used to be held for the life of the process, because the only thing
|
|
170
|
+
// that started the clock was a reader letting go.
|
|
171
|
+
for (const everWanted of [true, false]) {
|
|
172
|
+
assert.equal(swarmDecisionFor({ wanted: false, everWanted }).onTheClock, true);
|
|
173
|
+
}
|
|
174
|
+
});
|
package/test/swarm-reach.test.js
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The line describing a swarm answers the question it is asked.
|
|
3
|
-
*
|
|
4
|
-
* It could not. `routes/api/sources/stats/get.js` printed `peers=N` beside
|
|
5
|
-
* `wires=?`, which reads as two quantities of which one is unknown — while
|
|
6
|
-
* WebTorrent's `numPeers` IS `wires.length` (`lib/torrent.js:254`, the same in
|
|
7
|
-
* 2.8.5 and 3.0.21), so the first was the connection count and the second was a
|
|
8
|
-
* field that has never printed anything, because the torrent lives on a worker
|
|
9
|
-
* thread where that property does not exist.
|
|
10
|
-
*
|
|
11
|
-
* What was missing is the other half: how many peers the client HOLDS but is
|
|
12
|
-
* not connected to, and what the tracker said the swarm has. Measured
|
|
13
|
-
* 2026-08-21 on `JUFD665.mp4`, the tracker answered `seeders=5` at 13:40:30 and
|
|
14
|
-
* the first wire arrived at 13:44:47 — 4 min 17 s in which the stats line
|
|
15
|
-
* repeated unchanged every two seconds while the answer ("offered, not
|
|
16
|
-
* connected") was already in the process.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
|
|
22
|
-
import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
|
|
23
|
-
|
|
24
|
-
test("connected and known are separate numbers", () => {
|
|
25
|
-
const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
|
|
26
|
-
assert.deepEqual(describeSwarmReach(torrent), {
|
|
27
|
-
connectedPeers: 2,
|
|
28
|
-
deliveringPeers: 0,
|
|
29
|
-
knownPeers: 7,
|
|
30
|
-
queuedPeers: 4
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("offered but not connected is distinguishable from nobody offered", () => {
|
|
35
|
-
// The two shapes the field produced. They need opposite investigations, and
|
|
36
|
-
// one line has to tell them apart.
|
|
37
|
-
const offeredNotConnected = describeSwarmReach({ wires: [], _peersLength: 5, _numQueued: 5 });
|
|
38
|
-
const nobodyOffered = describeSwarmReach({ wires: [], _peersLength: 0, _numQueued: 0 });
|
|
39
|
-
assert.equal(offeredNotConnected.connectedPeers, 0);
|
|
40
|
-
assert.equal(nobodyOffered.connectedPeers, 0);
|
|
41
|
-
assert.notEqual(offeredNotConnected.knownPeers, nobodyOffered.knownPeers);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("internals that are gone say nothing rather than breaking the poll", () => {
|
|
45
|
-
// `_peersLength` and `_numQueued` are not WebTorrent's published interface.
|
|
46
|
-
// The browser polls this every two seconds, so a version that drops them must
|
|
47
|
-
// cost the field and not the answer.
|
|
48
|
-
assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
|
|
49
|
-
connectedPeers: 1,
|
|
50
|
-
deliveringPeers: 0,
|
|
51
|
-
knownPeers: null,
|
|
52
|
-
queuedPeers: null
|
|
53
|
-
});
|
|
54
|
-
assert.deepEqual(describeSwarmReach({}), {
|
|
55
|
-
connectedPeers: 0,
|
|
56
|
-
deliveringPeers: 0,
|
|
57
|
-
knownPeers: null,
|
|
58
|
-
queuedPeers: null
|
|
59
|
-
});
|
|
60
|
-
assert.deepEqual(describeSwarmReach(null), {
|
|
61
|
-
connectedPeers: 0,
|
|
62
|
-
deliveringPeers: 0,
|
|
63
|
-
knownPeers: null,
|
|
64
|
-
queuedPeers: null
|
|
65
|
-
});
|
|
66
|
-
const throwing = {
|
|
67
|
-
wires: [],
|
|
68
|
-
get _peersLength() {
|
|
69
|
-
throw new Error("destroyed");
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
assert.deepEqual(describeSwarmReach(throwing), {
|
|
73
|
-
connectedPeers: 0,
|
|
74
|
-
deliveringPeers: 0,
|
|
75
|
-
knownPeers: null,
|
|
76
|
-
queuedPeers: null
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("the wait for a first peer is a measured quantity", () => {
|
|
81
|
-
// The field case: added 13:40:30.357, first wire 13:44:47.123.
|
|
82
|
-
assert.equal(secondsToFirstPeer(1000, 258_766), 257.766);
|
|
83
|
-
assert.equal(secondsToFirstPeer(1000, 1000), 0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("no peer yet is not a duration", () => {
|
|
87
|
-
assert.equal(secondsToFirstPeer(1000, null), null);
|
|
88
|
-
assert.equal(secondsToFirstPeer(null, 2000), null);
|
|
89
|
-
// A clock that went backwards is not a negative wait; it is no reading.
|
|
90
|
-
assert.equal(secondsToFirstPeer(2000, 1000), null);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("the best tracker answer wins, not the most recent", () => {
|
|
94
|
-
// A live tracker says 500, a dead one answers 0 two seconds later. Keeping
|
|
95
|
-
// the last would print "nobody offered" about a swarm of five hundred, which
|
|
96
|
-
// inverts the one distinction this figure is carried for.
|
|
97
|
-
const answers = [
|
|
98
|
-
{ seeders: 500, leechers: 40 },
|
|
99
|
-
{ seeders: 0, leechers: 0 }
|
|
100
|
-
];
|
|
101
|
-
assert.deepEqual(bestAnnounce(answers), { seeders: 500, leechers: 40, trackers: 2 });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("trackers that answered are counted even when none knew anything", () => {
|
|
105
|
-
assert.deepEqual(bestAnnounce([{ seeders: null, leechers: null }]), {
|
|
106
|
-
seeders: null,
|
|
107
|
-
leechers: null,
|
|
108
|
-
trackers: 1
|
|
109
|
-
});
|
|
110
|
-
// No tracker has answered at all — a different state from "answered, knows
|
|
111
|
-
// nobody", and the line says so.
|
|
112
|
-
assert.deepEqual(bestAnnounce([]), { seeders: null, leechers: null, trackers: 0 });
|
|
113
|
-
assert.deepEqual(bestAnnounce(null), { seeders: null, leechers: null, trackers: 0 });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("leechers travel with the seeder count they were reported beside", () => {
|
|
117
|
-
const answers = [
|
|
118
|
-
{ seeders: 2, leechers: 99 },
|
|
119
|
-
{ seeders: 7, leechers: 3 }
|
|
120
|
-
];
|
|
121
|
-
assert.equal(bestAnnounce(answers).leechers, 3);
|
|
122
|
-
// Including when the winning answer gave no leecher count: carrying the
|
|
123
|
-
// previous tracker's figure forward would present two trackers' numbers as
|
|
124
|
-
// one reading.
|
|
125
|
-
assert.deepEqual(bestAnnounce([{ seeders: 2, leechers: 99 }, { seeders: 7, leechers: null }]), {
|
|
126
|
-
seeders: 7,
|
|
127
|
-
leechers: null,
|
|
128
|
-
trackers: 2
|
|
129
|
-
});
|
|
130
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The line describing a swarm answers the question it is asked.
|
|
3
|
+
*
|
|
4
|
+
* It could not. `routes/api/sources/stats/get.js` printed `peers=N` beside
|
|
5
|
+
* `wires=?`, which reads as two quantities of which one is unknown — while
|
|
6
|
+
* WebTorrent's `numPeers` IS `wires.length` (`lib/torrent.js:254`, the same in
|
|
7
|
+
* 2.8.5 and 3.0.21), so the first was the connection count and the second was a
|
|
8
|
+
* field that has never printed anything, because the torrent lives on a worker
|
|
9
|
+
* thread where that property does not exist.
|
|
10
|
+
*
|
|
11
|
+
* What was missing is the other half: how many peers the client HOLDS but is
|
|
12
|
+
* not connected to, and what the tracker said the swarm has. Measured
|
|
13
|
+
* 2026-08-21 on `JUFD665.mp4`, the tracker answered `seeders=5` at 13:40:30 and
|
|
14
|
+
* the first wire arrived at 13:44:47 — 4 min 17 s in which the stats line
|
|
15
|
+
* repeated unchanged every two seconds while the answer ("offered, not
|
|
16
|
+
* connected") was already in the process.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
|
|
22
|
+
import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
|
|
23
|
+
|
|
24
|
+
test("connected and known are separate numbers", () => {
|
|
25
|
+
const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
|
|
26
|
+
assert.deepEqual(describeSwarmReach(torrent), {
|
|
27
|
+
connectedPeers: 2,
|
|
28
|
+
deliveringPeers: 0,
|
|
29
|
+
knownPeers: 7,
|
|
30
|
+
queuedPeers: 4
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("offered but not connected is distinguishable from nobody offered", () => {
|
|
35
|
+
// The two shapes the field produced. They need opposite investigations, and
|
|
36
|
+
// one line has to tell them apart.
|
|
37
|
+
const offeredNotConnected = describeSwarmReach({ wires: [], _peersLength: 5, _numQueued: 5 });
|
|
38
|
+
const nobodyOffered = describeSwarmReach({ wires: [], _peersLength: 0, _numQueued: 0 });
|
|
39
|
+
assert.equal(offeredNotConnected.connectedPeers, 0);
|
|
40
|
+
assert.equal(nobodyOffered.connectedPeers, 0);
|
|
41
|
+
assert.notEqual(offeredNotConnected.knownPeers, nobodyOffered.knownPeers);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("internals that are gone say nothing rather than breaking the poll", () => {
|
|
45
|
+
// `_peersLength` and `_numQueued` are not WebTorrent's published interface.
|
|
46
|
+
// The browser polls this every two seconds, so a version that drops them must
|
|
47
|
+
// cost the field and not the answer.
|
|
48
|
+
assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
|
|
49
|
+
connectedPeers: 1,
|
|
50
|
+
deliveringPeers: 0,
|
|
51
|
+
knownPeers: null,
|
|
52
|
+
queuedPeers: null
|
|
53
|
+
});
|
|
54
|
+
assert.deepEqual(describeSwarmReach({}), {
|
|
55
|
+
connectedPeers: 0,
|
|
56
|
+
deliveringPeers: 0,
|
|
57
|
+
knownPeers: null,
|
|
58
|
+
queuedPeers: null
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(describeSwarmReach(null), {
|
|
61
|
+
connectedPeers: 0,
|
|
62
|
+
deliveringPeers: 0,
|
|
63
|
+
knownPeers: null,
|
|
64
|
+
queuedPeers: null
|
|
65
|
+
});
|
|
66
|
+
const throwing = {
|
|
67
|
+
wires: [],
|
|
68
|
+
get _peersLength() {
|
|
69
|
+
throw new Error("destroyed");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
assert.deepEqual(describeSwarmReach(throwing), {
|
|
73
|
+
connectedPeers: 0,
|
|
74
|
+
deliveringPeers: 0,
|
|
75
|
+
knownPeers: null,
|
|
76
|
+
queuedPeers: null
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("the wait for a first peer is a measured quantity", () => {
|
|
81
|
+
// The field case: added 13:40:30.357, first wire 13:44:47.123.
|
|
82
|
+
assert.equal(secondsToFirstPeer(1000, 258_766), 257.766);
|
|
83
|
+
assert.equal(secondsToFirstPeer(1000, 1000), 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("no peer yet is not a duration", () => {
|
|
87
|
+
assert.equal(secondsToFirstPeer(1000, null), null);
|
|
88
|
+
assert.equal(secondsToFirstPeer(null, 2000), null);
|
|
89
|
+
// A clock that went backwards is not a negative wait; it is no reading.
|
|
90
|
+
assert.equal(secondsToFirstPeer(2000, 1000), null);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("the best tracker answer wins, not the most recent", () => {
|
|
94
|
+
// A live tracker says 500, a dead one answers 0 two seconds later. Keeping
|
|
95
|
+
// the last would print "nobody offered" about a swarm of five hundred, which
|
|
96
|
+
// inverts the one distinction this figure is carried for.
|
|
97
|
+
const answers = [
|
|
98
|
+
{ seeders: 500, leechers: 40 },
|
|
99
|
+
{ seeders: 0, leechers: 0 }
|
|
100
|
+
];
|
|
101
|
+
assert.deepEqual(bestAnnounce(answers), { seeders: 500, leechers: 40, trackers: 2 });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("trackers that answered are counted even when none knew anything", () => {
|
|
105
|
+
assert.deepEqual(bestAnnounce([{ seeders: null, leechers: null }]), {
|
|
106
|
+
seeders: null,
|
|
107
|
+
leechers: null,
|
|
108
|
+
trackers: 1
|
|
109
|
+
});
|
|
110
|
+
// No tracker has answered at all — a different state from "answered, knows
|
|
111
|
+
// nobody", and the line says so.
|
|
112
|
+
assert.deepEqual(bestAnnounce([]), { seeders: null, leechers: null, trackers: 0 });
|
|
113
|
+
assert.deepEqual(bestAnnounce(null), { seeders: null, leechers: null, trackers: 0 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("leechers travel with the seeder count they were reported beside", () => {
|
|
117
|
+
const answers = [
|
|
118
|
+
{ seeders: 2, leechers: 99 },
|
|
119
|
+
{ seeders: 7, leechers: 3 }
|
|
120
|
+
];
|
|
121
|
+
assert.equal(bestAnnounce(answers).leechers, 3);
|
|
122
|
+
// Including when the winning answer gave no leecher count: carrying the
|
|
123
|
+
// previous tracker's figure forward would present two trackers' numbers as
|
|
124
|
+
// one reading.
|
|
125
|
+
assert.deepEqual(bestAnnounce([{ seeders: 2, leechers: 99 }, { seeders: 7, leechers: null }]), {
|
|
126
|
+
seeders: 7,
|
|
127
|
+
leechers: null,
|
|
128
|
+
trackers: 2
|
|
129
|
+
});
|
|
130
|
+
});
|