@torrent-tv/proxy 2.101.4 → 2.101.5
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
CHANGED
|
@@ -828,7 +828,9 @@ export class ViewerRequests {
|
|
|
828
828
|
logger.info(`playback held ${session.id} consumer=${consumerId}: delay ${seconds(delay)}s by ` +
|
|
829
829
|
(holding.kind === "position-not-held" ? "the position, which nothing buffered covers" :
|
|
830
830
|
`the ${holding.kind} of ${holding.track} #${holding.segment} in ${seconds(holding.atSeconds)}s against ` +
|
|
831
|
-
`${seconds(holding.aheadSeconds)}s of media before it${holding.requiresSource ? ", its input still downloading" : ""}`
|
|
831
|
+
`${seconds(holding.aheadSeconds)}s of media before it${holding.requiresSource ? ", its input still downloading" : ""}` +
|
|
832
|
+
(Number.isFinite(holding.sourceArrivalSeconds) ? ` until ${seconds(holding.sourceArrivalSeconds)}s by ` +
|
|
833
|
+
(holding.sourcePlanned ? "the torrent's queues" : "the measured download rate") : "")) +
|
|
832
834
|
`; buffered ${seconds(forecast.bufferedSeconds)}s; rates ${rates.join(", ") || "none"}; download ${downloads.join(", ") || "none"}`);
|
|
833
835
|
}
|
|
834
836
|
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import { setImmediate as nextTurn } from "node:timers/promises";
|
|
2
2
|
import { piecesOf } from "../demand/pieces.js";
|
|
3
3
|
import { pieceStoreOf } from "../piece-store-of.js";
|
|
4
|
-
import { compareBands } from "./bands.js";
|
|
5
4
|
|
|
6
5
|
const BLOCK_BYTES = 16 * 1024;
|
|
7
6
|
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* requests are changed.
|
|
7
|
+
/** When the pieces whose every missing block is asked for arrive, from the
|
|
8
|
+
* queues that hold them. A block is reserved to the wire it was asked of, so a
|
|
9
|
+
* faster peer elsewhere does not bring it sooner (WebTorrent 2.8.5 `_request`,
|
|
10
|
+
* torrent.js:1758). Every other missing piece is left unknown: the start
|
|
11
|
+
* forecast brings those at the source's measured service, in its own media
|
|
12
|
+
* order (`viewer/playback-readiness.js`), one model of supply rather than a
|
|
13
|
+
* second one here (torrent-tv/meta#185). No requests are changed.
|
|
15
14
|
* Times are conditional on the measured rate and successful verification.
|
|
16
15
|
*/
|
|
17
16
|
export async function futureDownload(entries, { now = Date.now(), findStore = pieceStoreOf, isCurrent = () => true } = {}) {
|
|
18
17
|
if (!isCurrent()) return new Map();
|
|
19
18
|
const forecasts = new Map();
|
|
20
|
-
const pending = [];
|
|
21
19
|
let steps = 0;
|
|
22
|
-
for (const { torrent, register
|
|
20
|
+
for (const { torrent, register } of entries) {
|
|
23
21
|
if (torrent.destroyed || torrent.ready === false) continue;
|
|
24
|
-
const wanted = new
|
|
22
|
+
const wanted = new Set();
|
|
25
23
|
for (const window of register.windows()) {
|
|
26
24
|
const file = torrent.files?.[window.fileIndex];
|
|
27
25
|
if (!file) continue;
|
|
@@ -29,10 +27,7 @@ export async function futureDownload(entries, { now = Date.now(), findStore = pi
|
|
|
29
27
|
byteEnd: Math.min(window.byteEnd, file.length - 1), pieceLength: torrent.pieceLength });
|
|
30
28
|
if (!range) continue;
|
|
31
29
|
for (let piece = range.from; piece <= range.to; piece++) {
|
|
32
|
-
|
|
33
|
-
if (!previous || compareBands(window, previous) < 0) {
|
|
34
|
-
wanted.set(piece, { urgency: window.urgency, priority: window.priority, deadlineAt: window.deadlineAt });
|
|
35
|
-
}
|
|
30
|
+
wanted.add(piece);
|
|
36
31
|
if (++steps % 256 === 0) {
|
|
37
32
|
await nextTurn();
|
|
38
33
|
if (!isCurrent()) return new Map();
|
|
@@ -63,7 +58,7 @@ export async function futureDownload(entries, { now = Date.now(), findStore = pi
|
|
|
63
58
|
}
|
|
64
59
|
const store = findStore(torrent);
|
|
65
60
|
const arrivals = new Map();
|
|
66
|
-
for (const
|
|
61
|
+
for (const index of wanted) {
|
|
67
62
|
const present = store?.locationOf ? store.locationOf(index) !== "missing" : torrent.bitfield?.get(index);
|
|
68
63
|
if (present) {
|
|
69
64
|
arrivals.set(index, now);
|
|
@@ -81,33 +76,6 @@ export async function futureDownload(entries, { now = Date.now(), findStore = pi
|
|
|
81
76
|
at = Math.max(at, arrival);
|
|
82
77
|
}
|
|
83
78
|
arrivals.set(index, at);
|
|
84
|
-
// EVERY MISSING BLOCK ASKED FOR: the queues holding them say when it
|
|
85
|
-
// arrives. Each block is reserved to the wire it was asked of, so a faster
|
|
86
|
-
// peer elsewhere does not bring it sooner, and its bytes are not the
|
|
87
|
-
// band's to share out (torrent-tv/meta#178 review, WebTorrent 2.8.5
|
|
88
|
-
// `_request`, torrent.js:1758).
|
|
89
|
-
if (at !== null) {
|
|
90
|
-
if (++steps % 256 === 0) {
|
|
91
|
-
await nextTurn();
|
|
92
|
-
if (!isCurrent()) return new Map();
|
|
93
|
-
}
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const remaining = piece?.missing;
|
|
97
|
-
const holders = [];
|
|
98
|
-
for (const wire of torrent.wires ?? []) {
|
|
99
|
-
// Only a peer that can be asked for it now: one choking us is asked for
|
|
100
|
-
// nothing outside the pieces it allows, whatever it carried before.
|
|
101
|
-
if (wire.destroyed || !wire.peerPieces?.get(index) || !mayAsk(wire, index)) continue;
|
|
102
|
-
// What it carried with its queue kept full, where it has been seen so:
|
|
103
|
-
// its speed while our selection gave it little measures our asking
|
|
104
|
-
// (`SelectionLimit`, torrent-tv/meta#178).
|
|
105
|
-
const rate = Number(limit?.capacityOf?.(wire) ?? wire.downloadSpeed?.());
|
|
106
|
-
if (Number.isFinite(rate) && rate > 0) holders.push({ wire, rate });
|
|
107
|
-
}
|
|
108
|
-
pending.push({ arrivals, index, ...band, holders, remaining:
|
|
109
|
-
Number.isSafeInteger(remaining) && remaining > 0 && remaining <= piece.length && holders.length > 0
|
|
110
|
-
? remaining : null });
|
|
111
79
|
if (++steps % 256 === 0) {
|
|
112
80
|
await nextTurn();
|
|
113
81
|
if (!isCurrent()) return new Map();
|
|
@@ -115,77 +83,5 @@ export async function futureDownload(entries, { now = Date.now(), findStore = pi
|
|
|
115
83
|
}
|
|
116
84
|
forecasts.set(torrent, { measuredAt: now, arrivals });
|
|
117
85
|
}
|
|
118
|
-
// WITHIN A BAND, IN THE ORDER THE PIECES COME. The picker walks each
|
|
119
|
-
// selection from its first piece up (WebTorrent 2.8.5's default `sequential`
|
|
120
|
-
// strategy, which this proxy keeps), so a piece of a band arrives once those
|
|
121
|
-
// before it have. Charged the whole band's end instead, a band of many
|
|
122
|
-
// segments — the map states one per stretch of a level since 2.97.27 — would
|
|
123
|
-
// put its nearest piece at its farthest piece's time (torrent-tv/meta#178).
|
|
124
|
-
//
|
|
125
|
-
// AND IN PARALLEL WHERE THE PEERS DIFFER. A piece is brought by the peers
|
|
126
|
-
// holding it, each from when it has brought what was asked of it before, and
|
|
127
|
-
// no sooner than the measured supply of every peer that holds a piece still
|
|
128
|
-
// to come carries all the bytes ahead of it. Summed one piece after another
|
|
129
|
-
// at its own holders' speed, a two-hour film whose far pieces each had a few
|
|
130
|
-
// unchoked holders was forecast to arrive in 127 838 s at 2.33 MB/s, and a
|
|
131
|
-
// seek was held for 39 s on it (r49, torrent-tv/meta#178). One holder for
|
|
132
|
-
// every piece gives the sum it was; a piece nobody can be asked for still
|
|
133
|
-
// leaves every piece after it unknown.
|
|
134
|
-
pending.sort((left, right) => compareBands(left, right) || left.index - right.index);
|
|
135
|
-
const supply = new Map();
|
|
136
|
-
for (const { holders } of pending) for (const { wire, rate } of holders) supply.set(wire, rate);
|
|
137
|
-
const total = [...supply.values()].reduce((sum, rate) => sum + rate, 0);
|
|
138
|
-
const freeAt = new Map();
|
|
139
|
-
let ahead = 0;
|
|
140
|
-
let known = true;
|
|
141
|
-
for (const { arrivals, index, holders, remaining } of pending) {
|
|
142
|
-
if (!known || remaining === null || !(total > 0)) {
|
|
143
|
-
known = false;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
ahead += remaining;
|
|
147
|
-
const finish = Math.max(sharedFinish(holders, freeAt, remaining, now), now + ahead / total * 1000);
|
|
148
|
-
for (const { wire } of holders) freeAt.set(wire, Math.max(freeAt.get(wire) ?? now, finish));
|
|
149
|
-
arrivals.set(index, finish);
|
|
150
|
-
}
|
|
151
86
|
return isCurrent() ? forecasts : new Map();
|
|
152
87
|
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* When peers bringing `bytes` together finish: each starts when it is free
|
|
156
|
-
* and brings at its own rate, so the finish is where their combined bytes
|
|
157
|
-
* reach `bytes` (the earliest of them working alone at first, more joining as
|
|
158
|
-
* they come free).
|
|
159
|
-
*
|
|
160
|
-
* @param {Array<{ wire: object, rate: number }>} holders - Bytes per second.
|
|
161
|
-
* @param {Map<object, number>} freeAt - Milliseconds.
|
|
162
|
-
* @param {number} bytes
|
|
163
|
-
* @param {number} now - Milliseconds.
|
|
164
|
-
* @returns {number} Milliseconds.
|
|
165
|
-
*/
|
|
166
|
-
function sharedFinish(holders, freeAt, bytes, now) {
|
|
167
|
-
const starts = holders.map(({ wire, rate }) => ({ at: Math.max(now, freeAt.get(wire) ?? now), rate }))
|
|
168
|
-
.sort((left, right) => left.at - right.at);
|
|
169
|
-
let rate = 0;
|
|
170
|
-
let weighted = 0;
|
|
171
|
-
for (let index = 0; index < starts.length; index++) {
|
|
172
|
-
rate += starts[index].rate;
|
|
173
|
-
weighted += starts[index].rate * starts[index].at;
|
|
174
|
-
const finish = (bytes * 1000 + weighted) / rate;
|
|
175
|
-
if (index === starts.length - 1 || finish <= starts[index + 1].at) return finish;
|
|
176
|
-
}
|
|
177
|
-
return now;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Whether this peer can be asked for this piece now: it is not choking us, or
|
|
182
|
-
* it allows this piece while it does (BEP 6, WebTorrent 2.8.5 `_updateWire`).
|
|
183
|
-
*
|
|
184
|
-
* @param {{ peerChoking?: boolean, hasFast?: boolean, peerAllowedFastSet?: number[] }} wire
|
|
185
|
-
* @param {number} index
|
|
186
|
-
* @returns {boolean}
|
|
187
|
-
*/
|
|
188
|
-
function mayAsk(wire, index) {
|
|
189
|
-
if (wire.peerChoking !== true) return true;
|
|
190
|
-
return wire.hasFast === true && Array.isArray(wire.peerAllowedFastSet) && wire.peerAllowedFastSet.includes(index);
|
|
191
|
-
}
|
|
@@ -380,7 +380,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
380
380
|
const arrival = (range.availableAt - now) / 1000;
|
|
381
381
|
// The entire protocol piece becomes readable after verification;
|
|
382
382
|
// bytes inside it never arrive proportionally to their file address.
|
|
383
|
-
sourceByteService.get(id).push({ start: range.start, end: range.end, begin: arrival, finish: arrival });
|
|
383
|
+
sourceByteService.get(id).push({ start: range.start, end: range.end, begin: arrival, finish: arrival, planned: true });
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
}
|
|
@@ -409,6 +409,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
409
409
|
for (const job of jobs) {
|
|
410
410
|
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
|
|
411
411
|
let sourceArrivalAt = 0;
|
|
412
|
+
const plan = { finish: 0 };
|
|
412
413
|
for (const [sourceId, ranges] of sourceByteIntervals) {
|
|
413
414
|
const source = sourceState.get(sourceId);
|
|
414
415
|
if (!source) {
|
|
@@ -417,7 +418,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
417
418
|
for (const interval of ranges) {
|
|
418
419
|
const missing = uncoveredIntervals(interval, sourceByteCoverage.get(sourceId) ?? []);
|
|
419
420
|
if (missing.length === 0) {
|
|
420
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(interval, sourceArrivals.get(sourceId)));
|
|
421
|
+
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(interval, sourceArrivals.get(sourceId), plan));
|
|
421
422
|
continue;
|
|
422
423
|
}
|
|
423
424
|
|
|
@@ -468,10 +469,13 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
468
469
|
}
|
|
469
470
|
const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, admittedAt);
|
|
470
471
|
trackFinish[trackIndex] = Math.max(trackFinish[trackIndex], producedAt);
|
|
471
|
-
|
|
472
|
+
const sourcePlanned = sourceArrivalAt > 0 && plan.finish >= sourceArrivalAt;
|
|
473
|
+
productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivalAt > 0, sourceArrivalAt,
|
|
474
|
+
sourcePlanned });
|
|
472
475
|
const deliveryAt = link.finish(sizeBits, Math.max(transferFinish, producedAt));
|
|
473
476
|
transferFinish = deliveryAt;
|
|
474
|
-
completions.push({ at: deliveryAt + finiteNonNegative(track.appendSeconds), trackIndex, segment
|
|
477
|
+
completions.push({ at: deliveryAt + finiteNonNegative(track.appendSeconds), trackIndex, segment, sourceArrivalAt,
|
|
478
|
+
sourcePlanned });
|
|
475
479
|
}
|
|
476
480
|
|
|
477
481
|
const constraints = [];
|
|
@@ -479,10 +483,12 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
479
483
|
// largest of thousands, and the log could only say that some term was
|
|
480
484
|
// positive while the picture stood for 17 s (torrent-tv/meta#178).
|
|
481
485
|
let holding = null;
|
|
482
|
-
const constrain = (value, kind, trackIndex, segment, atSeconds, aheadSeconds, requiresSource
|
|
486
|
+
const constrain = (value, kind, trackIndex, segment, atSeconds, aheadSeconds, requiresSource,
|
|
487
|
+
sourceArrivalAt = 0, sourcePlanned = false) => {
|
|
483
488
|
constraints.push(value);
|
|
484
489
|
if (value > 0 && (holding === null || value > holding.value)) {
|
|
485
|
-
holding = { value, kind, trackIndex, segment, atSeconds, aheadSeconds, requiresSource
|
|
490
|
+
holding = { value, kind, trackIndex, segment, atSeconds, aheadSeconds, requiresSource, sourceArrivalAt,
|
|
491
|
+
sourcePlanned };
|
|
486
492
|
}
|
|
487
493
|
};
|
|
488
494
|
const end = rescale(fromSeconds(duration), unit);
|
|
@@ -501,7 +507,8 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
501
507
|
for (const range of completion.segment.ranges) addMediaRange(delivered, range);
|
|
502
508
|
after = contiguousEnd(delivered, at);
|
|
503
509
|
const ahead = secondsAhead(earliest(before, trackEnd), at);
|
|
504
|
-
constrain(completion.at - ahead, "delivery", trackIndex, completion.segment.index, completion.at, ahead, false
|
|
510
|
+
constrain(completion.at - ahead, "delivery", trackIndex, completion.segment.index, completion.at, ahead, false,
|
|
511
|
+
completion.sourceArrivalAt, completion.sourcePlanned);
|
|
505
512
|
if (compare(after, before) <= 0 && completion.segment.ranges.some(({ start }) => compare(start, before) > 0)) {
|
|
506
513
|
// A timestamp hole cannot be repaired by waiting longer. Keep it
|
|
507
514
|
// distinct from an unavailable rate or a slow but finite service.
|
|
@@ -524,7 +531,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
524
531
|
: Math.max(0, state.tailEndSeconds - toSeconds(before));
|
|
525
532
|
const ahead = Math.max(0, secondsAhead(before, at) - Math.min(reserve, left) * Number(production.requiresSource));
|
|
526
533
|
constrain(production.at - ahead, "production", trackIndex, production.segment.index, production.at, ahead,
|
|
527
|
-
production.requiresSource);
|
|
534
|
+
production.requiresSource, production.sourceArrivalAt, production.sourcePlanned);
|
|
528
535
|
}
|
|
529
536
|
}
|
|
530
537
|
if (trackState.some(({ tail }) => tail.length > 0)) {
|
|
@@ -548,7 +555,11 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
548
555
|
holdingBy: ready ? null : holding === null ? (originReady ? null : { kind: "position-not-held" })
|
|
549
556
|
: { kind: holding.kind, track: tracks[holding.trackIndex]?.id ?? holding.trackIndex,
|
|
550
557
|
segment: holding.segment, atSeconds: holding.atSeconds, aheadSeconds: holding.aheadSeconds,
|
|
551
|
-
requiresSource: holding.requiresSource
|
|
558
|
+
requiresSource: holding.requiresSource,
|
|
559
|
+
// When its input's last bytes come, and whether the torrent thread's
|
|
560
|
+
// queues say so rather than the source's measured service.
|
|
561
|
+
...(holding.sourceArrivalAt > 0 ? { sourceArrivalSeconds: holding.sourceArrivalAt,
|
|
562
|
+
sourcePlanned: holding.sourcePlanned } : {}) },
|
|
552
563
|
preparedUntilSeconds: Math.min(duration, ...trackState.map((state, index) => {
|
|
553
564
|
const ranges = state.clientRanges.map((range) => ({ ...range }));
|
|
554
565
|
for (const item of productions.filter((item) => item.trackIndex === index && item.at <= delay)) {
|
|
@@ -600,6 +611,7 @@ function scheduleTail({ trackState, sourceState, sourceByteCoverage, sourceArriv
|
|
|
600
611
|
return state;
|
|
601
612
|
};
|
|
602
613
|
const next = trackState.map(() => 0);
|
|
614
|
+
const plan = { finish: 0 };
|
|
603
615
|
let delivered = transferFinish;
|
|
604
616
|
for (;;) {
|
|
605
617
|
// The next piece in media order across the tracks, as the near window orders them.
|
|
@@ -635,6 +647,7 @@ function scheduleTail({ trackState, sourceState, sourceByteCoverage, sourceArriv
|
|
|
635
647
|
work = Math.max(0, work);
|
|
636
648
|
}
|
|
637
649
|
let sourceArrivalAt = 0;
|
|
650
|
+
plan.finish = 0;
|
|
638
651
|
if (work > 0) {
|
|
639
652
|
if (!state.curve) return { reason: "encode-rate-unavailable" };
|
|
640
653
|
const trackSources = Array.isArray(track.sourceIds) ? track.sourceIds : [];
|
|
@@ -653,7 +666,7 @@ function scheduleTail({ trackState, sourceState, sourceByteCoverage, sourceArriv
|
|
|
653
666
|
let order = 0;
|
|
654
667
|
for (const range of ranges) {
|
|
655
668
|
// Bytes the torrent thread planned, or the near window brought, come when they were to come.
|
|
656
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(range, sourceArrivals.get(sourceId)));
|
|
669
|
+
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(range, sourceArrivals.get(sourceId), plan));
|
|
657
670
|
const after = missingBefore(bytes, range.end);
|
|
658
671
|
if (after > missingBefore(bytes, range.start)) order = Math.max(order, after);
|
|
659
672
|
}
|
|
@@ -703,10 +716,13 @@ function scheduleTail({ trackState, sourceState, sourceByteCoverage, sourceArriv
|
|
|
703
716
|
// hole stops the media is decided in the near window, in exact time.
|
|
704
717
|
const ahead = Math.max(0, startsAt - position);
|
|
705
718
|
const requiresSource = sourceArrivalAt > 0;
|
|
706
|
-
|
|
719
|
+
const sourcePlanned = requiresSource && plan.finish >= sourceArrivalAt;
|
|
720
|
+
constrain(completedAt - ahead, "delivery", trackIndex, segment.index, completedAt, ahead, false, sourceArrivalAt,
|
|
721
|
+
sourcePlanned);
|
|
707
722
|
const covered = Math.max(0, ahead - Math.min(reserve, Math.max(0, state.tailEndSeconds - startsAt)) *
|
|
708
723
|
Number(requiresSource));
|
|
709
|
-
constrain(producedAt - covered, "production", trackIndex, segment.index, producedAt, covered, requiresSource
|
|
724
|
+
constrain(producedAt - covered, "production", trackIndex, segment.index, producedAt, covered, requiresSource,
|
|
725
|
+
sourceArrivalAt, sourcePlanned);
|
|
710
726
|
}
|
|
711
727
|
}
|
|
712
728
|
|
|
@@ -1023,7 +1039,16 @@ function indexedArrivals(ranges) {
|
|
|
1023
1039
|
.map(range => ({ ...range, maximumEnd: maximumEnd = Math.max(maximumEnd, range.end) }));
|
|
1024
1040
|
}
|
|
1025
1041
|
|
|
1026
|
-
|
|
1042
|
+
/**
|
|
1043
|
+
* When the last of an interval's bytes arrives, by the arrivals indexed; with
|
|
1044
|
+
* `plan`, the latest of those the torrent thread's queues gave is kept in it.
|
|
1045
|
+
*
|
|
1046
|
+
* @param {{ start: number, end: number }} interval
|
|
1047
|
+
* @param {Array<{ start: number, end: number, finish: number, maximumEnd: number, planned?: boolean }>} [ranges]
|
|
1048
|
+
* @param {{ finish: number } | null} [plan]
|
|
1049
|
+
* @returns {number}
|
|
1050
|
+
*/
|
|
1051
|
+
function latestArrival(interval, ranges = [], plan = null) {
|
|
1027
1052
|
let low = 0, high = ranges.length;
|
|
1028
1053
|
while (low < high) {
|
|
1029
1054
|
const middle = (low + high) >>> 1;
|
|
@@ -1032,7 +1057,10 @@ function latestArrival(interval, ranges = []) {
|
|
|
1032
1057
|
}
|
|
1033
1058
|
let finish = 0;
|
|
1034
1059
|
for (let index = low; index < ranges.length && ranges[index].start < interval.end; index++) {
|
|
1035
|
-
if (ranges[index].end > interval.start)
|
|
1060
|
+
if (ranges[index].end > interval.start) {
|
|
1061
|
+
finish = Math.max(finish, ranges[index].finish);
|
|
1062
|
+
if (plan && ranges[index].planned) plan.finish = Math.max(plan.finish, ranges[index].finish);
|
|
1063
|
+
}
|
|
1036
1064
|
}
|
|
1037
1065
|
return finish;
|
|
1038
1066
|
}
|