@torrent-tv/proxy 2.89.0 → 2.89.2
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 +6 -1
- package/CHANGELOG.md +15 -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 +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +42 -8
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -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 +380 -380
- 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/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/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 +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -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 +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/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/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- 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-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +476 -458
- package/services/encode/segment-formats/index.js +103 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +604 -541
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +790 -786
- 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 +1024 -1020
- 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/segment-store/SegmentStore.js +1255 -1255
- 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 +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +67 -0
- 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 -0
- package/services/{server → viewer}/playback-readiness.js +635 -516
- 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 -70
- 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/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 +146 -162
- 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/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/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -112
- 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/media-time.test.js +62 -0
- 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 +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 +518 -483
- package/test/playback-start-regressions.test.js +101 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -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 +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-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 +105 -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 +209 -209
- 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 +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -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 +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/whole-file-held-read.test.js +30 -0
- 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,271 +1,271 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The transport both sides of the torrent worker share.
|
|
3
|
-
*
|
|
4
|
-
* One place holds the request/reply bookkeeping and the streaming rules, so the
|
|
5
|
-
* worker and its main-thread client cannot drift apart on the details that
|
|
6
|
-
* matter: which side transfers, which side acknowledges, and when a stream is
|
|
7
|
-
* allowed to keep going.
|
|
8
|
-
*
|
|
9
|
-
* See `protocol.js` for why the design is what it is — every number in it came
|
|
10
|
-
* from a measurement, not a preference.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { Event, STREAM_HIGH_WATER_CHUNKS } from "./protocol.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Issue request ids that stay unique for the life of a thread.
|
|
17
|
-
*
|
|
18
|
-
* @returns {() => number}
|
|
19
|
-
*/
|
|
20
|
-
export function createRequestIds() {
|
|
21
|
-
let next = 0;
|
|
22
|
-
return () => (next += 1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Wrap a port's messages into request/reply calls.
|
|
27
|
-
*
|
|
28
|
-
* Callers get promises; the plumbing of matching replies to requests, and of
|
|
29
|
-
* turning a worker-side failure back into a rejection, lives here. `Error`
|
|
30
|
-
* objects do not survive a thread boundary, so failures cross as messages and
|
|
31
|
-
* are rebuilt into `Error`s on arrival — a caller sees an ordinary rejection.
|
|
32
|
-
*
|
|
33
|
-
* @param {import("node:worker_threads").MessagePort | import("node:worker_threads").Worker} port
|
|
34
|
-
* @returns {{
|
|
35
|
-
* call: (command: string, params?: object) => Promise<unknown>,
|
|
36
|
-
* handleReply: (message: object) => boolean,
|
|
37
|
-
* rejectAll: (reason: Error) => void
|
|
38
|
-
* }}
|
|
39
|
-
*/
|
|
40
|
-
export function createCaller(port) {
|
|
41
|
-
const nextId = createRequestIds();
|
|
42
|
-
const pending = new Map();
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
/**
|
|
46
|
-
* The one source of request ids on this side of the channel.
|
|
47
|
-
*
|
|
48
|
-
* Reads used to number themselves from a second counter, which put two
|
|
49
|
-
* unrelated sequences in one namespace: a read and a command could both be
|
|
50
|
-
* in flight as id 5, and the worker's reply to the read then resolved the
|
|
51
|
-
* command — with the read's result, silently, while the command's real
|
|
52
|
-
* answer arrived later and was dropped as unknown. Handing out every id
|
|
53
|
-
* from here makes that collision impossible rather than unlikely.
|
|
54
|
-
*/
|
|
55
|
-
nextId,
|
|
56
|
-
|
|
57
|
-
call(command, params = {}) {
|
|
58
|
-
return new Promise((resolve, reject) => {
|
|
59
|
-
const id = nextId();
|
|
60
|
-
pending.set(id, { resolve, reject });
|
|
61
|
-
port.postMessage({ command, id, params });
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Feed a message in; returns true when it was a reply this caller owned.
|
|
67
|
-
*/
|
|
68
|
-
handleReply(message) {
|
|
69
|
-
const entry = pending.get(message?.id);
|
|
70
|
-
if (!entry) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
if (message.type === Event.RESULT) {
|
|
74
|
-
pending.delete(message.id);
|
|
75
|
-
entry.resolve(message.result);
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
if (message.type === Event.ERROR) {
|
|
79
|
-
pending.delete(message.id);
|
|
80
|
-
entry.reject(new Error(message.error ?? "Torrent worker request failed."));
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Fail everything outstanding — the worker died, or is being shut down.
|
|
88
|
-
*/
|
|
89
|
-
rejectAll(reason) {
|
|
90
|
-
for (const [, entry] of pending) {
|
|
91
|
-
entry.reject(reason);
|
|
92
|
-
}
|
|
93
|
-
pending.clear();
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Receive a chunked body as an ordinary `ReadableStream`.
|
|
100
|
-
*
|
|
101
|
-
* This is the half that makes the design pay off: chunks arrive as transferred
|
|
102
|
-
* buffers (no copying), and are handed on through a standard stream, so callers
|
|
103
|
-
* treat it exactly like any other body. Each chunk is acknowledged as it is
|
|
104
|
-
* enqueued, which is what lets the worker keep only
|
|
105
|
-
* {@link STREAM_HIGH_WATER_CHUNKS} in flight.
|
|
106
|
-
*
|
|
107
|
-
* Cancelling the stream — a viewer navigating away, a superseded seek — sends
|
|
108
|
-
* the cancel command, so the worker stops reading rather than filling a queue
|
|
109
|
-
* nobody will drain.
|
|
110
|
-
*
|
|
111
|
-
* @param {object} params
|
|
112
|
-
* @param {import("node:worker_threads").Worker} params.port
|
|
113
|
-
* @param {number} params.requestId
|
|
114
|
-
* @param {() => void} params.onCancel - Sends CANCEL_READ for this request.
|
|
115
|
-
* @returns {{ stream: ReadableStream<Uint8Array>, push: (bytes: Uint8Array) => void, close: () => void, fail: (error: Error) => void }}
|
|
116
|
-
*/
|
|
117
|
-
export function createReceiveStream({ port, requestId, onCancel }) {
|
|
118
|
-
let controller = null;
|
|
119
|
-
let finished = false;
|
|
120
|
-
|
|
121
|
-
const stream = new ReadableStream({
|
|
122
|
-
start(streamController) {
|
|
123
|
-
controller = streamController;
|
|
124
|
-
},
|
|
125
|
-
cancel() {
|
|
126
|
-
if (!finished) {
|
|
127
|
-
finished = true;
|
|
128
|
-
onCancel();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
stream,
|
|
135
|
-
|
|
136
|
-
push(bytes) {
|
|
137
|
-
if (finished || !controller) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
controller.enqueue(bytes);
|
|
141
|
-
// Acknowledge only once the data is in the stream's own queue, so the
|
|
142
|
-
// worker's in-flight count reflects what has actually been taken up.
|
|
143
|
-
port.postMessage({ type: Event.CHUNK_ACK, id: requestId });
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
close() {
|
|
147
|
-
if (finished || !controller) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
finished = true;
|
|
151
|
-
controller.close();
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
fail(error) {
|
|
155
|
-
if (finished || !controller) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
finished = true;
|
|
159
|
-
controller.error(error);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Send a body as chunks, pausing when too many are unacknowledged.
|
|
166
|
-
*
|
|
167
|
-
* The worker side of the same arrangement. `waitForCapacity` resolves when the
|
|
168
|
-
* main thread has taken up enough of what was sent; without it a fast disk
|
|
169
|
-
* would outrun the channel and rebuild in the message queue exactly the memory
|
|
170
|
-
* the transfers were saving.
|
|
171
|
-
*
|
|
172
|
-
* @param {object} params
|
|
173
|
-
* @param {import("node:worker_threads").MessagePort} params.port
|
|
174
|
-
* @param {number} params.requestId
|
|
175
|
-
* @returns {{ send: (bytes: Buffer) => Promise<void>, end: () => void, ack: () => void, cancel: () => void, isCancelled: () => boolean, onCancel: (listener: () => void) => () => void }}
|
|
176
|
-
*/
|
|
177
|
-
export function createSendStream({ port, requestId }) {
|
|
178
|
-
let inFlight = 0;
|
|
179
|
-
let cancelled = false;
|
|
180
|
-
let wake = null;
|
|
181
|
-
/** Told the moment the read is cancelled. @type {Set<() => void>} */
|
|
182
|
-
const cancelListeners = new Set();
|
|
183
|
-
|
|
184
|
-
const waitForCapacity = () => {
|
|
185
|
-
if (cancelled || inFlight < STREAM_HIGH_WATER_CHUNKS) {
|
|
186
|
-
return Promise.resolve();
|
|
187
|
-
}
|
|
188
|
-
return new Promise((resolve) => {
|
|
189
|
-
wake = resolve;
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
async send(bytes) {
|
|
195
|
-
await waitForCapacity();
|
|
196
|
-
if (cancelled) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
inFlight += 1;
|
|
200
|
-
// Copy into memory this transport allocated, then transfer THAT.
|
|
201
|
-
//
|
|
202
|
-
// Transferring the caller's buffer is faster and was what shipped, but it
|
|
203
|
-
// is only correct if the caller owns the memory outright — and no test at
|
|
204
|
-
// this boundary can establish that. 2.9.73 tried to decide it by
|
|
205
|
-
// inspection (`byteOffset === 0 && byteLength === buffer.byteLength`),
|
|
206
|
-
// which answers "does this view cover its region", not "did we allocate
|
|
207
|
-
// it". WebTorrent's piece cache returns a buffer covering its whole
|
|
208
|
-
// region and keeps using it, so the check passed and the transfer
|
|
209
|
-
// detached the cache: every later read failed with a detached
|
|
210
|
-
// ArrayBuffer, and because the error never reached the reader it looked
|
|
211
|
-
// like an empty file (`Stream ends prematurely at 0`).
|
|
212
|
-
//
|
|
213
|
-
// The copy costs 3.64 ms per 8 MB on the field host, against 37 ms for a
|
|
214
|
-
// structured clone. It disappears entirely for pieces read through the
|
|
215
|
-
// shared piece store, which the main thread reads by offset without any
|
|
216
|
-
// hand-over at all.
|
|
217
|
-
const payload = new Uint8Array(bytes);
|
|
218
|
-
port.postMessage(
|
|
219
|
-
{ type: Event.CHUNK, id: requestId, bytes: payload },
|
|
220
|
-
[payload.buffer]
|
|
221
|
-
);
|
|
222
|
-
},
|
|
223
|
-
|
|
224
|
-
end() {
|
|
225
|
-
if (!cancelled) {
|
|
226
|
-
port.postMessage({ type: Event.READ_END, id: requestId });
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
ack() {
|
|
231
|
-
inFlight = Math.max(0, inFlight - 1);
|
|
232
|
-
if (wake) {
|
|
233
|
-
const resume = wake;
|
|
234
|
-
wake = null;
|
|
235
|
-
resume();
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
cancel() {
|
|
240
|
-
cancelled = true;
|
|
241
|
-
if (wake) {
|
|
242
|
-
const resume = wake;
|
|
243
|
-
wake = null;
|
|
244
|
-
resume();
|
|
245
|
-
}
|
|
246
|
-
for (const listener of [...cancelListeners]) {
|
|
247
|
-
listener();
|
|
248
|
-
}
|
|
249
|
-
cancelListeners.clear();
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
isCancelled() {
|
|
253
|
-
return cancelled;
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Be told when this read is cancelled — at once if it already has been.
|
|
258
|
-
*
|
|
259
|
-
* @param {() => void} listener
|
|
260
|
-
* @returns {() => void} Stop being told.
|
|
261
|
-
*/
|
|
262
|
-
onCancel(listener) {
|
|
263
|
-
if (cancelled) {
|
|
264
|
-
listener();
|
|
265
|
-
return () => {};
|
|
266
|
-
}
|
|
267
|
-
cancelListeners.add(listener);
|
|
268
|
-
return () => cancelListeners.delete(listener);
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file The transport both sides of the torrent worker share.
|
|
3
|
+
*
|
|
4
|
+
* One place holds the request/reply bookkeeping and the streaming rules, so the
|
|
5
|
+
* worker and its main-thread client cannot drift apart on the details that
|
|
6
|
+
* matter: which side transfers, which side acknowledges, and when a stream is
|
|
7
|
+
* allowed to keep going.
|
|
8
|
+
*
|
|
9
|
+
* See `protocol.js` for why the design is what it is — every number in it came
|
|
10
|
+
* from a measurement, not a preference.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Event, STREAM_HIGH_WATER_CHUNKS } from "./protocol.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Issue request ids that stay unique for the life of a thread.
|
|
17
|
+
*
|
|
18
|
+
* @returns {() => number}
|
|
19
|
+
*/
|
|
20
|
+
export function createRequestIds() {
|
|
21
|
+
let next = 0;
|
|
22
|
+
return () => (next += 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wrap a port's messages into request/reply calls.
|
|
27
|
+
*
|
|
28
|
+
* Callers get promises; the plumbing of matching replies to requests, and of
|
|
29
|
+
* turning a worker-side failure back into a rejection, lives here. `Error`
|
|
30
|
+
* objects do not survive a thread boundary, so failures cross as messages and
|
|
31
|
+
* are rebuilt into `Error`s on arrival — a caller sees an ordinary rejection.
|
|
32
|
+
*
|
|
33
|
+
* @param {import("node:worker_threads").MessagePort | import("node:worker_threads").Worker} port
|
|
34
|
+
* @returns {{
|
|
35
|
+
* call: (command: string, params?: object) => Promise<unknown>,
|
|
36
|
+
* handleReply: (message: object) => boolean,
|
|
37
|
+
* rejectAll: (reason: Error) => void
|
|
38
|
+
* }}
|
|
39
|
+
*/
|
|
40
|
+
export function createCaller(port) {
|
|
41
|
+
const nextId = createRequestIds();
|
|
42
|
+
const pending = new Map();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
/**
|
|
46
|
+
* The one source of request ids on this side of the channel.
|
|
47
|
+
*
|
|
48
|
+
* Reads used to number themselves from a second counter, which put two
|
|
49
|
+
* unrelated sequences in one namespace: a read and a command could both be
|
|
50
|
+
* in flight as id 5, and the worker's reply to the read then resolved the
|
|
51
|
+
* command — with the read's result, silently, while the command's real
|
|
52
|
+
* answer arrived later and was dropped as unknown. Handing out every id
|
|
53
|
+
* from here makes that collision impossible rather than unlikely.
|
|
54
|
+
*/
|
|
55
|
+
nextId,
|
|
56
|
+
|
|
57
|
+
call(command, params = {}) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const id = nextId();
|
|
60
|
+
pending.set(id, { resolve, reject });
|
|
61
|
+
port.postMessage({ command, id, params });
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Feed a message in; returns true when it was a reply this caller owned.
|
|
67
|
+
*/
|
|
68
|
+
handleReply(message) {
|
|
69
|
+
const entry = pending.get(message?.id);
|
|
70
|
+
if (!entry) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (message.type === Event.RESULT) {
|
|
74
|
+
pending.delete(message.id);
|
|
75
|
+
entry.resolve(message.result);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (message.type === Event.ERROR) {
|
|
79
|
+
pending.delete(message.id);
|
|
80
|
+
entry.reject(new Error(message.error ?? "Torrent worker request failed."));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fail everything outstanding — the worker died, or is being shut down.
|
|
88
|
+
*/
|
|
89
|
+
rejectAll(reason) {
|
|
90
|
+
for (const [, entry] of pending) {
|
|
91
|
+
entry.reject(reason);
|
|
92
|
+
}
|
|
93
|
+
pending.clear();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Receive a chunked body as an ordinary `ReadableStream`.
|
|
100
|
+
*
|
|
101
|
+
* This is the half that makes the design pay off: chunks arrive as transferred
|
|
102
|
+
* buffers (no copying), and are handed on through a standard stream, so callers
|
|
103
|
+
* treat it exactly like any other body. Each chunk is acknowledged as it is
|
|
104
|
+
* enqueued, which is what lets the worker keep only
|
|
105
|
+
* {@link STREAM_HIGH_WATER_CHUNKS} in flight.
|
|
106
|
+
*
|
|
107
|
+
* Cancelling the stream — a viewer navigating away, a superseded seek — sends
|
|
108
|
+
* the cancel command, so the worker stops reading rather than filling a queue
|
|
109
|
+
* nobody will drain.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} params
|
|
112
|
+
* @param {import("node:worker_threads").Worker} params.port
|
|
113
|
+
* @param {number} params.requestId
|
|
114
|
+
* @param {() => void} params.onCancel - Sends CANCEL_READ for this request.
|
|
115
|
+
* @returns {{ stream: ReadableStream<Uint8Array>, push: (bytes: Uint8Array) => void, close: () => void, fail: (error: Error) => void }}
|
|
116
|
+
*/
|
|
117
|
+
export function createReceiveStream({ port, requestId, onCancel }) {
|
|
118
|
+
let controller = null;
|
|
119
|
+
let finished = false;
|
|
120
|
+
|
|
121
|
+
const stream = new ReadableStream({
|
|
122
|
+
start(streamController) {
|
|
123
|
+
controller = streamController;
|
|
124
|
+
},
|
|
125
|
+
cancel() {
|
|
126
|
+
if (!finished) {
|
|
127
|
+
finished = true;
|
|
128
|
+
onCancel();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
stream,
|
|
135
|
+
|
|
136
|
+
push(bytes) {
|
|
137
|
+
if (finished || !controller) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
controller.enqueue(bytes);
|
|
141
|
+
// Acknowledge only once the data is in the stream's own queue, so the
|
|
142
|
+
// worker's in-flight count reflects what has actually been taken up.
|
|
143
|
+
port.postMessage({ type: Event.CHUNK_ACK, id: requestId });
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
close() {
|
|
147
|
+
if (finished || !controller) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
finished = true;
|
|
151
|
+
controller.close();
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
fail(error) {
|
|
155
|
+
if (finished || !controller) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
finished = true;
|
|
159
|
+
controller.error(error);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Send a body as chunks, pausing when too many are unacknowledged.
|
|
166
|
+
*
|
|
167
|
+
* The worker side of the same arrangement. `waitForCapacity` resolves when the
|
|
168
|
+
* main thread has taken up enough of what was sent; without it a fast disk
|
|
169
|
+
* would outrun the channel and rebuild in the message queue exactly the memory
|
|
170
|
+
* the transfers were saving.
|
|
171
|
+
*
|
|
172
|
+
* @param {object} params
|
|
173
|
+
* @param {import("node:worker_threads").MessagePort} params.port
|
|
174
|
+
* @param {number} params.requestId
|
|
175
|
+
* @returns {{ send: (bytes: Buffer) => Promise<void>, end: () => void, ack: () => void, cancel: () => void, isCancelled: () => boolean, onCancel: (listener: () => void) => () => void }}
|
|
176
|
+
*/
|
|
177
|
+
export function createSendStream({ port, requestId }) {
|
|
178
|
+
let inFlight = 0;
|
|
179
|
+
let cancelled = false;
|
|
180
|
+
let wake = null;
|
|
181
|
+
/** Told the moment the read is cancelled. @type {Set<() => void>} */
|
|
182
|
+
const cancelListeners = new Set();
|
|
183
|
+
|
|
184
|
+
const waitForCapacity = () => {
|
|
185
|
+
if (cancelled || inFlight < STREAM_HIGH_WATER_CHUNKS) {
|
|
186
|
+
return Promise.resolve();
|
|
187
|
+
}
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
wake = resolve;
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
async send(bytes) {
|
|
195
|
+
await waitForCapacity();
|
|
196
|
+
if (cancelled) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
inFlight += 1;
|
|
200
|
+
// Copy into memory this transport allocated, then transfer THAT.
|
|
201
|
+
//
|
|
202
|
+
// Transferring the caller's buffer is faster and was what shipped, but it
|
|
203
|
+
// is only correct if the caller owns the memory outright — and no test at
|
|
204
|
+
// this boundary can establish that. 2.9.73 tried to decide it by
|
|
205
|
+
// inspection (`byteOffset === 0 && byteLength === buffer.byteLength`),
|
|
206
|
+
// which answers "does this view cover its region", not "did we allocate
|
|
207
|
+
// it". WebTorrent's piece cache returns a buffer covering its whole
|
|
208
|
+
// region and keeps using it, so the check passed and the transfer
|
|
209
|
+
// detached the cache: every later read failed with a detached
|
|
210
|
+
// ArrayBuffer, and because the error never reached the reader it looked
|
|
211
|
+
// like an empty file (`Stream ends prematurely at 0`).
|
|
212
|
+
//
|
|
213
|
+
// The copy costs 3.64 ms per 8 MB on the field host, against 37 ms for a
|
|
214
|
+
// structured clone. It disappears entirely for pieces read through the
|
|
215
|
+
// shared piece store, which the main thread reads by offset without any
|
|
216
|
+
// hand-over at all.
|
|
217
|
+
const payload = new Uint8Array(bytes);
|
|
218
|
+
port.postMessage(
|
|
219
|
+
{ type: Event.CHUNK, id: requestId, bytes: payload },
|
|
220
|
+
[payload.buffer]
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
end() {
|
|
225
|
+
if (!cancelled) {
|
|
226
|
+
port.postMessage({ type: Event.READ_END, id: requestId });
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
ack() {
|
|
231
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
232
|
+
if (wake) {
|
|
233
|
+
const resume = wake;
|
|
234
|
+
wake = null;
|
|
235
|
+
resume();
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
cancel() {
|
|
240
|
+
cancelled = true;
|
|
241
|
+
if (wake) {
|
|
242
|
+
const resume = wake;
|
|
243
|
+
wake = null;
|
|
244
|
+
resume();
|
|
245
|
+
}
|
|
246
|
+
for (const listener of [...cancelListeners]) {
|
|
247
|
+
listener();
|
|
248
|
+
}
|
|
249
|
+
cancelListeners.clear();
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
isCancelled() {
|
|
253
|
+
return cancelled;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Be told when this read is cancelled — at once if it already has been.
|
|
258
|
+
*
|
|
259
|
+
* @param {() => void} listener
|
|
260
|
+
* @returns {() => void} Stop being told.
|
|
261
|
+
*/
|
|
262
|
+
onCancel(listener) {
|
|
263
|
+
if (cancelled) {
|
|
264
|
+
listener();
|
|
265
|
+
return () => {};
|
|
266
|
+
}
|
|
267
|
+
cancelListeners.add(listener);
|
|
268
|
+
return () => cancelListeners.delete(listener);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { Worker } from "node:worker_threads";
|
|
18
18
|
import { Readable } from "node:stream";
|
|
19
|
+
import { open } from "node:fs/promises";
|
|
19
20
|
import { fileURLToPath } from "node:url";
|
|
20
21
|
import { logger, writeAlreadyDecided } from "../../../utils/logger.js";
|
|
21
22
|
import { createCaller, createReceiveStream } from "./channel.js";
|
|
@@ -362,6 +363,10 @@ export class TorrentWorkerClient {
|
|
|
362
363
|
* @returns {Promise<Array<[number, number]>>}
|
|
363
364
|
*/
|
|
364
365
|
async heldRanges({ sourceKey, fileIndex }) {
|
|
366
|
+
const whole = this.#wholeFileOf(sourceKey, fileIndex);
|
|
367
|
+
if (whole) {
|
|
368
|
+
return whole.length > 0 ? [[0, whole.length - 1]] : [];
|
|
369
|
+
}
|
|
365
370
|
const answer = await this.#caller.call(Command.HELD_RANGES, { sourceKey, fileIndex });
|
|
366
371
|
return Array.isArray(answer?.ranges) ? answer.ranges : [];
|
|
367
372
|
}
|
|
@@ -374,6 +379,21 @@ export class TorrentWorkerClient {
|
|
|
374
379
|
* @returns {Promise<Buffer | null>}
|
|
375
380
|
*/
|
|
376
381
|
async readHeld({ sourceKey, fileIndex, start, end }) {
|
|
382
|
+
const whole = this.#wholeFileOf(sourceKey, fileIndex);
|
|
383
|
+
if (whole) {
|
|
384
|
+
try {
|
|
385
|
+
return await readWholeFile(whole, start, end);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// The file is gone — removed for space, as the stream route also finds.
|
|
388
|
+
// Forgotten here too, so the torrent answers from now on.
|
|
389
|
+
const key = `${String(sourceKey).slice("torrent:".length).toLowerCase()}/${fileIndex}`;
|
|
390
|
+
if (this.wholeFiles.get(key) === whole) {
|
|
391
|
+
this.wholeFiles.delete(key);
|
|
392
|
+
}
|
|
393
|
+
logger.info(`whole files: "${whole.name}" could not be read (${error?.code ?? error?.message ?? error}); the torrent answers for it now`);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
377
397
|
const answer = await this.#caller.call(Command.READ_HELD, { sourceKey, fileIndex, start, end });
|
|
378
398
|
const bytes = answer?.bytes;
|
|
379
399
|
// The memory was handed over by the worker, so it is this thread's alone:
|
|
@@ -381,6 +401,28 @@ export class TorrentWorkerClient {
|
|
|
381
401
|
return bytes ? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) : null;
|
|
382
402
|
}
|
|
383
403
|
|
|
404
|
+
/**
|
|
405
|
+
* The file kept whole on disk for this source and index, if there is one.
|
|
406
|
+
*
|
|
407
|
+
* Once a file is kept whole its pieces leave the torrent's store, while the
|
|
408
|
+
* torrent's bitfield still says they are held: a read through the torrent
|
|
409
|
+
* then waits on a store that no longer has them. Field 2026-10-01: every read
|
|
410
|
+
* of the subtitle walk on such a file was given up after 30 s. The file
|
|
411
|
+
* itself is what holds the bytes, so it is what answers — as the stream route
|
|
412
|
+
* already does.
|
|
413
|
+
*
|
|
414
|
+
* @param {string} sourceKey
|
|
415
|
+
* @param {number} fileIndex
|
|
416
|
+
* @returns {{ path: string, length: number, name: string } | null}
|
|
417
|
+
*/
|
|
418
|
+
#wholeFileOf(sourceKey, fileIndex) {
|
|
419
|
+
if (this.wholeFiles.size === 0 || !String(sourceKey).startsWith("torrent:")) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const infoHash = String(sourceKey).slice("torrent:".length).toLowerCase();
|
|
423
|
+
return this.wholeFiles.get(`${infoHash}/${fileIndex}`) ?? null;
|
|
424
|
+
}
|
|
425
|
+
|
|
384
426
|
async warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds }) {
|
|
385
427
|
return this.#caller.call(Command.WARM_POSITION, { sourceKey, fileIndex, positionSeconds, durationSeconds });
|
|
386
428
|
}
|
|
@@ -767,3 +809,28 @@ export class TorrentWorkerClient {
|
|
|
767
809
|
});
|
|
768
810
|
}
|
|
769
811
|
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Bytes of a file kept whole, read from the disk.
|
|
815
|
+
*
|
|
816
|
+
* @param {{ path: string, length: number }} whole
|
|
817
|
+
* @param {number} start
|
|
818
|
+
* @param {number} end - Inclusive.
|
|
819
|
+
* @returns {Promise<Buffer | null>} Null where the range is not inside the
|
|
820
|
+
* file or the file is shorter than it was kept as. Rejects when the file
|
|
821
|
+
* cannot be opened.
|
|
822
|
+
*/
|
|
823
|
+
export async function readWholeFile(whole, start, end) {
|
|
824
|
+
const last = Math.min(end, whole.length - 1);
|
|
825
|
+
if (!(start >= 0) || !(last >= start)) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
const handle = await open(whole.path, "r");
|
|
829
|
+
try {
|
|
830
|
+
const bytes = Buffer.allocUnsafe(last - start + 1);
|
|
831
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, start);
|
|
832
|
+
return bytesRead === bytes.length ? bytes : null;
|
|
833
|
+
} finally {
|
|
834
|
+
await handle.close();
|
|
835
|
+
}
|
|
836
|
+
}
|