@torrent-tv/proxy 2.95.3 → 2.95.4
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/package.json +1 -1
- package/routes/encode-input/get.js +29 -0
- package/server.js +55 -10
- package/services/encode/EncodeInputs.js +8 -2
- package/services/encode/EncodeRuns.js +21 -3
- package/services/encode/OriginalInput.js +59 -0
- package/services/encode/admitted-command.js +8 -2
- package/services/encode/output/Timeline.js +4 -0
- package/services/encode/piece-completeness.js +6 -2
- package/services/encode/run-command.js +5 -5
- package/services/encode/segment-formats/fmp4.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +26 -2
- package/services/encode/source-command.js +239 -0
- package/services/media/ContainerOrchestrator.js +7 -1
- package/services/media/container/MatroskaContainer.js +79 -0
- package/services/media/container/ffprobe-record.js +2 -0
- package/services/server/SegmentServing.js +3 -1
- package/services/server/wire-outputs.js +2 -1
- package/services/storage/piece-store/shared-piece-store.js +8 -1
- package/services/torrent/download/FutureDownload.js +98 -150
- package/services/torrent/download/SwarmSelection.js +45 -33
- package/services/torrent/download/bands.js +5 -0
- package/services/torrent/download/registry.js +5 -11
- package/services/viewer/DownloadMaps.js +1 -1
- package/services/viewer/NativeSourceMap.js +25 -0
- package/services/viewer/PriorityMap.js +1 -0
- package/services/viewer/playback-readiness.js +61 -19
- package/services/viewer/segment-demands.js +6 -2
- package/services/torrent/download/DeadlineScheduler.js +0 -195
|
@@ -208,6 +208,8 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
208
208
|
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
209
209
|
const state = trackState[trackIndex];
|
|
210
210
|
const { track, segments } = state;
|
|
211
|
+
const processingCoverage = numericCoverage((Array.isArray(track.processingRanges) ? track.processingRanges : [])
|
|
212
|
+
.filter(range => Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
|
|
211
213
|
const measuredBitsPerMediaSecond = state.bitsPerMediaSecond;
|
|
212
214
|
for (const segment of segments) {
|
|
213
215
|
// Media the page already holds is not transferred again.
|
|
@@ -230,9 +232,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
230
232
|
// Attribute unfinished work to the actual run interval. A global
|
|
231
233
|
// position from another run must not erase a missing earlier segment.
|
|
232
234
|
const workStart = segment.startSeconds;
|
|
233
|
-
const remainingProcessing = uncoveredIntervals({ start: workStart, end: segment.endSeconds },
|
|
234
|
-
(Array.isArray(track.processingRanges) ? track.processingRanges : []).filter((range) =>
|
|
235
|
-
Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
|
|
235
|
+
const remainingProcessing = uncoveredIntervals({ start: workStart, end: segment.endSeconds }, processingCoverage);
|
|
236
236
|
const encodeWork = produced ? 0 : remainingProcessing.reduce((sum, part) => sum + part.end - part.start, 0);
|
|
237
237
|
|
|
238
238
|
const sourceByteIntervals = new Map();
|
|
@@ -309,29 +309,29 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
const trackFinish = trackState.map(({ track }) => finiteNonNegative(track.startupRemainingSeconds));
|
|
312
|
+
const sourceByteCoverage = new Map([...sourceByteService].map(([id, ranges]) => [id, numericCoverage(ranges)]));
|
|
313
|
+
const sourceArrivals = new Map([...sourceByteService].map(([id, ranges]) => [id, indexedArrivals(ranges)]));
|
|
312
314
|
let transferFinish = 0;
|
|
313
315
|
const completions = [];
|
|
314
316
|
const productions = [];
|
|
315
317
|
for (const job of jobs) {
|
|
316
318
|
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
|
|
317
|
-
|
|
319
|
+
let sourceArrivalAt = 0;
|
|
318
320
|
for (const [sourceId, ranges] of sourceByteIntervals) {
|
|
319
|
-
const served = sourceByteService.get(sourceId) ?? [];
|
|
320
321
|
for (const interval of ranges) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
const missing = uncoveredIntervals(interval, sourceByteCoverage.get(sourceId) ?? []);
|
|
323
|
+
if (missing.length) return { ...result(false, null, buffered, reserve, null, "download-schedule-unavailable", preparedSegments),
|
|
324
|
+
unavailableSource: { sourceId, segmentIndex: segment.index, range: interval, missing } };
|
|
325
|
+
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(interval, sourceArrivals.get(sourceId)));
|
|
325
326
|
}
|
|
326
|
-
sourceByteService.set(sourceId, served);
|
|
327
327
|
}
|
|
328
328
|
// Production accepts a complete held segment input. It cannot overlap its
|
|
329
329
|
// own source download; reused input retains its original arrival time.
|
|
330
330
|
const processing = trackState[trackIndex].curve;
|
|
331
|
-
const admittedAt = Math.max(trackFinish[trackIndex],
|
|
331
|
+
const admittedAt = Math.max(trackFinish[trackIndex], sourceArrivalAt);
|
|
332
332
|
const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, admittedAt);
|
|
333
333
|
trackFinish[trackIndex] = Math.max(trackFinish[trackIndex], producedAt);
|
|
334
|
-
productions.push({ at: producedAt, trackIndex, segment, requiresSource:
|
|
334
|
+
productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivalAt > 0 });
|
|
335
335
|
const deliveryAt = link.finish(sizeBits, Math.max(transferFinish, producedAt));
|
|
336
336
|
transferFinish = deliveryAt;
|
|
337
337
|
completions.push({ at: deliveryAt + finiteNonNegative(track.appendSeconds), trackIndex, segment });
|
|
@@ -483,15 +483,57 @@ function heldRanges(ranges, segments, unit) {
|
|
|
483
483
|
return held;
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
+
/** Coverage is joined once per resource, independently of arrival times. */
|
|
487
|
+
function numericCoverage(ranges) {
|
|
488
|
+
const joined = [];
|
|
489
|
+
for (const range of [...ranges].sort((left, right) => left.start - right.start || left.end - right.end)) {
|
|
490
|
+
const previous = joined.at(-1);
|
|
491
|
+
if (previous && range.start <= previous.end) previous.end = Math.max(previous.end, range.end);
|
|
492
|
+
else joined.push({ start: range.start, end: range.end });
|
|
493
|
+
}
|
|
494
|
+
return joined;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Already held bytes have no arrival delay. Index future arrivals once. */
|
|
498
|
+
function indexedArrivals(ranges) {
|
|
499
|
+
let maximumEnd = 0;
|
|
500
|
+
return ranges.filter(range => range.finish > 0)
|
|
501
|
+
.sort((left, right) => left.start - right.start || left.end - right.end)
|
|
502
|
+
.map(range => ({ ...range, maximumEnd: maximumEnd = Math.max(maximumEnd, range.end) }));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function latestArrival(interval, ranges = []) {
|
|
506
|
+
let low = 0, high = ranges.length;
|
|
507
|
+
while (low < high) {
|
|
508
|
+
const middle = (low + high) >>> 1;
|
|
509
|
+
if (ranges[middle].maximumEnd <= interval.start) low = middle + 1;
|
|
510
|
+
else high = middle;
|
|
511
|
+
}
|
|
512
|
+
let finish = 0;
|
|
513
|
+
for (let index = low; index < ranges.length && ranges[index].start < interval.end; index++) {
|
|
514
|
+
if (ranges[index].end > interval.start) finish = Math.max(finish, ranges[index].finish);
|
|
515
|
+
}
|
|
516
|
+
return finish;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Exact gaps in sorted disjoint numeric coverage, without per-range splitting. */
|
|
486
520
|
function uncoveredIntervals(interval, served) {
|
|
487
|
-
let
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
521
|
+
let low = 0, high = served.length;
|
|
522
|
+
while (low < high) {
|
|
523
|
+
const middle = (low + high) >>> 1;
|
|
524
|
+
if (served[middle].end <= interval.start) low = middle + 1;
|
|
525
|
+
else high = middle;
|
|
526
|
+
}
|
|
527
|
+
const missing = [];
|
|
528
|
+
let cursor = interval.start;
|
|
529
|
+
for (let index = low; index < served.length && cursor < interval.end; index++) {
|
|
530
|
+
const held = served[index];
|
|
531
|
+
if (held.start >= interval.end) break;
|
|
532
|
+
if (held.start > cursor) missing.push({ start: cursor, end: held.start });
|
|
533
|
+
cursor = Math.max(cursor, held.end);
|
|
493
534
|
}
|
|
494
|
-
|
|
535
|
+
if (cursor < interval.end) missing.push({ start: cursor, end: interval.end });
|
|
536
|
+
return missing;
|
|
495
537
|
}
|
|
496
538
|
|
|
497
539
|
/**
|
|
@@ -16,13 +16,17 @@ export function segmentDemands(output, fileIndex, zones) {
|
|
|
16
16
|
if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
|
|
17
17
|
const overlapping = zones.filter(zone => zone.priority > 0 && zone.from < to && zone.to > from);
|
|
18
18
|
if (!overlapping.length) continue;
|
|
19
|
-
|
|
19
|
+
// A zone states the deadline of its near edge. A later segment within it
|
|
20
|
+
// is reached later, at one second of film per second of playback.
|
|
21
|
+
const deadlineOf = zone => Number.isFinite(zone.deadlineAt)
|
|
22
|
+
? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
|
|
23
|
+
const owner = overlapping.reduce((first, zone) => deadlineOf(zone) < deadlineOf(first) ? zone : first);
|
|
20
24
|
demands.push({ index, from, to, tracks, owner,
|
|
21
25
|
priority: Math.max(...overlapping.map(zone => zone.priority)),
|
|
22
26
|
urgent: overlapping.some(zone => zone.urgent),
|
|
23
27
|
behind: overlapping.every(zone => zone.behind),
|
|
24
28
|
deferred: overlapping.every(zone => zone.deferred),
|
|
25
|
-
deadlineAt: Math.min(...overlapping.map(
|
|
29
|
+
deadlineAt: Math.min(...overlapping.map(deadlineOf)) });
|
|
26
30
|
}
|
|
27
31
|
return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
|
|
28
32
|
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { piecesOf } from "../demand/pieces.js";
|
|
2
|
-
import { pieceStoreOf } from "../piece-store-of.js";
|
|
3
|
-
|
|
4
|
-
/** The block length and one-second request allowance used by WebTorrent. */
|
|
5
|
-
const BLOCK_BYTES = 16 * 1024;
|
|
6
|
-
const compiledDemand = new WeakMap();
|
|
7
|
-
const requestArrivals = new WeakMap();
|
|
8
|
-
|
|
9
|
-
/** Keep the same reassignment evidence in an isolated forecast request. */
|
|
10
|
-
export function copyRequestArrival(source, target) {
|
|
11
|
-
if (requestArrivals.has(source)) requestArrivals.set(target, requestArrivals.get(source));
|
|
12
|
-
return target;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function requestArrival(wire, request, now) {
|
|
16
|
-
if (!requestArrivals.has(request)) {
|
|
17
|
-
const speed = Number(wire.downloadSpeed?.());
|
|
18
|
-
let bytes = 0;
|
|
19
|
-
for (const queued of wire.requests ?? []) {
|
|
20
|
-
bytes += queued.length;
|
|
21
|
-
if (queued === request) break;
|
|
22
|
-
}
|
|
23
|
-
requestArrivals.set(request, speed > 0 ? now + bytes / speed * 1000 : Infinity);
|
|
24
|
-
}
|
|
25
|
-
return requestArrivals.get(request);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function peerRequestCapacity(torrent, wire, speed = Math.max(0, Number(wire.downloadSpeed?.()) || 0)) {
|
|
29
|
-
if (wire.type === "webSeed") {
|
|
30
|
-
return Math.min(1 + Math.ceil(speed / torrent.pieceLength), torrent.maxWebConns);
|
|
31
|
-
}
|
|
32
|
-
let capacity = 2 + Math.ceil(speed / BLOCK_BYTES);
|
|
33
|
-
const maximum = wire.peerExtendedHandshake?.reqq;
|
|
34
|
-
if (Number.isFinite(maximum) && maximum > 0) capacity = Math.min(capacity, Math.floor(maximum));
|
|
35
|
-
return capacity;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function requestCompletionAt(wire, bytes, now, speed = Number(wire.downloadSpeed?.())) {
|
|
39
|
-
if (!(speed > 0)) return Number.POSITIVE_INFINITY;
|
|
40
|
-
const queued = (wire.requests ?? []).reduce((total, request) => total + request.length, 0);
|
|
41
|
-
return now + (queued + bytes) / speed * 1000;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function peerCanServe(wire, piece) {
|
|
45
|
-
return !wire.destroyed && (!wire.peerChoking || (wire.hasFast && wire.peerAllowedFastSet?.includes(piece))) &&
|
|
46
|
-
wire.peerPieces?.get(piece);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function peerCanRequest(wire) {
|
|
50
|
-
return !wire.destroyed &&
|
|
51
|
-
(!wire.peerChoking || (wire.hasFast && wire.peerAllowedFastSet?.length > 0));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Identical map ordering for present requests and conditional future requests. */
|
|
55
|
-
export function compareDownloadCandidates(a, b) {
|
|
56
|
-
return a.deadlineAt - b.deadlineAt || b.priority - a.priority || a.order - b.order || a.piece - b.piece;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** One ordering across torrents; peers retain protocol-owned block verification. */
|
|
60
|
-
export class DeadlineScheduler {
|
|
61
|
-
#entries;
|
|
62
|
-
#running = false;
|
|
63
|
-
#store;
|
|
64
|
-
|
|
65
|
-
constructor({ entries, findStore = pieceStoreOf }) {
|
|
66
|
-
this.#entries = entries;
|
|
67
|
-
this.#store = findStore;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
reconcile(now = Date.now(), { torrent = null, wire = null } = {}) {
|
|
71
|
-
if (this.#running) return { requested: 0, reassigned: 0 };
|
|
72
|
-
// WebTorrent updates each peer in turn. A full or choked peer cannot issue
|
|
73
|
-
// a request, so its heartbeat must not scan every file's byte availability.
|
|
74
|
-
if (wire && (!peerCanRequest(wire) ||
|
|
75
|
-
(wire.requests?.length ?? 0) >= peerRequestCapacity(torrent, wire))) {
|
|
76
|
-
return { requested: 0, reassigned: 0 };
|
|
77
|
-
}
|
|
78
|
-
this.#running = true;
|
|
79
|
-
try {
|
|
80
|
-
const entries = [...this.#entries()].filter(entry => !torrent || entry.torrent === torrent);
|
|
81
|
-
return dispatchDownloadCandidates(downloadCandidates(entries, this.#store), now, { wire });
|
|
82
|
-
} finally {
|
|
83
|
-
this.#running = false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/** The published map defines the same candidates for every scheduling pass. */
|
|
90
|
-
export function downloadCandidates(entries, findStore = pieceStoreOf) {
|
|
91
|
-
const candidates = [];
|
|
92
|
-
for (const { torrent, register } of entries) {
|
|
93
|
-
if (torrent.destroyed || torrent.ready === false || typeof torrent._request !== "function") continue;
|
|
94
|
-
const store = findStore(torrent);
|
|
95
|
-
let compiled = compiledDemand.get(register);
|
|
96
|
-
if (!Number.isSafeInteger(register.revision) || compiled?.revision !== register.revision ||
|
|
97
|
-
compiled.torrent !== torrent || compiled.files !== torrent.files || compiled.pieceLength !== torrent.pieceLength) {
|
|
98
|
-
const byPiece = new Map();
|
|
99
|
-
for (const window of register.windows()) {
|
|
100
|
-
const file = torrent.files?.[window.fileIndex];
|
|
101
|
-
if (!file) continue;
|
|
102
|
-
const range = piecesOf({ fileOffset: file.offset, byteStart: window.byteStart, byteEnd: Math.min(window.byteEnd, file.length - 1), pieceLength: torrent.pieceLength });
|
|
103
|
-
if (!range) continue;
|
|
104
|
-
for (let piece = range.from; piece <= range.to; piece++) {
|
|
105
|
-
const prior = byPiece.get(piece);
|
|
106
|
-
if (!prior || window.deadlineAt < prior.deadlineAt ||
|
|
107
|
-
(window.deadlineAt === prior.deadlineAt && (window.priority > prior.priority ||
|
|
108
|
-
(window.priority === prior.priority && window.order < prior.order)))) {
|
|
109
|
-
byPiece.set(piece, { torrent, piece, deadlineAt: window.deadlineAt, priority: window.priority, order: window.order, requestId: window.requestId });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
compiled = { torrent, revision: register.revision, files: torrent.files,
|
|
114
|
-
pieceLength: torrent.pieceLength, pieces: [...byPiece.values()] };
|
|
115
|
-
compiledDemand.set(register, compiled);
|
|
116
|
-
}
|
|
117
|
-
for (const candidate of compiled.pieces) {
|
|
118
|
-
const present = store?.locationOf ? store.locationOf(candidate.piece) !== "missing" : torrent.bitfield?.get(candidate.piece);
|
|
119
|
-
if (!present) candidates.push(candidate);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
candidates.sort(compareDownloadCandidates);
|
|
123
|
-
return candidates;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Run against protocol peers or an isolated future-state copy. */
|
|
127
|
-
export function dispatchDownloadCandidates(candidates, now, { wire: changedWire = null } = {}) {
|
|
128
|
-
let requested = 0, reassigned = 0;
|
|
129
|
-
// Rates and advertised limits cannot change during this synchronous pass.
|
|
130
|
-
// Read each peer once, and only inspect torrents once regardless of map size.
|
|
131
|
-
const readings = new Map();
|
|
132
|
-
const readingOf = (torrent, wire) => {
|
|
133
|
-
let reading = readings.get(wire);
|
|
134
|
-
if (!reading) {
|
|
135
|
-
const speed = Math.max(0, Number(wire.downloadSpeed?.()) || 0);
|
|
136
|
-
readings.set(wire, reading = { speed, capacity: peerRequestCapacity(torrent, wire, speed) });
|
|
137
|
-
}
|
|
138
|
-
return reading;
|
|
139
|
-
};
|
|
140
|
-
const available = (torrent, wire) => peerCanRequest(wire) &&
|
|
141
|
-
(wire.requests?.length ?? 0) < readingOf(torrent, wire).capacity;
|
|
142
|
-
const completionAt = (torrent, wire, bytes) =>
|
|
143
|
-
requestCompletionAt(wire, bytes, now, readingOf(torrent, wire).speed);
|
|
144
|
-
const writable = new Map();
|
|
145
|
-
for (const torrent of new Set(candidates.map(candidate => candidate.torrent))) {
|
|
146
|
-
const peers = (changedWire ? [changedWire] : torrent.wires ?? [])
|
|
147
|
-
.filter(wire => available(torrent, wire));
|
|
148
|
-
if (peers.length) writable.set(torrent, peers);
|
|
149
|
-
}
|
|
150
|
-
for (const candidate of candidates) {
|
|
151
|
-
if (!writable.size) break;
|
|
152
|
-
const { torrent, piece, deadlineAt, requestId } = candidate;
|
|
153
|
-
if (!writable.has(torrent)) continue;
|
|
154
|
-
const peers = writable.get(torrent);
|
|
155
|
-
const wires = peers.filter(wire => peerCanServe(wire, piece) && available(torrent, wire));
|
|
156
|
-
while (wires.length) {
|
|
157
|
-
wires.sort((a, b) => completionAt(torrent, a, BLOCK_BYTES) - completionAt(torrent, b, BLOCK_BYTES) ||
|
|
158
|
-
(a.requests?.length ?? 0) - (b.requests?.length ?? 0));
|
|
159
|
-
const wire = wires[0];
|
|
160
|
-
const before = wire.requests?.length ?? 0;
|
|
161
|
-
let accepted = torrent._request(wire, piece, false);
|
|
162
|
-
if (!accepted && Number.isFinite(deadlineAt)) {
|
|
163
|
-
const fasterAt = completionAt(torrent, wire, BLOCK_BYTES);
|
|
164
|
-
for (const slower of torrent.wires ?? []) {
|
|
165
|
-
if (slower === wire || typeof slower.cancel !== "function") continue;
|
|
166
|
-
const lateAt = completionAt(torrent, slower, 0);
|
|
167
|
-
if (!Number.isFinite(lateAt) || !(lateAt > deadlineAt && fasterAt < lateAt)) continue;
|
|
168
|
-
const request = slower.requests?.find(one => one.piece === piece);
|
|
169
|
-
if (!request) continue;
|
|
170
|
-
// A later queue estimate must not move the same block between peers
|
|
171
|
-
// indefinitely. Reassignment must improve its previous arrival;
|
|
172
|
-
// protocol completion or timeout ends this request's evidence.
|
|
173
|
-
if (!(fasterAt < requestArrival(slower, request, now))) continue;
|
|
174
|
-
slower.cancel(piece, request.offset, request.length);
|
|
175
|
-
reassigned++;
|
|
176
|
-
torrent.emit?.("download-request-cancelled", { ...request, reason: "deadline-reassigned", requestId });
|
|
177
|
-
accepted = torrent._request(wire, piece, false);
|
|
178
|
-
if (accepted) {
|
|
179
|
-
const replacement = wire.requests?.find(one => one.piece === piece && one.offset === request.offset);
|
|
180
|
-
if (replacement) requestArrivals.set(replacement, fasterAt);
|
|
181
|
-
}
|
|
182
|
-
if (accepted) break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (accepted) {
|
|
186
|
-
requested++;
|
|
187
|
-
torrent.emit?.("download-requested", { piece, deadlineAt, requestId, peer: wire.remoteAddress, queued: wire.requests?.length ?? 0 });
|
|
188
|
-
}
|
|
189
|
-
if (!accepted || (wire.requests?.length ?? 0) <= before ||
|
|
190
|
-
(wire.requests?.length ?? 0) >= readingOf(torrent, wire).capacity) wires.shift();
|
|
191
|
-
}
|
|
192
|
-
if (!peers.some(wire => available(torrent, wire))) writable.delete(torrent);
|
|
193
|
-
}
|
|
194
|
-
return { requested, reassigned };
|
|
195
|
-
}
|