@torrent-tv/proxy 2.85.1 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2138 -1909
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -489
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +46 -0
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -44
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -518
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -172
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9689
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -79,21 +79,26 @@ test("nothing about the machine is asked only of a software encoder", () => {
|
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("the
|
|
82
|
+
test("the calibration is given the encoder that will actually run, and software beside it", () => {
|
|
83
83
|
const server = readFileSync(path.join(HERE, "..", "server.js"), "utf8");
|
|
84
84
|
assert.match(
|
|
85
85
|
server,
|
|
86
|
-
/
|
|
87
|
-
"the
|
|
86
|
+
/calibrateEncoder\(\{[^}]*encoder: videoEncoder/,
|
|
87
|
+
"the calibration is not told which encoder to measure"
|
|
88
|
+
);
|
|
89
|
+
assert.match(
|
|
90
|
+
server,
|
|
91
|
+
/calibrateEncoder\(\{[^}]*encoder: softwareDescriptor\(\)/,
|
|
92
|
+
"software, which a failing device falls back to, is not calibrated"
|
|
88
93
|
);
|
|
89
94
|
});
|
|
90
95
|
|
|
91
96
|
test("a start and a stop are measured before any viewer, and reach the plan", () => {
|
|
92
97
|
const server = readFileSync(path.join(HERE, "..", "server.js"), "utf8");
|
|
93
98
|
assert.match(server, /await measureStartAndStop\(/, "nothing measures a start at startup");
|
|
94
|
-
assert.match(server, /\n startStopCost,/, "the reading never reaches the
|
|
99
|
+
assert.match(server, /\n startStopCost,/, "the reading never reaches the wiring of the encoding");
|
|
95
100
|
|
|
96
|
-
const manager = readFileSync(path.join(HERE, "..", "services", "
|
|
101
|
+
const manager = readFileSync(path.join(HERE, "..", "services", "server", "wire-outputs.js"), "utf8");
|
|
97
102
|
assert.match(
|
|
98
103
|
manager,
|
|
99
104
|
/noteStartupCosts\(startStopCost\)/,
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
20
|
|
|
21
|
-
import { AviContainer } from "../services/container/AviContainer.js";
|
|
22
|
-
import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
|
|
23
|
-
import { Mp4Container } from "../services/container/Mp4Container.js";
|
|
24
|
-
import { SubtitleFileContainer } from "../services/container/SubtitleFileContainer.js";
|
|
25
|
-
import { MarkupKind, TextSubtitleTrack } from "../services/tracks/TextSubtitleTrack.js";
|
|
21
|
+
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
22
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
23
|
+
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
24
|
+
import { SubtitleFileContainer } from "../services/media/container/SubtitleFileContainer.js";
|
|
25
|
+
import { MarkupKind, TextSubtitleTrack } from "../services/media/tracks/TextSubtitleTrack.js";
|
|
26
26
|
|
|
27
27
|
/** The line from the field report, as Matroska stores it. */
|
|
28
28
|
const FIELD_BLOCK =
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
2
|
+
* @file WHAT answers a pull for subtitle cues — the walk, and nothing else.
|
|
3
3
|
*
|
|
4
4
|
* Field 2026-09-03, on `[HorribleSubs] Drifters - 04 [1080p].mkv`. The file had
|
|
5
5
|
* been downloaded in an earlier sitting, so the torrent worker's cluster walk
|
|
@@ -10,25 +10,27 @@
|
|
|
10
10
|
* indexed. So the viewer watched the first 82 s with no subtitles and the rest
|
|
11
11
|
* of the episode with them.
|
|
12
12
|
*
|
|
13
|
-
* The pull
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* why nothing reported a failure.
|
|
13
|
+
* The pull was answered by something that could not read pieces. Whatever a
|
|
14
|
+
* pull is handed, the walk decides what it may read from it, so every range
|
|
15
|
+
* read as "not downloaded", nothing was walked, and an empty document came
|
|
16
|
+
* back. An empty document is also the right answer for a file that holds no
|
|
17
|
+
* cues yet, which is why nothing reported a failure.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* The thread stopped being the distinction on 2026-09-15 — the walk runs beside
|
|
20
|
+
* the sessions now, handed which byte ranges of a file are downloaded whole —
|
|
21
|
+
* but the property it was standing for did not: a pull is answered by the SAME
|
|
22
|
+
* walk that pushes, its own figures travel back, and anything that cannot read
|
|
23
|
+
* says so rather than answering emptily.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import test from "node:test";
|
|
26
27
|
import assert from "node:assert/strict";
|
|
27
28
|
|
|
28
|
-
import { SubtitleOrchestrator } from "../services/
|
|
29
|
-
import { cuesHeldFor } from "../services/
|
|
29
|
+
import { SubtitleOrchestrator } from "../services/media/SubtitleOrchestrator.js";
|
|
30
|
+
import { cuesHeldFor } from "../services/media/SubtitleCues.js";
|
|
31
|
+
import { heldFileOver } from "./helpers/held-file.js";
|
|
30
32
|
|
|
31
|
-
/**
|
|
33
|
+
/** A torrent that cannot say which of itself is downloaded. */
|
|
32
34
|
function mainThreadTorrent() {
|
|
33
35
|
return {
|
|
34
36
|
infoHash: "0".repeat(40),
|
|
@@ -38,24 +40,22 @@ function mainThreadTorrent() {
|
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
test("a pull is answered by the
|
|
43
|
+
test("a pull is answered by the walk, and the walk's own figures travel back", async () => {
|
|
42
44
|
const asked = [];
|
|
43
|
-
const
|
|
44
|
-
async
|
|
45
|
-
asked.push({ sourceKey
|
|
45
|
+
const cues = {
|
|
46
|
+
async held(torrent, fileIndex, sourceKey, trackNumber) {
|
|
47
|
+
asked.push({ sourceKey, fileIndex, trackNumber });
|
|
46
48
|
return {
|
|
47
49
|
cues: [{ startSeconds: 5.4, endSeconds: 9.1, text: "So what if you brought them over?", seq: 1 }],
|
|
48
50
|
coveredClusters: 283,
|
|
49
51
|
indexedClusters: 283,
|
|
50
|
-
codecId: "S_TEXT/ASS",
|
|
51
|
-
codecPrivate: "",
|
|
52
|
-
language: ""
|
|
52
|
+
track: { codecId: "S_TEXT/ASS", codecPrivate: "", language: "" }
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
const orchestrator = new SubtitleOrchestrator({ forget() {} });
|
|
58
|
-
const held = await orchestrator.getCues(
|
|
57
|
+
const orchestrator = new SubtitleOrchestrator({ forget() {} }, cues);
|
|
58
|
+
const held = await orchestrator.getCues(mainThreadTorrent(), 33, "a".repeat(40), 3);
|
|
59
59
|
|
|
60
60
|
assert.deepEqual(asked, [{ sourceKey: "a".repeat(40), fileIndex: 33, trackNumber: 3 }]);
|
|
61
61
|
assert.equal(held.cues.length, 1);
|
|
@@ -66,11 +66,11 @@ test("a pull is answered by the thread that owns the torrent, not walked here",
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
test("the cursor of a pulled cue is the found-order the worker assigned", async () => {
|
|
69
|
-
// The browser mixes cursors from pulls and pushes.
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
async
|
|
69
|
+
// The browser mixes cursors from pulls and pushes. A second walk would start
|
|
70
|
+
// a second `seq` counter and the two would not be comparable — which is why
|
|
71
|
+
// both ends go through the one walk, with one per-file state.
|
|
72
|
+
const cues = {
|
|
73
|
+
async held() {
|
|
74
74
|
return {
|
|
75
75
|
cues: [
|
|
76
76
|
{ startSeconds: 5.4, endSeconds: 9.1, text: "one", seq: 1 },
|
|
@@ -78,27 +78,25 @@ test("the cursor of a pulled cue is the found-order the worker assigned", async
|
|
|
78
78
|
],
|
|
79
79
|
coveredClusters: 12,
|
|
80
80
|
indexedClusters: 283,
|
|
81
|
-
codecId: "S_TEXT/ASS",
|
|
82
|
-
codecPrivate: "",
|
|
83
|
-
language: ""
|
|
81
|
+
track: { codecId: "S_TEXT/ASS", codecPrivate: "", language: "" }
|
|
84
82
|
};
|
|
85
83
|
}
|
|
86
84
|
};
|
|
87
|
-
const orchestrator = new SubtitleOrchestrator({ forget() {} });
|
|
88
|
-
const held = await orchestrator.getCues(
|
|
85
|
+
const orchestrator = new SubtitleOrchestrator({ forget() {} }, cues);
|
|
86
|
+
const held = await orchestrator.getCues(mainThreadTorrent(), 33, "a".repeat(40), 3);
|
|
89
87
|
assert.deepEqual(held.cues.map((cue) => cue.seq), [1, 31]);
|
|
90
88
|
});
|
|
91
89
|
|
|
92
|
-
test("
|
|
90
|
+
test("an orchestrator with no walk is refused, not answered emptily", async () => {
|
|
93
91
|
const orchestrator = new SubtitleOrchestrator({ forget() {} });
|
|
94
|
-
const held = await orchestrator.getCues(
|
|
92
|
+
const held = await orchestrator.getCues(mainThreadTorrent(), 33, "a".repeat(40), 3);
|
|
95
93
|
assert.deepEqual(held, { cues: [], coveredClusters: 0, indexedClusters: 0, track: null });
|
|
96
94
|
});
|
|
97
95
|
|
|
98
96
|
test("the walk refuses a torrent that cannot say which pieces it holds", async () => {
|
|
99
|
-
// Called directly
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
const held = await cuesHeldFor(mainThreadTorrent(), 33, "b".repeat(40), 3);
|
|
97
|
+
// Called directly. Without this guard the answer is an empty list
|
|
98
|
+
// indistinguishable from a file with no cues, which is what hid the defect
|
|
99
|
+
// for a whole session.
|
|
100
|
+
const held = await cuesHeldFor(heldFileOver(mainThreadTorrent(), 33, "b".repeat(40)), 3);
|
|
103
101
|
assert.deepEqual(held, { cues: [], coveredClusters: 0, indexedClusters: 0, track: null });
|
|
104
102
|
});
|
|
@@ -19,7 +19,8 @@ import test from "node:test";
|
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
20
|
import { Readable } from "node:stream";
|
|
21
21
|
|
|
22
|
-
import { cuesHeldFor, warmSubtitleCues, forgetSubtitles } from "../services/
|
|
22
|
+
import { cuesHeldFor, warmSubtitleCues, forgetSubtitles } from "../services/media/SubtitleCues.js";
|
|
23
|
+
import { heldFileOver } from "./helpers/held-file.js";
|
|
23
24
|
|
|
24
25
|
const ID_EBML = 0x1a45dfa3;
|
|
25
26
|
const ID_SEGMENT = 0x18538067;
|
|
@@ -255,7 +256,7 @@ test("a walk of a downloaded file gives up every cue, unframed, with its times",
|
|
|
255
256
|
const sourceKey = "a".repeat(40);
|
|
256
257
|
forgetSubtitles(sourceKey);
|
|
257
258
|
try {
|
|
258
|
-
const plain = await cuesHeldFor(torrent, 0, sourceKey, 2);
|
|
259
|
+
const plain = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
|
|
259
260
|
assert.deepEqual(
|
|
260
261
|
plain.cues.map((cue) => [cue.startSeconds, cue.endSeconds, cue.text]),
|
|
261
262
|
[
|
|
@@ -264,7 +265,7 @@ test("a walk of a downloaded file gives up every cue, unframed, with its times",
|
|
|
264
265
|
]
|
|
265
266
|
);
|
|
266
267
|
|
|
267
|
-
const ass = await cuesHeldFor(torrent, 0, sourceKey, 3);
|
|
268
|
+
const ass = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3);
|
|
268
269
|
assert.deepEqual(
|
|
269
270
|
ass.cues.map((cue) => [cue.startSeconds, cue.endSeconds, cue.text]),
|
|
270
271
|
[
|
|
@@ -284,7 +285,7 @@ test("the cursor is the order cues were FOUND, and it never repeats", async () =
|
|
|
284
285
|
const sourceKey = "b".repeat(40);
|
|
285
286
|
forgetSubtitles(sourceKey);
|
|
286
287
|
try {
|
|
287
|
-
const held = await cuesHeldFor(torrent, 0, sourceKey, 2);
|
|
288
|
+
const held = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
|
|
288
289
|
const cursors = held.cues.map((cue) => cue.seq);
|
|
289
290
|
assert.deepEqual(cursors, [1, 2], "two cues found, in the order they were read");
|
|
290
291
|
assert.equal(new Set(cursors).size, cursors.length, "no cue shares a cursor with another");
|
|
@@ -300,7 +301,7 @@ test("a cluster whose bytes are not downloaded is left for next time", async ()
|
|
|
300
301
|
const sourceKey = "c".repeat(40);
|
|
301
302
|
forgetSubtitles(sourceKey);
|
|
302
303
|
try {
|
|
303
|
-
const held = await cuesHeldFor(torrent, 0, sourceKey, 2);
|
|
304
|
+
const held = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
|
|
304
305
|
assert.deepEqual(
|
|
305
306
|
held.cues.map((cue) => cue.text),
|
|
306
307
|
["First English line"],
|
|
@@ -319,10 +320,10 @@ test("one walk fills every track, and a second call reads no cluster again", asy
|
|
|
319
320
|
const sourceKey = "d".repeat(40);
|
|
320
321
|
forgetSubtitles(sourceKey);
|
|
321
322
|
try {
|
|
322
|
-
await cuesHeldFor(torrent, 0, sourceKey, 2);
|
|
323
|
+
await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 2);
|
|
323
324
|
const clusterReads = reads.filter((range) => clusterAt.includes(range.start)).length;
|
|
324
325
|
|
|
325
|
-
const other = await cuesHeldFor(torrent, 0, sourceKey, 3);
|
|
326
|
+
const other = await cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3);
|
|
326
327
|
assert.equal(other.cues.length, 2, "the other track was filled by the same walk");
|
|
327
328
|
assert.equal(
|
|
328
329
|
reads.filter((range) => clusterAt.includes(range.start)).length,
|
|
@@ -340,14 +341,14 @@ test("the warm pass reports what is new, by the number the browser knows", async
|
|
|
340
341
|
const sourceKey = "e".repeat(40);
|
|
341
342
|
forgetSubtitles(sourceKey);
|
|
342
343
|
try {
|
|
343
|
-
const first = await warmSubtitleCues(torrent, 0, sourceKey);
|
|
344
|
+
const first = await warmSubtitleCues(heldFileOver(torrent, 0, sourceKey));
|
|
344
345
|
assert.deepEqual(
|
|
345
346
|
first.map((entry) => [entry.trackIndex, entry.cues.length, entry.language]).sort(),
|
|
346
347
|
[[0, 2, "eng"], [1, 2, "rus"]].sort(),
|
|
347
348
|
"both text tracks gained two cues, numbered as ffmpeg numbers them"
|
|
348
349
|
);
|
|
349
350
|
|
|
350
|
-
const again = await warmSubtitleCues(torrent, 0, sourceKey);
|
|
351
|
+
const again = await warmSubtitleCues(heldFileOver(torrent, 0, sourceKey));
|
|
351
352
|
assert.deepEqual(again, [], "nothing is new the second time round");
|
|
352
353
|
} finally {
|
|
353
354
|
forgetSubtitles(sourceKey);
|
|
@@ -360,7 +361,7 @@ test("a torrent that cannot say which pieces it holds is refused, not answered e
|
|
|
360
361
|
const sourceKey = "f".repeat(40);
|
|
361
362
|
forgetSubtitles(sourceKey);
|
|
362
363
|
try {
|
|
363
|
-
const held = await cuesHeldFor({ ...torrent,
|
|
364
|
+
const held = await cuesHeldFor({ ...heldFileOver(torrent, 0, sourceKey), heldRanges: null }, 2);
|
|
364
365
|
assert.deepEqual(held.cues, []);
|
|
365
366
|
assert.equal(held.track, null, "the answer says nothing was read, not that there is nothing");
|
|
366
367
|
} finally {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { Container } from "../services/container/Container.js";
|
|
3
|
+
import { Container } from "../services/media/container/Container.js";
|
|
4
4
|
|
|
5
5
|
test("a flag the container wrote is carried through, and one it did not is not", () => {
|
|
6
6
|
const banner = [
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
import test from "node:test";
|
|
25
25
|
import assert from "node:assert/strict";
|
|
26
26
|
|
|
27
|
-
import { detectLanguage } from "../services/tracks/language-detect.js";
|
|
28
|
-
import { SubtitleFileContainer } from "../services/container/SubtitleFileContainer.js";
|
|
29
|
-
import { TextSubtitleTrack } from "../services/tracks/TextSubtitleTrack.js";
|
|
30
|
-
import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
|
|
27
|
+
import { detectLanguage } from "../services/media/tracks/language-detect.js";
|
|
28
|
+
import { SubtitleFileContainer } from "../services/media/container/SubtitleFileContainer.js";
|
|
29
|
+
import { TextSubtitleTrack } from "../services/media/tracks/TextSubtitleTrack.js";
|
|
30
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
31
31
|
|
|
32
32
|
/** Varied Russian dialogue, the length a few minutes of an episode carries. */
|
|
33
33
|
const RUSSIAN_DIALOGUE = [
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
import test from "node:test";
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
22
|
import { Readable } from "node:stream";
|
|
23
|
-
import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
|
|
24
|
-
import { cuesHeldFor, forgetSubtitles } from "../services/
|
|
23
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
24
|
+
import { cuesHeldFor, forgetSubtitles } from "../services/media/SubtitleCues.js";
|
|
25
|
+
import { heldFileOver } from "./helpers/held-file.js";
|
|
25
26
|
|
|
26
27
|
const ID_EBML = 0x1a45dfa3;
|
|
27
28
|
const ID_SEGMENT = 0x18538067;
|
|
@@ -263,8 +264,8 @@ test("two walks of one file at the same time read each cluster once", async () =
|
|
|
263
264
|
// Both text tracks at once, which is what the warmup does on every verified
|
|
264
265
|
// piece and every three seconds.
|
|
265
266
|
const [first, second] = await Promise.all([
|
|
266
|
-
cuesHeldFor(torrent, 0, sourceKey, 3),
|
|
267
|
-
cuesHeldFor(torrent, 0, sourceKey, 4)
|
|
267
|
+
cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3),
|
|
268
|
+
cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 4)
|
|
268
269
|
]);
|
|
269
270
|
|
|
270
271
|
assert.equal(first.coveredClusters, 1, "the cluster the table names was walked");
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import test from "node:test";
|
|
12
12
|
|
|
13
|
-
import { minimumBufferFrom, requiredSpeedFrom } from "../services/supply-margin.js";
|
|
13
|
+
import { minimumBufferFrom, requiredSpeedFrom } from "../services/torrent/supply-margin.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Waits spaced by `intervalSec`, each lasting `waitSec`.
|
|
@@ -32,9 +32,9 @@ import {
|
|
|
32
32
|
rejoinSwarm,
|
|
33
33
|
stateFileEdges,
|
|
34
34
|
swarmDecisionFor
|
|
35
|
-
} from "../services/torrent-pool.js";
|
|
36
|
-
import { demandFor, forgetTorrent } from "../services/download/registry.js";
|
|
37
|
-
import { Urgency } from "../services/demand/index.js";
|
|
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
38
|
|
|
39
39
|
/**
|
|
40
40
|
* A torrent that records what was done to it. Only the surface the rule
|
package/test/swarm-reach.test.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
20
|
import test from "node:test";
|
|
21
21
|
|
|
22
|
-
import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent-pool.js";
|
|
22
|
+
import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent/torrent-pool.js";
|
|
23
23
|
|
|
24
24
|
test("connected and known are separate numbers", () => {
|
|
25
25
|
const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
|
|
12
|
-
import { DemandRegister } from "../services/demand/DemandRegister.js";
|
|
13
|
-
import { Urgency } from "../services/demand/Urgency.js";
|
|
14
|
-
import { SwarmSelection } from "../services/download/SwarmSelection.js";
|
|
12
|
+
import { DemandRegister } from "../services/torrent/demand/DemandRegister.js";
|
|
13
|
+
import { Urgency } from "../services/torrent/demand/Urgency.js";
|
|
14
|
+
import { SwarmSelection } from "../services/torrent/download/SwarmSelection.js";
|
|
15
15
|
|
|
16
16
|
const PIECE = 1024;
|
|
17
17
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
|
-
import { duplicateTailFor } from "../services/torrent
|
|
14
|
+
import { duplicateTailFor } from "../services/torrent/worker/fastest-wires.js";
|
|
15
15
|
|
|
16
16
|
/** A piece with `chunks` blocks, of which `missing` at the end are absent. */
|
|
17
17
|
function piece(chunks, missing) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Every call across the thread boundary has something to land on.
|
|
3
|
+
*
|
|
4
|
+
* The boundary is three files that have to agree, and nothing made them:
|
|
5
|
+
* `protocol.js` names the commands, `worker.js` answers them, `client.js` asks,
|
|
6
|
+
* and `pool-adapter.js` is what the main thread calls. A mismatch anywhere in
|
|
7
|
+
* that chain is invisible to every other check in this repository, because a
|
|
8
|
+
* unit test of either end alone passes and the failure needs a real worker.
|
|
9
|
+
*
|
|
10
|
+
* It was not hypothetical. Removing three commands on 2026-09-15 took an
|
|
11
|
+
* unrelated neighbour with them — `client.fillFile`, which the adapter went on
|
|
12
|
+
* calling — and left two adapter methods calling commands that no longer
|
|
13
|
+
* existed. One of those two is the browser's own subtitle request, and it did
|
|
14
|
+
* not even throw where anybody would see it: the orchestrator caught the error
|
|
15
|
+
* and answered an EMPTY document, which is indistinguishable from a file that
|
|
16
|
+
* genuinely holds no cues. That is the exact failure the whole subtitle path is
|
|
17
|
+
* written to avoid, and it would have reached the field.
|
|
18
|
+
*
|
|
19
|
+
* These are structural because the property is: there is no input that makes a
|
|
20
|
+
* call land on a method that is not there.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import test from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const WORKER = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../services/torrent/worker");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function source(name) {
|
|
36
|
+
return readFileSync(path.join(WORKER, name), "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Names a file defines at the top level of a class: methods, and fields it
|
|
41
|
+
* assigns to itself. Both are callable or readable from outside.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} text
|
|
44
|
+
* @returns {Set<string>}
|
|
45
|
+
*/
|
|
46
|
+
function membersOf(text) {
|
|
47
|
+
const names = new Set();
|
|
48
|
+
for (const match of text.matchAll(/^\s{2}(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?([a-zA-Z#][a-zA-Z0-9]*)\s*[(=]/gm)) {
|
|
49
|
+
names.add(match[1]);
|
|
50
|
+
}
|
|
51
|
+
// Fields a constructor or a method assigns rather than declares.
|
|
52
|
+
for (const match of text.matchAll(/this\.([a-zA-Z][a-zA-Z0-9]*)\s*=/g)) {
|
|
53
|
+
names.add(match[1]);
|
|
54
|
+
}
|
|
55
|
+
return names;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("every client method the adapter calls exists", () => {
|
|
59
|
+
const have = membersOf(source("client.js"));
|
|
60
|
+
const used = [...source("pool-adapter.js").matchAll(/#client\.([a-zA-Z][a-zA-Z0-9]*)/g)].map((m) => m[1]);
|
|
61
|
+
|
|
62
|
+
assert.deepEqual([...new Set(used)].filter((name) => !have.has(name)), []);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("every command the client sends is answered by the worker", () => {
|
|
66
|
+
const protocol = source("protocol.js");
|
|
67
|
+
const answered = new Set([...source("worker.js").matchAll(/case Command\.([A-Z_]+):/g)].map((m) => m[1]));
|
|
68
|
+
const sent = [...source("client.js").matchAll(/Command\.([A-Z_]+)/g)].map((m) => m[1]);
|
|
69
|
+
|
|
70
|
+
const unanswered = [...new Set(sent)].filter((name) => !answered.has(name));
|
|
71
|
+
assert.deepEqual(unanswered, [], "a command nobody answers waits for a reply that never comes");
|
|
72
|
+
|
|
73
|
+
const undeclared = [...new Set(sent)].filter((name) => !protocol.includes(` ${name}:`));
|
|
74
|
+
assert.deepEqual(undeclared, [], "and one the protocol does not name is `undefined` on the wire");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("every event the worker posts is received somewhere on the main thread", () => {
|
|
78
|
+
// TWO receivers, and they answer different shapes. `channel.js` matches the
|
|
79
|
+
// replies to a request by its id — a result, an error, a chunk of a body —
|
|
80
|
+
// while `client.js` has a `case` for each event that belongs to nobody's
|
|
81
|
+
// request. An event handled by neither is a fact announced into silence.
|
|
82
|
+
const receiving = `${source("client.js")}
|
|
83
|
+
${source("channel.js")}`;
|
|
84
|
+
const handled = new Set([
|
|
85
|
+
...[...receiving.matchAll(/case Event\.([A-Z_]+):/g)].map((m) => m[1]),
|
|
86
|
+
...[...receiving.matchAll(/message\.type === Event\.([A-Z_]+)/g)].map((m) => m[1])
|
|
87
|
+
]);
|
|
88
|
+
const posted = [...source("worker.js").matchAll(/type:\s*Event\.([A-Z_]+)/g)].map((m) => m[1]);
|
|
89
|
+
|
|
90
|
+
assert.deepEqual([...new Set(posted)].filter((name) => !handled.has(name)), []);
|
|
91
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The torrent thread serves bytes. It does not read what a file says.
|
|
3
|
+
*
|
|
4
|
+
* Both halves of that were true of the design and only one was true of the
|
|
5
|
+
* code: pieces have lived in shared memory since the reader was rewritten, and
|
|
6
|
+
* the torrent thread ALSO parsed containers — the track table, the media info
|
|
7
|
+
* and the keyframe index — and carried each answer back over the channel.
|
|
8
|
+
*
|
|
9
|
+
* The stated reason was "the main thread cannot open a read stream on one of
|
|
10
|
+
* its files". That is true of WebTorrent's own API and not of the bytes: a
|
|
11
|
+
* container is built from one function, `readRange(start, end)`, and the read
|
|
12
|
+
* that serves every segment already waits for what has not arrived and steers
|
|
13
|
+
* the swarm toward it. What the split cost was three commands, a second
|
|
14
|
+
* `ContainerOrchestrator` in that thread, and — because an answer then had to
|
|
15
|
+
* be carried across — the question of where each such answer is KEPT, which is
|
|
16
|
+
* how one fact came to be stored twice.
|
|
17
|
+
*
|
|
18
|
+
* These checks are structural because the property is structural: there is no
|
|
19
|
+
* input that makes a parse happen in the wrong thread, only an import.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const WORKER = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../services/torrent/worker");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} name
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function source(name) {
|
|
35
|
+
return readFileSync(path.join(WORKER, name), "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("no command asks the torrent thread what a file declares", () => {
|
|
39
|
+
const protocol = source("protocol.js");
|
|
40
|
+
|
|
41
|
+
for (const gone of [
|
|
42
|
+
"CONTAINER_TRACKS",
|
|
43
|
+
"CONTAINER_MEDIA_INFO",
|
|
44
|
+
"CONTAINER_KEYFRAMES",
|
|
45
|
+
"SUBTITLE_TRACKS",
|
|
46
|
+
"SUBTITLE_CUES"
|
|
47
|
+
]) {
|
|
48
|
+
assert.equal(
|
|
49
|
+
protocol.includes(gone),
|
|
50
|
+
false,
|
|
51
|
+
`${gone} is a question about the FILE, and it is answered where the sessions are`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("nothing in that thread reaches into the media layer at all", () => {
|
|
57
|
+
// The last one to was the subtitle cue walk, and it moved with the rest: it
|
|
58
|
+
// is handed which ranges of a file are downloaded whole and how to read one
|
|
59
|
+
// of those, and it has no use for a torrent beyond that. What stayed behind
|
|
60
|
+
// is the two things only this thread can answer — the bitfield as a list of
|
|
61
|
+
// ranges, and a store read that never fetches (`held-bytes.js`).
|
|
62
|
+
const reaching = readdirSync(WORKER)
|
|
63
|
+
.filter((name) => name.endsWith(".js"))
|
|
64
|
+
.filter((name) => /from "(?:\.\.\/)+media\//.test(source(name)));
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(reaching, []);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("what the walk may read is answered as a list, not asked per cluster", () => {
|
|
70
|
+
// A pass asks about every cluster of the file — hundreds on the field files,
|
|
71
|
+
// every three seconds. One list per pass costs one message; the same answers
|
|
72
|
+
// one at a time would cost hundreds of round trips for a walk that is meant
|
|
73
|
+
// to be free when there is nothing new to find.
|
|
74
|
+
const held = source("held-bytes.js");
|
|
75
|
+
|
|
76
|
+
assert.match(held, /export function heldRangesOf\(torrent, fileIndex\)/);
|
|
77
|
+
assert.match(held, /export function readHeldBytes\(/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("the walk's read never fetches", () => {
|
|
81
|
+
// The ordinary range read declares demand and steers the swarm, which is
|
|
82
|
+
// right for a viewer waiting on a segment and wrong here: switching subtitles
|
|
83
|
+
// on must not pull bytes the viewer is not waiting for.
|
|
84
|
+
const held = source("held-bytes.js");
|
|
85
|
+
|
|
86
|
+
assert.equal(held.includes("demandFor"), false);
|
|
87
|
+
assert.equal(held.includes("readFragments"), false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("the resume warm is TOLD how long the file runs", () => {
|
|
91
|
+
// It used to read the container itself to find out, which is what a file
|
|
92
|
+
// states about itself. What is left here is the part that really is the
|
|
93
|
+
// torrent's: a position in seconds becomes a byte offset, and that region is
|
|
94
|
+
// pulled off the swarm.
|
|
95
|
+
const warm = source("resume-warm.js");
|
|
96
|
+
|
|
97
|
+
assert.match(warm, /options\.durationSeconds/);
|
|
98
|
+
assert.equal(warm.includes("containerOrchestrator"), false);
|
|
99
|
+
assert.equal(warm.includes("ContainerFactory"), false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("the main thread can read a byte range of a file", () => {
|
|
103
|
+
// The one thing the move needed, and the whole of what a container is built
|
|
104
|
+
// from. Without it the parse has nowhere to get its bytes and the commands
|
|
105
|
+
// come back.
|
|
106
|
+
assert.match(source("pool-adapter.js"), /async readRangeOf\(torrent, fileIndex, start, end\)/);
|
|
107
|
+
});
|
package/test/thread-pool.test.js
CHANGED
|
@@ -21,7 +21,7 @@ test("the pool is set, and left alone when the deployment states its own", async
|
|
|
21
21
|
const previous = process.env.UV_THREADPOOL_SIZE;
|
|
22
22
|
try {
|
|
23
23
|
process.env.UV_THREADPOOL_SIZE = "";
|
|
24
|
-
await import(`../
|
|
24
|
+
await import(`../bin/thread-pool.js?first=${Date.now()}`);
|
|
25
25
|
assert.equal(
|
|
26
26
|
Number(process.env.UV_THREADPOOL_SIZE) >= 16,
|
|
27
27
|
true,
|
|
@@ -29,7 +29,7 @@ test("the pool is set, and left alone when the deployment states its own", async
|
|
|
29
29
|
);
|
|
30
30
|
|
|
31
31
|
process.env.UV_THREADPOOL_SIZE = "8";
|
|
32
|
-
await import(`../
|
|
32
|
+
await import(`../bin/thread-pool.js?stated=${Date.now()}`);
|
|
33
33
|
assert.equal(process.env.UV_THREADPOOL_SIZE, "8", "a stated size is the deployment's to choose");
|
|
34
34
|
} finally {
|
|
35
35
|
if (previous === undefined) {
|
|
@@ -47,7 +47,7 @@ test("the entry point imports it before anything that could create the pool", as
|
|
|
47
47
|
|
|
48
48
|
assert.equal(
|
|
49
49
|
imports[0],
|
|
50
|
-
"
|
|
50
|
+
"./thread-pool.js",
|
|
51
51
|
"a module's imports run before its body, so this cannot be a statement in cli.js"
|
|
52
52
|
);
|
|
53
53
|
});
|
package/test/timeline.test.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
|
-
import { Timeline, Timelines } from "../services/output/Timeline.js";
|
|
14
|
+
import { Timeline, Timelines } from "../services/encode/output/Timeline.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* @returns {Timeline}
|
|
@@ -73,17 +73,6 @@ test("which segment holds a moment", () => {
|
|
|
73
73
|
assert.equal(timeline.indexForTime(99), 3, "past the end is the last segment, not an error");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("a fresh table starts with an empty tally of how well its index matched", () => {
|
|
77
|
-
const timeline = fourSecondGrid();
|
|
78
|
-
|
|
79
|
-
// The tally is a fact about the FILE and its index: asked per session it
|
|
80
|
-
// would be answered a different number of times for one film depending on
|
|
81
|
-
// how many people happened to watch it.
|
|
82
|
-
assert.equal(timeline.indexCheck.checked, 0);
|
|
83
|
-
assert.equal(timeline.indexCheck.disagreed, 0);
|
|
84
|
-
assert.equal(timeline.indexCheck.firstDisagreementIndex, -1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
76
|
test("a timeline nobody holds is dropped", () => {
|
|
88
77
|
const timelines = new Timelines();
|
|
89
78
|
const kept = timelines.get(Timelines.keyFor("torrent:abc", 0, "keyframe"), fourSecondGrid);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import { test } from "node:test";
|
|
11
|
-
import { baseDrawFrom, costPerMegabyteFrom } from "../services/torrent-cost.js";
|
|
11
|
+
import { baseDrawFrom, costPerMegabyteFrom } from "../services/encode/torrent-cost.js";
|
|
12
12
|
|
|
13
13
|
test("the base draw is the share of a core spent with nothing to do", () => {
|
|
14
14
|
assert.equal(baseDrawFrom({ cpuSeconds: 0.5, elapsedSeconds: 5 }), 0.1);
|