@torrent-tv/proxy 2.86.0 → 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 -1915
- 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 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- 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 -45
- 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 -515
- 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 -202
- 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 -9608
- 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/services/viewer/link-readings.js +0 -56
- 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
|
@@ -54,8 +54,6 @@ const REMOVAL = { force: true, maxRetries: 10, retryDelay: 20 };
|
|
|
54
54
|
export class PieceDiskStore {
|
|
55
55
|
#directory;
|
|
56
56
|
|
|
57
|
-
#chunkLength;
|
|
58
|
-
|
|
59
57
|
/** Piece index → its length on disk. @type {Map<number, number>} */
|
|
60
58
|
#stored = new Map();
|
|
61
59
|
|
|
@@ -99,9 +97,6 @@ export class PieceDiskStore {
|
|
|
99
97
|
* @param {string} params.directory - Where this torrent's pieces live.
|
|
100
98
|
* @param {string} params.name - A name unique to the torrent; it becomes the
|
|
101
99
|
* directory inside `directory`.
|
|
102
|
-
* @param {number} params.chunkLength - The torrent's piece length. Kept for
|
|
103
|
-
* the caller's arithmetic; a piece's own length is recorded as it is written,
|
|
104
|
-
* because the last piece of a torrent is shorter.
|
|
105
100
|
* @param {number | null} [params.allowanceBytes] - What it may hold. Null
|
|
106
101
|
* means nobody has said yet, and nothing is evicted until somebody does.
|
|
107
102
|
* @param {() => number} [params.now]
|
|
@@ -109,14 +104,12 @@ export class PieceDiskStore {
|
|
|
109
104
|
constructor({
|
|
110
105
|
directory,
|
|
111
106
|
name,
|
|
112
|
-
chunkLength,
|
|
113
107
|
allowanceBytes = null,
|
|
114
108
|
now = Date.now,
|
|
115
109
|
readHeads = () => [],
|
|
116
110
|
onForgotten = () => undefined
|
|
117
111
|
}) {
|
|
118
112
|
this.#directory = path.join(directory, name);
|
|
119
|
-
this.#chunkLength = chunkLength;
|
|
120
113
|
this.#allowanceBytes = Number.isFinite(allowanceBytes) && allowanceBytes >= 0 ? allowanceBytes : null;
|
|
121
114
|
this.#now = now;
|
|
122
115
|
this.#readHeads = typeof readHeads === "function" ? readHeads : () => [];
|
|
@@ -41,9 +41,8 @@
|
|
|
41
41
|
* and nothing here disagrees.
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
-
import
|
|
45
|
-
import {
|
|
46
|
-
import { divideAllowance, machineAllowanceBytes, OtherDemand } from "./allowance.js";
|
|
44
|
+
import { divideAllowance, machineAllowanceBytes, OtherDemand } from "../allowance.js";
|
|
45
|
+
import { availableMemoryBytes as availableMemorySync } from "../machine-memory.js";
|
|
47
46
|
import { PieceLru } from "./piece-lru.js";
|
|
48
47
|
import { PieceDiskStore } from "./piece-disk-store.js";
|
|
49
48
|
|
|
@@ -112,12 +111,20 @@ export function forgetMachineMemory() {
|
|
|
112
111
|
|
|
113
112
|
export { divideAllowance, machineAllowanceBytes };
|
|
114
113
|
|
|
115
|
-
export function reviseStoreBudgets() {
|
|
114
|
+
export function reviseStoreBudgets(allowanceBytes = null) {
|
|
116
115
|
const stores = [...liveStores];
|
|
117
116
|
const held = stores.reduce((sum, store) => sum + store.residentBytes, 0);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// TOLD, NOT WORKED OUT. Memory has one owner and it is on the main thread,
|
|
118
|
+
// where the disk's owner is — and it must be the same one, because the two
|
|
119
|
+
// resources trade: pieces that do not fit in memory are spilled to disk, so
|
|
120
|
+
// what this store is given decides how much disk it needs. What arrives is
|
|
121
|
+
// this thread's whole share; the stores divide it between themselves.
|
|
122
|
+
//
|
|
123
|
+
// Null is a thread that has not been told yet, and it works its own share out
|
|
124
|
+
// for that one pass rather than growing without a bound.
|
|
125
|
+
const allowance = Number.isFinite(allowanceBytes) && allowanceBytes !== null
|
|
126
|
+
? Math.max(0, allowanceBytes)
|
|
127
|
+
: machineAllowanceBytes(availableMemorySync(), held, noteMachineMemory(availableMemorySync(), held));
|
|
121
128
|
const shares = divideAllowance(stores.map((store) => store.wantedBytes), allowance);
|
|
122
129
|
const revised = [];
|
|
123
130
|
for (const [position, store] of stores.entries()) {
|
|
@@ -175,18 +182,6 @@ function defaultMemoryBytes() {
|
|
|
175
182
|
return Math.floor(allowance / (stores.length + 1));
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
function availableMemorySync() {
|
|
179
|
-
try {
|
|
180
|
-
const text = readFileSync("/proc/meminfo", "utf8");
|
|
181
|
-
const match = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(text);
|
|
182
|
-
if (match) {
|
|
183
|
-
return Number(match[1]) * 1024;
|
|
184
|
-
}
|
|
185
|
-
} catch {
|
|
186
|
-
}
|
|
187
|
-
return os.freemem();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
185
|
/**
|
|
191
186
|
* How many blocks of piece memory this thread has let go of, and how many the
|
|
192
187
|
* collector has actually taken back.
|
|
@@ -204,7 +199,7 @@ function availableMemorySync() {
|
|
|
204
199
|
*
|
|
205
200
|
* What it does NOT prove: a `SharedArrayBuffer`'s memory is shared between the
|
|
206
201
|
* isolates, so this thread's handle going means only that THIS thread let go.
|
|
207
|
-
* The main thread counts its own (`torrent
|
|
202
|
+
* The main thread counts its own (`torrent/worker/client.js`), and the pair is
|
|
208
203
|
* what answers the question.
|
|
209
204
|
*/
|
|
210
205
|
const released = { count: 0, collected: 0 };
|
|
@@ -522,7 +517,6 @@ export class SharedPieceStore {
|
|
|
522
517
|
this.#disk = options.disk ?? new PieceDiskStore({
|
|
523
518
|
directory: options.path ?? ".",
|
|
524
519
|
name: `${this.#name}.pieces`,
|
|
525
|
-
chunkLength,
|
|
526
520
|
// What it may hold is settled by the same revision that settles memory,
|
|
527
521
|
// within a minute of the store existing. Until then it is unbounded, which
|
|
528
522
|
// is what it has always been — the difference is that it now stops.
|
|
@@ -1871,3 +1865,16 @@ export class SharedPieceStore {
|
|
|
1871
1865
|
this.#disk.destroy().then(() => callback(null), (error) => callback(error));
|
|
1872
1866
|
}
|
|
1873
1867
|
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* What the stores in this thread hold and would take, for the owner of memory.
|
|
1871
|
+
*
|
|
1872
|
+
* @returns {{ held: number, wanted: number }}
|
|
1873
|
+
*/
|
|
1874
|
+
export function memoryClaim() {
|
|
1875
|
+
const stores = [...liveStores];
|
|
1876
|
+
return {
|
|
1877
|
+
held: stores.reduce((sum, store) => sum + store.residentBytes, 0),
|
|
1878
|
+
wanted: stores.reduce((sum, store) => sum + store.wantedBytes, 0)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { createHash } from "node:crypto";
|
|
43
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
43
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, watch, writeFileSync } from "node:fs";
|
|
44
44
|
import { rmSync } from "node:fs";
|
|
45
45
|
import os from "node:os";
|
|
46
46
|
import path from "node:path";
|
|
@@ -90,6 +90,27 @@ export class SegmentStore {
|
|
|
90
90
|
/** Output key → when it was last asked for. @type {Map<string, number>} */
|
|
91
91
|
#touched = new Map();
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Output key → the init segment served for it, once one has been read whole.
|
|
95
|
+
*
|
|
96
|
+
* A property of the output's produced material, so it lives with that
|
|
97
|
+
* material and goes when it goes. The player fetches it once and never again,
|
|
98
|
+
* so the first whole one is kept and every later request gets the same bytes,
|
|
99
|
+
* even though a re-encoding run rewrites the file on disk.
|
|
100
|
+
*
|
|
101
|
+
* @type {Map<string, Buffer>}
|
|
102
|
+
*/
|
|
103
|
+
#inits = new Map();
|
|
104
|
+
|
|
105
|
+
/** Told of every segment published, whatever it is. @type {Set<(key: string, index: number) => void>} */
|
|
106
|
+
#publishedListeners = new Set();
|
|
107
|
+
|
|
108
|
+
/** Segment publication waiters by output key and segment number. @type {Map<string, Map<number, Set<Function>>>} */
|
|
109
|
+
#waiters = new Map();
|
|
110
|
+
|
|
111
|
+
/** Output key → the watch on its directory. @type {Map<string, import("node:fs").FSWatcher>} */
|
|
112
|
+
#watchers = new Map();
|
|
113
|
+
|
|
93
114
|
/** @type {{ info: Function, warn: Function }} */
|
|
94
115
|
#logger;
|
|
95
116
|
|
|
@@ -144,6 +165,9 @@ export class SegmentStore {
|
|
|
144
165
|
writeFileSync(path.join(dir, KEY_FILE), `${key}\n`, "utf8");
|
|
145
166
|
}
|
|
146
167
|
this.#touched.set(key, this.#now());
|
|
168
|
+
// Production is about to begin here, and a piece closing in this directory
|
|
169
|
+
// is what every held request is waiting for.
|
|
170
|
+
this.#watch(key);
|
|
147
171
|
return dir;
|
|
148
172
|
}
|
|
149
173
|
|
|
@@ -187,6 +211,7 @@ export class SegmentStore {
|
|
|
187
211
|
return known;
|
|
188
212
|
}
|
|
189
213
|
const byNumber = new Map();
|
|
214
|
+
const sizes = new Map();
|
|
190
215
|
let bytes = 0;
|
|
191
216
|
let highest = -1;
|
|
192
217
|
// The biggest piece and which number it is. What reads it is the figure the
|
|
@@ -220,6 +245,7 @@ export class SegmentStore {
|
|
|
220
245
|
continue;
|
|
221
246
|
}
|
|
222
247
|
byNumber.set(index, full);
|
|
248
|
+
sizes.set(index, size);
|
|
223
249
|
bytes += size;
|
|
224
250
|
if (index > highest) {
|
|
225
251
|
highest = index;
|
|
@@ -232,7 +258,7 @@ export class SegmentStore {
|
|
|
232
258
|
this.#held.delete(key);
|
|
233
259
|
return empty;
|
|
234
260
|
}
|
|
235
|
-
const contents = { readAt: mtime, byNumber, bytes, unproven: highest, largest };
|
|
261
|
+
const contents = { readAt: mtime, byNumber, sizes, bytes, unproven: highest, largest };
|
|
236
262
|
this.#held.set(key, contents);
|
|
237
263
|
return contents;
|
|
238
264
|
}
|
|
@@ -276,6 +302,20 @@ export class SegmentStore {
|
|
|
276
302
|
return this.refresh(key).byNumber.size;
|
|
277
303
|
}
|
|
278
304
|
|
|
305
|
+
/**
|
|
306
|
+
* The size in bytes of every finished piece of this output, by number.
|
|
307
|
+
*
|
|
308
|
+
* Read by whoever keeps what an output's segments carried (roadmap item 97,
|
|
309
|
+
* step 14). A reading, not a request: it does not mark the output as read,
|
|
310
|
+
* so it does not keep an output nobody is watching.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} key
|
|
313
|
+
* @returns {Map<number, number>}
|
|
314
|
+
*/
|
|
315
|
+
sizesOf(key) {
|
|
316
|
+
return new Map(this.refresh(key).sizes ?? []);
|
|
317
|
+
}
|
|
318
|
+
|
|
279
319
|
/**
|
|
280
320
|
* The biggest piece this output has made, and which number it is.
|
|
281
321
|
*
|
|
@@ -292,6 +332,16 @@ export class SegmentStore {
|
|
|
292
332
|
}
|
|
293
333
|
|
|
294
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Every output whose pieces this store knows how to read: those in use and
|
|
337
|
+
* those a previous life of the process left.
|
|
338
|
+
*
|
|
339
|
+
* @returns {string[]}
|
|
340
|
+
*/
|
|
341
|
+
addresses() {
|
|
342
|
+
return [...this.#formats.keys()];
|
|
343
|
+
}
|
|
344
|
+
|
|
295
345
|
/**
|
|
296
346
|
* Whether this piece is finished, and may therefore be served.
|
|
297
347
|
*
|
|
@@ -395,9 +445,287 @@ export class SegmentStore {
|
|
|
395
445
|
}
|
|
396
446
|
// What the directory holds has changed, so the memory of it is stale.
|
|
397
447
|
this.#held.delete(key);
|
|
448
|
+
const index = format?.segmentIndexFromName?.(served) ?? -1;
|
|
449
|
+
if (index >= 0) {
|
|
450
|
+
this.announce(key, index);
|
|
451
|
+
for (const listener of this.#publishedListeners) {
|
|
452
|
+
listener(key, index);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
398
455
|
return served;
|
|
399
456
|
}
|
|
400
457
|
|
|
458
|
+
/**
|
|
459
|
+
* The init segment kept for this output, or null until one was read whole.
|
|
460
|
+
*
|
|
461
|
+
* @param {string} key
|
|
462
|
+
* @returns {Buffer | null}
|
|
463
|
+
*/
|
|
464
|
+
initOf(key) {
|
|
465
|
+
return this.#inits.get(key) ?? null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Keep the init segment served for this output. The first whole one stands;
|
|
470
|
+
* an empty read is not an init (ffmpeg creates the file before it writes the
|
|
471
|
+
* header into it) and is refused.
|
|
472
|
+
*
|
|
473
|
+
* @param {string} key
|
|
474
|
+
* @param {Buffer | null} bytes
|
|
475
|
+
* @returns {Buffer | null} The init that now stands for this output.
|
|
476
|
+
*/
|
|
477
|
+
keepInit(key, bytes) {
|
|
478
|
+
if (!this.#inits.has(key) && bytes && bytes.length > 0) {
|
|
479
|
+
this.#inits.set(key, bytes);
|
|
480
|
+
}
|
|
481
|
+
return this.initOf(key);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Be told of every segment published: what the store holds has grown, which
|
|
486
|
+
* is when whether it still fits its share is worth asking.
|
|
487
|
+
*
|
|
488
|
+
* @param {(key: string, index: number) => void} listener
|
|
489
|
+
* @returns {() => void} Stop being told.
|
|
490
|
+
*/
|
|
491
|
+
onPublished(listener) {
|
|
492
|
+
this.#publishedListeners.add(listener);
|
|
493
|
+
return () => this.#publishedListeners.delete(listener);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Watch this output's directory, so a piece closed by ANY writer is an event.
|
|
498
|
+
*
|
|
499
|
+
* WHY IT EXISTS, and it is one fault rather than a precaution. A piece is
|
|
500
|
+
* finished in one of two ways, and only one of them said so. The `segment`
|
|
501
|
+
* muxer writes under a working name and reports the closure on a channel of
|
|
502
|
+
* its own (`publish`); the `hls` muxer writes through a temporary name of its
|
|
503
|
+
* own (`+temp_file`) and reports nothing, so the file simply appears under the
|
|
504
|
+
* name it is served as. That branch is taken by every re-encoded output on the
|
|
505
|
+
* even grid — the ordinary quality step — and for those `waitFor` could only
|
|
506
|
+
* end on its deadline: the segment was on disk and the request that wanted it
|
|
507
|
+
* went on waiting, up to a full minute, and was then answered 503.
|
|
508
|
+
*
|
|
509
|
+
* So the store watches what it owns. The rename into place moves the
|
|
510
|
+
* directory's modification time, the kernel says so, and both branches reach
|
|
511
|
+
* one statement: this number is now present. Nothing polls.
|
|
512
|
+
*
|
|
513
|
+
* Idempotent, and started from the two places that need it — the directory
|
|
514
|
+
* being made for a run, and a wait beginning on an output this process has
|
|
515
|
+
* adopted rather than made.
|
|
516
|
+
*
|
|
517
|
+
* WHOEVER REMOVES ONE OF THESE DIRECTORIES GOES THROUGH `drop`, which stops
|
|
518
|
+
* the watch first. On Linux, where this runs, an open watch does not prevent
|
|
519
|
+
* a directory being removed; on Windows it does, and `rmSync` then retries
|
|
520
|
+
* until it gives up — measured, a check that removed a store's directory
|
|
521
|
+
* behind its back took 110 s instead of 3.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} key
|
|
524
|
+
* @returns {void}
|
|
525
|
+
*/
|
|
526
|
+
#watch(key) {
|
|
527
|
+
if (this.#watchers.has(key)) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const dir = path.join(this.#root, directoryNameFor(key));
|
|
531
|
+
// Nothing has been made for this output yet. A wait that begins first will
|
|
532
|
+
// ask again — `directoryFor` is what a run calls before it writes — and
|
|
533
|
+
// asking the kernel to watch what is not there throws, once for every wait
|
|
534
|
+
// that begins meanwhile.
|
|
535
|
+
if (!existsSync(dir)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const watcher = watch(dir, { persistent: false }, (_event, name) => {
|
|
540
|
+
this.#noticed(key, typeof name === "string" ? name : null);
|
|
541
|
+
});
|
|
542
|
+
// A directory that goes away takes its watch with it rather than leaving
|
|
543
|
+
// an error nobody reads.
|
|
544
|
+
watcher.on("error", () => this.#unwatch(key));
|
|
545
|
+
this.#watchers.set(key, watcher);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
// Watching is how a wait ends early, not how a segment is found: every
|
|
548
|
+
// reader still reads the disk. Said rather than swallowed, because a
|
|
549
|
+
// store that cannot watch holds each request for its whole deadline, and
|
|
550
|
+
// that is worth knowing before the field says it.
|
|
551
|
+
this.#logger?.warn?.(
|
|
552
|
+
`segment store: cannot watch ${key.slice(0, 60)}: ` +
|
|
553
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Stop watching one output's directory.
|
|
560
|
+
*
|
|
561
|
+
* @param {string} key
|
|
562
|
+
* @returns {void}
|
|
563
|
+
*/
|
|
564
|
+
#unwatch(key) {
|
|
565
|
+
const watcher = this.#watchers.get(key);
|
|
566
|
+
if (!watcher) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
this.#watchers.delete(key);
|
|
570
|
+
try {
|
|
571
|
+
watcher.close();
|
|
572
|
+
} catch {
|
|
573
|
+
// Already closed with its directory.
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The directory moved: say what is now there.
|
|
579
|
+
*
|
|
580
|
+
* A REMOVAL MOVES IT TOO, so the name is checked against the disk before
|
|
581
|
+
* anything is announced — otherwise clearing up after a dead run would read
|
|
582
|
+
* as that run's pieces arriving.
|
|
583
|
+
*
|
|
584
|
+
* @param {string} key
|
|
585
|
+
* @param {string | null} name - What changed, where the platform says.
|
|
586
|
+
* @returns {void}
|
|
587
|
+
*/
|
|
588
|
+
#noticed(key, name) {
|
|
589
|
+
// WHAT WAS READ OF THIS DIRECTORY IS STALE. Dropped rather than re-read:
|
|
590
|
+
// re-reading HERE is quadratic in the pieces of a film — one listing per
|
|
591
|
+
// file written, of a directory that grows with every file, measured at 110 s
|
|
592
|
+
// against 3 s over 482 pieces — while dropping costs one listing to whoever
|
|
593
|
+
// asks next, which is bounded by the questions and not by the writes.
|
|
594
|
+
//
|
|
595
|
+
// IT IS NOT ENOUGH TO LET `refresh` NOTICE BY ITSELF. It re-reads when the
|
|
596
|
+
// directory's modification time has moved, and that time is stated in
|
|
597
|
+
// milliseconds: a file written in the same millisecond as the last reading
|
|
598
|
+
// leaves it unmoved, and the piece is then reported absent although it is
|
|
599
|
+
// there. That is the hazard `refresh` documents and `publish` has always
|
|
600
|
+
// guarded against by dropping the memory.
|
|
601
|
+
this.#held.delete(key);
|
|
602
|
+
const format = this.#formats.get(key);
|
|
603
|
+
const dir = path.join(this.#root, directoryNameFor(key));
|
|
604
|
+
const index = name && format?.isSegmentFileName?.(name)
|
|
605
|
+
? (format.segmentIndexFromName?.(name) ?? -1)
|
|
606
|
+
: -1;
|
|
607
|
+
if (index >= 0) {
|
|
608
|
+
// A REMOVAL MOVES THE DIRECTORY TOO, so the one file is asked about
|
|
609
|
+
// before anything is announced — otherwise clearing up after a dead run
|
|
610
|
+
// would read as that run's pieces arriving.
|
|
611
|
+
if (!existsSync(path.join(dir, name))) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.#announceArrival(key, index);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (name) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// The platform named nothing. Then whoever is waiting asks about their own
|
|
621
|
+
// piece, which is the same question with the same answer and no guessing.
|
|
622
|
+
const byIndex = this.#waiters.get(key);
|
|
623
|
+
if (!byIndex || !format?.segmentFileName) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
for (const waiting of [...byIndex.keys()]) {
|
|
627
|
+
if (existsSync(path.join(dir, format.segmentFileName(waiting)))) {
|
|
628
|
+
this.#announceArrival(key, waiting);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* One statement that a number is now present here.
|
|
635
|
+
*
|
|
636
|
+
* @param {string} key
|
|
637
|
+
* @param {number} index
|
|
638
|
+
* @returns {void}
|
|
639
|
+
*/
|
|
640
|
+
#announceArrival(key, index) {
|
|
641
|
+
this.announce(key, index);
|
|
642
|
+
for (const listener of this.#publishedListeners) {
|
|
643
|
+
listener(key, index);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Announce that a segment is now present. This stores no readiness state;
|
|
649
|
+
* the file under its served name remains the only fact.
|
|
650
|
+
*
|
|
651
|
+
* @param {string} key
|
|
652
|
+
* @param {number} index
|
|
653
|
+
*/
|
|
654
|
+
announce(key, index) {
|
|
655
|
+
const byIndex = this.#waiters.get(key);
|
|
656
|
+
const waiting = byIndex?.get(index);
|
|
657
|
+
if (!waiting) return;
|
|
658
|
+
byIndex.delete(index);
|
|
659
|
+
if (byIndex.size === 0) this.#waiters.delete(key);
|
|
660
|
+
for (const resolve of waiting) resolve(true);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* How many requests are waiting on this output right now.
|
|
665
|
+
*
|
|
666
|
+
* The store's own state, and the one fact that says a wait EXISTS rather than
|
|
667
|
+
* is about to: a check that ends a wait it has not yet registered proves
|
|
668
|
+
* nothing, and passes just as well without the thing it is checking.
|
|
669
|
+
*
|
|
670
|
+
* @param {string} key
|
|
671
|
+
* @returns {number}
|
|
672
|
+
*/
|
|
673
|
+
waitingFor(key) {
|
|
674
|
+
let waiting = 0;
|
|
675
|
+
for (const holders of this.#waiters.get(key)?.values() ?? []) {
|
|
676
|
+
waiting += holders.size;
|
|
677
|
+
}
|
|
678
|
+
return waiting;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Wait for a segment publication event or one deadline. This does not poll
|
|
683
|
+
* the filesystem.
|
|
684
|
+
*
|
|
685
|
+
* @param {string} key
|
|
686
|
+
* @param {number} index
|
|
687
|
+
* @param {number} timeoutMs - May be infinite: then only the publication or
|
|
688
|
+
* `cancelled` ends the wait.
|
|
689
|
+
* @param {Promise<unknown> | null} [cancelled] - Settles when whoever waits
|
|
690
|
+
* has gone, so the waiter does not outlive them.
|
|
691
|
+
* @returns {Promise<boolean>}
|
|
692
|
+
*/
|
|
693
|
+
waitFor(key, index, timeoutMs, cancelled = null) {
|
|
694
|
+
if (this.pathOf(key, index)) return Promise.resolve(true);
|
|
695
|
+
// An output adopted from a previous process has a directory nobody made in
|
|
696
|
+
// this one, so the wait itself is the first moment it is watched.
|
|
697
|
+
this.#watch(key);
|
|
698
|
+
return new Promise((resolve) => {
|
|
699
|
+
const byIndex = this.#waiters.get(key) ?? new Map();
|
|
700
|
+
const waiting = byIndex.get(index) ?? new Set();
|
|
701
|
+
this.#waiters.set(key, byIndex);
|
|
702
|
+
byIndex.set(index, waiting);
|
|
703
|
+
let settled = false;
|
|
704
|
+
const finish = (published) => {
|
|
705
|
+
if (settled) return;
|
|
706
|
+
settled = true;
|
|
707
|
+
if (timer) clearTimeout(timer);
|
|
708
|
+
waiting.delete(finish);
|
|
709
|
+
// Removed only if they are still the sets this waiter joined: an
|
|
710
|
+
// announcement takes the set out before calling its waiters, and a new
|
|
711
|
+
// waiter may have registered a fresh one under the same number since.
|
|
712
|
+
if (waiting.size === 0 && byIndex.get(index) === waiting) byIndex.delete(index);
|
|
713
|
+
if (byIndex.size === 0 && this.#waiters.get(key) === byIndex) this.#waiters.delete(key);
|
|
714
|
+
resolve(published);
|
|
715
|
+
};
|
|
716
|
+
waiting.add(finish);
|
|
717
|
+
// No deadline is a wait that ends only on the publication: a timer given
|
|
718
|
+
// an infinite delay fires at once, which would turn the wait into a poll.
|
|
719
|
+
const timer = Number.isFinite(timeoutMs) ? setTimeout(() => finish(false), Math.max(0, timeoutMs)) : null;
|
|
720
|
+
timer?.unref?.();
|
|
721
|
+
cancelled?.then(() => finish(false));
|
|
722
|
+
// Publication may have landed after the first path check and before this
|
|
723
|
+
// waiter was registered. Recheck once after registration so that window
|
|
724
|
+
// cannot turn a present segment into a full-deadline wait.
|
|
725
|
+
if (this.pathOf(key, index)) finish(true);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
401
729
|
/**
|
|
402
730
|
* Clear up after a run that has ended: remove what it left unfinished.
|
|
403
731
|
*
|
|
@@ -437,6 +765,42 @@ export class SegmentStore {
|
|
|
437
765
|
}
|
|
438
766
|
|
|
439
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Where the file of this name is, or null when this output does not hold it.
|
|
770
|
+
*
|
|
771
|
+
* By NAME rather than by number, because that is what a request carries. The
|
|
772
|
+
* conversion is the store's own business: it is told how this output's files
|
|
773
|
+
* are named (`useFormat`) and therefore needs nobody else to read one.
|
|
774
|
+
*
|
|
775
|
+
* @param {string} key
|
|
776
|
+
* @param {string} fileName
|
|
777
|
+
* @returns {string | null}
|
|
778
|
+
*/
|
|
779
|
+
pathOfName(key, fileName) {
|
|
780
|
+
const format = this.#formats.get(key);
|
|
781
|
+
const index = format?.segmentIndexFromName?.(fileName) ?? -1;
|
|
782
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
return this.pathOf(key, index);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Forget what this output's directory was last seen to hold.
|
|
790
|
+
*
|
|
791
|
+
* What is held is re-read when the directory's modification time moves, which
|
|
792
|
+
* is how a quiet request costs one `stat`. A caller that has just removed a
|
|
793
|
+
* file and asks again in the same tick would otherwise be told it is still
|
|
794
|
+
* there — the time has moved, but only by as much as the clock's own
|
|
795
|
+
* granularity, and that is not something to depend on.
|
|
796
|
+
*
|
|
797
|
+
* @param {string} key
|
|
798
|
+
* @returns {void}
|
|
799
|
+
*/
|
|
800
|
+
forget(key) {
|
|
801
|
+
this.#held.delete(key);
|
|
802
|
+
}
|
|
803
|
+
|
|
440
804
|
/**
|
|
441
805
|
* Where a segment is, or null when this output does not hold it.
|
|
442
806
|
*
|
|
@@ -475,9 +839,11 @@ export class SegmentStore {
|
|
|
475
839
|
} catch {
|
|
476
840
|
// Already gone, or in use; the next sweep sees it either way.
|
|
477
841
|
}
|
|
842
|
+
this.#unwatch(key);
|
|
478
843
|
this.#held.delete(key);
|
|
479
844
|
this.#formats.delete(key);
|
|
480
845
|
this.#touched.delete(key);
|
|
846
|
+
this.#inits.delete(key);
|
|
481
847
|
this.#logger.info(`segment-store dropped ${directoryNameFor(key)} (${because})`);
|
|
482
848
|
}
|
|
483
849
|
|
|
@@ -496,12 +862,30 @@ export class SegmentStore {
|
|
|
496
862
|
}
|
|
497
863
|
|
|
498
864
|
/**
|
|
499
|
-
* Throw away everything this store owns
|
|
865
|
+
* Throw away everything this store owns.
|
|
500
866
|
*
|
|
501
867
|
* For a clean exit. What is left on disk afterwards is by definition from a
|
|
502
868
|
* kill, which is the case the startup sweep exists for — and without this the
|
|
503
869
|
* sweep adopts, the exit leaves, and the next start adopts again, for ever.
|
|
504
870
|
*
|
|
871
|
+
* WHAT IT OWNS IS ITS DIRECTORIES, NOT THE ROOT. The root is a fixed path —
|
|
872
|
+
* `os.tmpdir()/torrent-tv-hls` — so it belongs to the machine and not to any
|
|
873
|
+
* one process, and this used to remove it whole. The comment that stood here
|
|
874
|
+
* named the hazard exactly ("another process may share the root") and did it
|
|
875
|
+
* anyway.
|
|
876
|
+
*
|
|
877
|
+
* Measured 2026-09-14, where a second process is ordinary: `node --test` runs
|
|
878
|
+
* every test file in its own process in parallel, and four files call this
|
|
879
|
+
* forty times between them. `segment-serve-wiring.test.js` then reported a
|
|
880
|
+
* segment missing that it had written itself a moment earlier, 2 to 4 checks
|
|
881
|
+
* per run, never the same ones — because another process had deleted the
|
|
882
|
+
* directory under it. The same thing happens to two proxies on one machine:
|
|
883
|
+
* a bare npm run beside the addon, or the wedge stand beside the product, and
|
|
884
|
+
* one of them exiting takes the other's segments mid-serve.
|
|
885
|
+
*
|
|
886
|
+
* A store removes what it made. It does not remove the container it made it
|
|
887
|
+
* in, and an empty root costs nothing.
|
|
888
|
+
*
|
|
505
889
|
* @param {string} because
|
|
506
890
|
* @returns {number} How many outputs went.
|
|
507
891
|
*/
|
|
@@ -511,11 +895,14 @@ export class SegmentStore {
|
|
|
511
895
|
this.drop(key, because);
|
|
512
896
|
dropped += 1;
|
|
513
897
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
898
|
+
// EVERY WATCH GOES, not only the watches of outputs that named their file
|
|
899
|
+
// format. A directory is made before anything says how its files are named
|
|
900
|
+
// — `directoryFor` is what a run calls, `useFormat` is a separate statement
|
|
901
|
+
// — so a store can hold a watch on a key that is in no format list, and
|
|
902
|
+
// dropping by that list left it open: on Windows the process then does not
|
|
903
|
+
// exit, which is how this was found.
|
|
904
|
+
for (const key of [...this.#watchers.keys()]) {
|
|
905
|
+
this.#unwatch(key);
|
|
519
906
|
}
|
|
520
907
|
return dropped;
|
|
521
908
|
}
|