@torrent-tv/proxy 2.90.0 → 2.92.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/bin/cli.js +5 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/transport/tunnel-client.js +111 -20
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { setImmediate as nextTurn } from "node:timers/promises";
|
|
2
|
+
import { downloadCandidates, dispatchDownloadCandidates, peerCanServe, peerRequestCapacity, copyRequestArrival } from "./DeadlineScheduler.js";
|
|
3
|
+
|
|
4
|
+
const BLOCK_BYTES = 16 * 1024;
|
|
5
|
+
|
|
6
|
+
/** Replay the actual request scheduler against copied protocol state only. */
|
|
7
|
+
export async function futureDownload(entries, { now = Date.now(), findStore, replayOnly = false, isCurrent = () => true } = {}) {
|
|
8
|
+
if (!isCurrent()) return new Map();
|
|
9
|
+
const candidates = downloadCandidates(entries, findStore);
|
|
10
|
+
const copies = new Map();
|
|
11
|
+
let clock = now;
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
const source = candidate.torrent;
|
|
14
|
+
let copy = copies.get(source);
|
|
15
|
+
if (!copy) {
|
|
16
|
+
copy = { pieceLength: source.pieceLength, maxWebConns: source.maxWebConns, states: new Map(), wires: [], arrivals: new Map() };
|
|
17
|
+
copies.set(source, copy);
|
|
18
|
+
for (const peer of source.wires ?? []) {
|
|
19
|
+
if (peer.destroyed || (!peer.requests?.length && peer.peerChoking && !peer.hasFast)) continue;
|
|
20
|
+
const speed = Math.max(0, Number(peer.downloadSpeed?.()) || 0);
|
|
21
|
+
const available = new Set(candidates.filter(one => one.torrent === source && peer.peerPieces?.get(one.piece)).map(one => one.piece));
|
|
22
|
+
if (!available.size && !peer.requests?.length) continue;
|
|
23
|
+
const wire = { type: peer.type, destroyed: peer.destroyed, peerChoking: peer.peerChoking,
|
|
24
|
+
hasFast: peer.hasFast, peerAllowedFastSet: [...(peer.peerAllowedFastSet ?? [])], peerPieces: { get: piece => available.has(piece) },
|
|
25
|
+
peerExtendedHandshake: { reqq: peer.peerExtendedHandshake?.reqq }, downloadSpeed: () => speed,
|
|
26
|
+
requests: (peer.requests ?? []).map(request => copyRequestArrival(request,
|
|
27
|
+
{ piece: request.piece, offset: request.offset, length: request.length, remaining: request.length })) };
|
|
28
|
+
wire.cancel = (piece, offset, length) => {
|
|
29
|
+
const index = wire.requests.findIndex(request => request.piece === piece && request.offset === offset && request.length === length);
|
|
30
|
+
if (index < 0) return;
|
|
31
|
+
wire.requests.splice(index, 1);
|
|
32
|
+
const state = copy.states.get(piece);
|
|
33
|
+
for (let block = offset / BLOCK_BYTES; state && block * BLOCK_BYTES < offset + length; block++) {
|
|
34
|
+
state.reserved.delete(block);
|
|
35
|
+
state.cursor = Math.min(state.cursor, block);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
copy.wires.push(wire);
|
|
39
|
+
}
|
|
40
|
+
copy._request = (wire, piece) => {
|
|
41
|
+
const state = copy.states.get(piece);
|
|
42
|
+
if (!state || state.complete) return false;
|
|
43
|
+
let block = state.cursor;
|
|
44
|
+
while (block < state.count && (state.present.has(block) || state.reserved.has(block))) block++;
|
|
45
|
+
if (block === state.count) return false;
|
|
46
|
+
const offset = block * BLOCK_BYTES;
|
|
47
|
+
const length = wire.type === "webSeed" ? state.length - offset : Math.min(BLOCK_BYTES, state.length - offset);
|
|
48
|
+
for (let index = block; index * BLOCK_BYTES < offset + length; index++) state.reserved.add(index);
|
|
49
|
+
state.cursor = Math.ceil((offset + length) / BLOCK_BYTES);
|
|
50
|
+
wire.requests.push({ piece, offset, length, remaining: length });
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const piece = source.pieces?.[candidate.piece];
|
|
55
|
+
if (!piece || !Number.isSafeInteger(piece.length) || piece.length <= 0 ||
|
|
56
|
+
(piece.missing !== piece.length && !Array.isArray(piece._buffer))) {
|
|
57
|
+
copy.arrivals.set(candidate.piece, null);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const count = Math.ceil(piece.length / BLOCK_BYTES);
|
|
61
|
+
const present = new Set();
|
|
62
|
+
for (let block = 0; block < count; block++) if (piece._buffer?.[block]) present.add(block);
|
|
63
|
+
const reserved = new Set();
|
|
64
|
+
for (const wire of copy.wires) for (const request of wire.requests) {
|
|
65
|
+
if (request.piece !== candidate.piece) continue;
|
|
66
|
+
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) reserved.add(block);
|
|
67
|
+
}
|
|
68
|
+
copy.states.set(candidate.piece, { length: piece.length, count, present, reserved, cursor: 0, complete: false });
|
|
69
|
+
}
|
|
70
|
+
const ordered = candidates.filter(candidate => copies.get(candidate.torrent).states.has(candidate.piece))
|
|
71
|
+
.map(candidate => ({ ...candidate, torrent: copies.get(candidate.torrent) }));
|
|
72
|
+
dispatchDownloadCandidates(ordered, clock);
|
|
73
|
+
const solved = new Set();
|
|
74
|
+
if (!replayOnly) for (const copy of copies.values()) {
|
|
75
|
+
if (copy.wires.length === 1 && !copy.wires[0].destroyed) {
|
|
76
|
+
finishSinglePeer(copy, ordered.filter(candidate => candidate.torrent === copy), now);
|
|
77
|
+
solved.add(copy);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const replay = ordered.filter(candidate => !solved.has(candidate.torrent));
|
|
81
|
+
const pendingCandidates = new Set(replay);
|
|
82
|
+
const candidateByCopy = new Map();
|
|
83
|
+
for (const candidate of replay) {
|
|
84
|
+
let pieces = candidateByCopy.get(candidate.torrent);
|
|
85
|
+
if (!pieces) candidateByCopy.set(candidate.torrent, pieces = new Map());
|
|
86
|
+
pieces.set(candidate.piece, candidate);
|
|
87
|
+
}
|
|
88
|
+
const wires = [...copies.values()].filter(copy => !solved.has(copy)).flatMap(copy => copy.wires.map(wire => ({ copy, wire })));
|
|
89
|
+
let steps = 0;
|
|
90
|
+
while (pendingCandidates.size) {
|
|
91
|
+
dispatchDownloadCandidates([...pendingCandidates], clock);
|
|
92
|
+
let next = null, delay = Infinity;
|
|
93
|
+
for (const entry of wires) {
|
|
94
|
+
const { wire } = entry;
|
|
95
|
+
if (!wire.requests.length || !(wire.downloadSpeed() > 0)) continue;
|
|
96
|
+
const arrivesIn = wire.requests[0].remaining / wire.downloadSpeed() * 1000;
|
|
97
|
+
if (arrivesIn < delay) { delay = arrivesIn; next = entry; }
|
|
98
|
+
}
|
|
99
|
+
if (!next || !Number.isFinite(delay)) break;
|
|
100
|
+
clock += delay;
|
|
101
|
+
for (const { copy, wire } of wires) {
|
|
102
|
+
const request = wire.requests[0];
|
|
103
|
+
if (!request) continue;
|
|
104
|
+
request.remaining = Math.max(0, request.remaining - delay / 1000 * wire.downloadSpeed());
|
|
105
|
+
if (wire === next.wire) request.remaining = 0;
|
|
106
|
+
if (request.remaining > 0) continue;
|
|
107
|
+
wire.requests.shift();
|
|
108
|
+
const state = copy.states.get(request.piece);
|
|
109
|
+
if (!state || state.complete) continue;
|
|
110
|
+
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
|
|
111
|
+
state.present.add(block);
|
|
112
|
+
state.reserved.delete(block);
|
|
113
|
+
}
|
|
114
|
+
if (state.present.size === state.count) {
|
|
115
|
+
state.complete = true;
|
|
116
|
+
state.present.clear();
|
|
117
|
+
state.reserved.clear();
|
|
118
|
+
copy.arrivals.set(request.piece, clock);
|
|
119
|
+
pendingCandidates.delete(candidateByCopy.get(copy)?.get(request.piece));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (++steps % 256 === 0) {
|
|
123
|
+
await nextTurn();
|
|
124
|
+
if (!isCurrent()) return new Map();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return new Map([...copies].map(([torrent, copy]) => {
|
|
128
|
+
for (const piece of copy.states.keys()) if (!copy.arrivals.has(piece)) copy.arrivals.set(piece, null);
|
|
129
|
+
return [torrent, { measuredAt: now, arrivals: copy.arrivals }];
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** With one peer there is no reassignment: its FIFO consumes the common map
|
|
134
|
+
* order. Sum that unchanged order instead of replaying every block arrival. */
|
|
135
|
+
function finishSinglePeer(copy, candidates, now) {
|
|
136
|
+
const wire = copy.wires[0];
|
|
137
|
+
const speed = wire.downloadSpeed();
|
|
138
|
+
if (!(speed > 0) || !(peerRequestCapacity(copy, wire) > 0)) return;
|
|
139
|
+
let at = now;
|
|
140
|
+
for (const request of wire.requests) {
|
|
141
|
+
at += request.length / speed * 1000;
|
|
142
|
+
const state = copy.states.get(request.piece);
|
|
143
|
+
if (!state || state.complete) continue;
|
|
144
|
+
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
|
|
145
|
+
state.present.add(block);
|
|
146
|
+
state.reserved.delete(block);
|
|
147
|
+
}
|
|
148
|
+
if (state.present.size === state.count) {
|
|
149
|
+
state.complete = true;
|
|
150
|
+
state.present.clear();
|
|
151
|
+
state.reserved.clear();
|
|
152
|
+
copy.arrivals.set(request.piece, at);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const { piece } of candidates) {
|
|
156
|
+
const state = copy.states.get(piece);
|
|
157
|
+
if (state.complete || !peerCanServe(wire, piece)) continue;
|
|
158
|
+
let missing = 0;
|
|
159
|
+
for (let block = 0; block < state.count; block++) {
|
|
160
|
+
if (!state.present.has(block)) missing += Math.min(BLOCK_BYTES, state.length - block * BLOCK_BYTES);
|
|
161
|
+
}
|
|
162
|
+
at += missing / speed * 1000;
|
|
163
|
+
state.complete = true;
|
|
164
|
+
copy.arrivals.set(piece, at);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
|
-
isConditional,
|
|
27
26
|
piecesOf,
|
|
28
27
|
selectionPriority,
|
|
29
28
|
Urgency,
|
|
@@ -36,8 +35,6 @@ export class SwarmSelection {
|
|
|
36
35
|
#register;
|
|
37
36
|
/** What was last stated to the library, so a restatement can be a no-op. */
|
|
38
37
|
#stated = new Map();
|
|
39
|
-
/** Pieces this instance marked for displacement, so it clears only its own. */
|
|
40
|
-
#displacing = null;
|
|
41
38
|
/** Claimants whose windows are currently protected in memory. */
|
|
42
39
|
#protectedInMemory = new Set();
|
|
43
40
|
#findStore;
|
|
@@ -72,11 +69,8 @@ export class SwarmSelection {
|
|
|
72
69
|
* shared and the question is not a per-torrent one.
|
|
73
70
|
* @returns {{ stated: number, withdrawn: number }}
|
|
74
71
|
*/
|
|
75
|
-
reconcile(
|
|
76
|
-
const levels = this.#register.
|
|
77
|
-
(window) => this.#isSatisfied(window),
|
|
78
|
-
speculativeAllowed
|
|
79
|
-
);
|
|
72
|
+
reconcile() {
|
|
73
|
+
const levels = [...new Set(this.#register.windows().map(window => window.urgency))];
|
|
80
74
|
/** @type {Map<string, { from: number, to: number, priority: number }>} */
|
|
81
75
|
const wanted = new Map();
|
|
82
76
|
for (const urgency of levels) {
|
|
@@ -102,6 +96,8 @@ export class SwarmSelection {
|
|
|
102
96
|
withdrawn += 1;
|
|
103
97
|
}
|
|
104
98
|
|
|
99
|
+
this.#cancelUnwanted(wanted.values());
|
|
100
|
+
|
|
105
101
|
let stated = 0;
|
|
106
102
|
for (const [key, range] of wanted) {
|
|
107
103
|
// Re-stated when the library has dropped it, even though this instance
|
|
@@ -114,7 +110,6 @@ export class SwarmSelection {
|
|
|
114
110
|
stated += 1;
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
this.#markDisplacement();
|
|
118
113
|
this.#projectIntoMemory();
|
|
119
114
|
return { stated, withdrawn };
|
|
120
115
|
}
|
|
@@ -125,7 +120,7 @@ export class SwarmSelection {
|
|
|
125
120
|
this.#deselect(range);
|
|
126
121
|
}
|
|
127
122
|
this.#stated.clear();
|
|
128
|
-
this.#
|
|
123
|
+
this.#cancelUnwanted([]);
|
|
129
124
|
const store = this.#findStore(this.#torrent);
|
|
130
125
|
for (const claimant of this.#protectedInMemory) {
|
|
131
126
|
store?.releaseProtection?.(claimant);
|
|
@@ -133,6 +128,27 @@ export class SwarmSelection {
|
|
|
133
128
|
this.#protectedInMemory.clear();
|
|
134
129
|
}
|
|
135
130
|
|
|
131
|
+
/** Cancel removed blocks in this pass; the wire callback releases reservations. */
|
|
132
|
+
#cancelUnwanted(ranges) {
|
|
133
|
+
const wanted = [...ranges];
|
|
134
|
+
for (const wire of this.#torrent.wires ?? []) {
|
|
135
|
+
for (const request of [...(wire.requests ?? [])]) {
|
|
136
|
+
if (wanted.some((range) => request.piece >= range.from && request.piece <= range.to)) continue;
|
|
137
|
+
if (typeof wire.cancel !== "function") continue;
|
|
138
|
+
try {
|
|
139
|
+
wire.cancel(request.piece, request.offset, request.length);
|
|
140
|
+
this.#torrent.emit?.("download-request-cancelled", {
|
|
141
|
+
piece: request.piece, offset: request.offset, length: request.length, reason: "demand-withdrawn"
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this.#torrent.emit?.("download-request-cancel-failed", {
|
|
145
|
+
piece: request.piece, offset: request.offset, length: request.length, error: error?.message ?? String(error)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
/**
|
|
137
153
|
* What is stated right now.
|
|
138
154
|
*
|
|
@@ -171,30 +187,6 @@ export class SwarmSelection {
|
|
|
171
187
|
*
|
|
172
188
|
* @returns {void}
|
|
173
189
|
*/
|
|
174
|
-
#markDisplacement() {
|
|
175
|
-
const blocked = this.#register
|
|
176
|
-
.at(Urgency.BLOCKED)
|
|
177
|
-
.map((window) => this.#piecesFor(window))
|
|
178
|
-
.filter((range) => range !== null);
|
|
179
|
-
if (blocked.length === 0) {
|
|
180
|
-
this.#clearDisplacement();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const nearest = Math.min(...blocked.map((range) => range.from));
|
|
184
|
-
const furthest = Math.max(...blocked.map((range) => range.to));
|
|
185
|
-
if (this.#displacing && this.#displacing.from === nearest && this.#displacing.to === furthest) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
this.#clearDisplacement();
|
|
189
|
-
try {
|
|
190
|
-
this.#torrent.critical?.(nearest, furthest);
|
|
191
|
-
this.#displacing = { from: nearest, to: furthest };
|
|
192
|
-
} catch {
|
|
193
|
-
// silent-ok: displacement is an optimisation, and a torrent being torn
|
|
194
|
-
// down is not worth failing a read over.
|
|
195
|
-
this.#displacing = null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
190
|
|
|
199
191
|
/**
|
|
200
192
|
* Tell the piece store which bytes will be read soon, from the same stated
|
|
@@ -234,7 +226,7 @@ export class SwarmSelection {
|
|
|
234
226
|
return;
|
|
235
227
|
}
|
|
236
228
|
const holding = new Set();
|
|
237
|
-
for (const urgency of [
|
|
229
|
+
for (const urgency of [...new Set(this.#register.windows().map(window => window.urgency))]) {
|
|
238
230
|
for (const window of this.#register.at(urgency)) {
|
|
239
231
|
const range = this.#piecesFor(window);
|
|
240
232
|
if (!range) {
|
|
@@ -243,7 +235,7 @@ export class SwarmSelection {
|
|
|
243
235
|
// The level goes with the range: it is what eviction compares when
|
|
244
236
|
// everything resident is wanted by somebody, and dropping it here is
|
|
245
237
|
// what left the store choosing by recency alone.
|
|
246
|
-
store.protectRange(window.claimant, range.from, range.to, window.urgency);
|
|
238
|
+
store.protectRange(window.claimant, range.from, range.to, window.urgency, window.deadlineAt, window.priority);
|
|
247
239
|
holding.add(window.claimant);
|
|
248
240
|
}
|
|
249
241
|
}
|
|
@@ -255,17 +247,6 @@ export class SwarmSelection {
|
|
|
255
247
|
this.#protectedInMemory = holding;
|
|
256
248
|
}
|
|
257
249
|
|
|
258
|
-
/** @returns {void} */
|
|
259
|
-
#clearDisplacement() {
|
|
260
|
-
if (!this.#displacing || !Array.isArray(this.#torrent._critical)) {
|
|
261
|
-
this.#displacing = null;
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
for (let index = this.#displacing.from; index <= this.#displacing.to; index += 1) {
|
|
265
|
-
this.#torrent._critical[index] = false;
|
|
266
|
-
}
|
|
267
|
-
this.#displacing = null;
|
|
268
|
-
}
|
|
269
250
|
|
|
270
251
|
/**
|
|
271
252
|
* The pieces a window covers, or null when the file or the torrent cannot
|
|
@@ -363,24 +344,14 @@ export class SwarmSelection {
|
|
|
363
344
|
*
|
|
364
345
|
* @returns {string}
|
|
365
346
|
*/
|
|
366
|
-
describe(
|
|
367
|
-
const stating = this.#register.levelsToState(
|
|
368
|
-
(window) => this.#isSatisfied(window),
|
|
369
|
-
speculativeAllowed
|
|
370
|
-
);
|
|
371
|
-
const speculative = stating.filter((urgency) => isConditional(urgency)).map(urgencyName);
|
|
347
|
+
describe() {
|
|
372
348
|
const needs = this.#register
|
|
373
349
|
.windows()
|
|
374
350
|
.map((window) => `${urgencyName(window.urgency)}:${window.claimant}`);
|
|
375
351
|
return (
|
|
376
352
|
`download: ${this.#stated.size} instruction(s) to the swarm from ` +
|
|
377
353
|
`${this.#register.size} stated need(s) [${needs.join(" ")}]` +
|
|
378
|
-
|
|
379
|
-
? `; ${speculative.join(" and ")} also stated — nothing urgent is missing`
|
|
380
|
-
: "; nothing speculative is stated — something urgent is still missing") +
|
|
381
|
-
(this.#displacing
|
|
382
|
-
? `; pieces ${this.#displacing.from}-${this.#displacing.to} may be taken from slow peers`
|
|
383
|
-
: "")
|
|
354
|
+
"; peer requests ordered by deadline and priority"
|
|
384
355
|
);
|
|
385
356
|
}
|
|
386
357
|
}
|
|
@@ -17,11 +17,31 @@
|
|
|
17
17
|
|
|
18
18
|
import { DemandRegister } from "../demand/DemandRegister.js";
|
|
19
19
|
import { SwarmSelection } from "./SwarmSelection.js";
|
|
20
|
+
import { DeadlineScheduler } from "./DeadlineScheduler.js";
|
|
21
|
+
import { futureDownload } from "./FutureDownload.js";
|
|
20
22
|
|
|
21
23
|
/** @type {WeakMap<object, { register: DemandRegister, selection: SwarmSelection }>} */
|
|
22
24
|
const byTorrent = new WeakMap();
|
|
23
25
|
/** @type {Set<{ register: DemandRegister, selection: SwarmSelection }>} */
|
|
24
26
|
const live = new Set();
|
|
27
|
+
const scheduler = new DeadlineScheduler({ entries: () => live });
|
|
28
|
+
let futurePending = null;
|
|
29
|
+
let publishingSelections = false;
|
|
30
|
+
|
|
31
|
+
/** Concurrent file reports share one snapshot of every live download map. */
|
|
32
|
+
export function forecastDownloads() {
|
|
33
|
+
if (!futurePending) {
|
|
34
|
+
const entries = [...live];
|
|
35
|
+
const revisions = entries.map(entry => entry.register.revision);
|
|
36
|
+
const withdrawals = entries.map(entry => entry.withdrawalRevision);
|
|
37
|
+
const isCurrent = () => entries.length === live.size && !entries.some((entry, index) =>
|
|
38
|
+
!live.has(entry) || entry.register.revision !== revisions[index] ||
|
|
39
|
+
entry.withdrawalRevision !== withdrawals[index]);
|
|
40
|
+
futurePending = futureDownload(entries, { isCurrent }).then(result =>
|
|
41
|
+
isCurrent() ? result : new Map()).finally(() => { futurePending = null; });
|
|
42
|
+
}
|
|
43
|
+
return futurePending;
|
|
44
|
+
}
|
|
25
45
|
|
|
26
46
|
/**
|
|
27
47
|
* The register and selection for a torrent, made on first use.
|
|
@@ -35,9 +55,19 @@ export function demandFor(torrent) {
|
|
|
35
55
|
return held;
|
|
36
56
|
}
|
|
37
57
|
const register = new DemandRegister();
|
|
38
|
-
const entry = { register, selection: new SwarmSelection({ torrent, register }) };
|
|
58
|
+
const entry = { torrent, register, selection: new SwarmSelection({ torrent, register }), withdrawalRevision: 0 };
|
|
59
|
+
if (typeof torrent._request === "function" && typeof torrent._updateWire === "function") {
|
|
60
|
+
entry.previousWireUpdate = torrent._updateWire;
|
|
61
|
+
torrent._updateWire = () => {
|
|
62
|
+
if (!publishingSelections) scheduler.reconcile();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
39
65
|
byTorrent.set(torrent, entry);
|
|
40
66
|
live.add(entry);
|
|
67
|
+
entry.onBytesChanged = () => reconcileAll();
|
|
68
|
+
entry.onWithdrawn = () => { entry.withdrawalRevision++; reconcileAll(); };
|
|
69
|
+
torrent.on?.("verified", entry.onBytesChanged);
|
|
70
|
+
torrent.on?.("piece-withdrawn", entry.onWithdrawn);
|
|
41
71
|
return entry;
|
|
42
72
|
}
|
|
43
73
|
|
|
@@ -74,6 +104,9 @@ export function forgetTorrent(torrent) {
|
|
|
74
104
|
}
|
|
75
105
|
held.selection.releaseAll();
|
|
76
106
|
held.register.clear();
|
|
107
|
+
torrent.removeListener?.("verified", held.onBytesChanged);
|
|
108
|
+
torrent.removeListener?.("piece-withdrawn", held.onWithdrawn);
|
|
109
|
+
if (held.previousWireUpdate) torrent._updateWire = held.previousWireUpdate;
|
|
77
110
|
byTorrent.delete(torrent);
|
|
78
111
|
live.delete(held);
|
|
79
112
|
}
|
|
@@ -89,14 +122,21 @@ export function forgetTorrent(torrent) {
|
|
|
89
122
|
* @returns {{ torrents: number, speculativeAllowed: boolean, stated: number, withdrawn: number }}
|
|
90
123
|
*/
|
|
91
124
|
export function reconcileAll() {
|
|
125
|
+
if (publishingSelections) return { torrents: live.size, speculativeAllowed: false, stated: 0, withdrawn: 0 };
|
|
92
126
|
const entries = [...live];
|
|
93
127
|
const speculativeAllowed = !entries.some((entry) => entry.selection.hasUrgentMissing());
|
|
94
128
|
let stated = 0;
|
|
95
129
|
let withdrawn = 0;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
publishingSelections = true;
|
|
131
|
+
try {
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const result = entry.selection.reconcile({ speculativeAllowed, deadlineDriven: !!entry.previousWireUpdate });
|
|
134
|
+
stated += result.stated;
|
|
135
|
+
withdrawn += result.withdrawn;
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
publishingSelections = false;
|
|
100
139
|
}
|
|
140
|
+
scheduler.reconcile();
|
|
101
141
|
return { torrents: entries.length, speculativeAllowed, stated, withdrawn };
|
|
102
142
|
}
|
|
@@ -76,5 +76,7 @@ export function withdrawClaim({ index, files, torrent, warn = () => undefined })
|
|
|
76
76
|
);
|
|
77
77
|
return "refused";
|
|
78
78
|
}
|
|
79
|
+
try { owner.emit?.("piece-withdrawn", index); }
|
|
80
|
+
catch (error) { warn(`piece withdrawal listener failed: ${error?.message ?? error}`); }
|
|
79
81
|
return "withdrawn";
|
|
80
82
|
}
|