@torrent-tv/proxy 2.80.5 → 2.80.6
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 +32 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +19 -1
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +53 -1
- package/services/encode/open-piece.js +22 -1
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +118 -89
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +40 -0
- package/test/output-speed.test.js +86 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
|
@@ -119,10 +119,10 @@ import { createDeliveryProbe, PROBE_INTERVAL_MS } from "./delivery-probe.js";
|
|
|
119
119
|
* @returns {{ certain: boolean, needMs: number | null }}
|
|
120
120
|
*/
|
|
121
121
|
export function wedgeIsCertain({ queuedBytes, bytesPerSecond, flatForMs, longestHealthyFlatMs = 0 }) {
|
|
122
|
-
if (!(
|
|
122
|
+
if (!(bytesPerSecond > 0)) {
|
|
123
123
|
return { certain: false, needMs: null };
|
|
124
124
|
}
|
|
125
|
-
const drainMs = (queuedBytes / bytesPerSecond) * 1000;
|
|
125
|
+
const drainMs = (Math.max(queuedBytes, 0) / bytesPerSecond) * 1000;
|
|
126
126
|
const needMs = Math.max(drainMs, longestHealthyFlatMs, PROBE_INTERVAL_MS);
|
|
127
127
|
return { certain: flatForMs >= needMs, needMs };
|
|
128
128
|
}
|
|
@@ -221,6 +221,37 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
221
221
|
};
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Update the browser-side channel byte counter for a session.
|
|
226
|
+
*
|
|
227
|
+
* The transport's `bytesReceived` advances on SACKs (140 B/s on a wedged
|
|
228
|
+
* association) even though every data channel is flat — so it gives a false
|
|
229
|
+
* "peer still sending" through every other tick (field 2026-09-06: 6 h 50 min
|
|
230
|
+
* wedge, `flowing`/`association-stopped` alternating every 500 ms). Channel
|
|
231
|
+
* counters are in the same `probe-echo` report and are flat on a wedge.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} sessionId
|
|
234
|
+
* @param {object} report - The `report` object from `probe-echo`.
|
|
235
|
+
* @returns {void}
|
|
236
|
+
*/
|
|
237
|
+
const noteBrowserReport = (sessionId, report) => {
|
|
238
|
+
const connection = connections.get(sessionId);
|
|
239
|
+
if (!connection || !report || typeof report !== "object") return;
|
|
240
|
+
const channels = report.channels;
|
|
241
|
+
if (!channels || typeof channels !== "object") return;
|
|
242
|
+
let total = 0;
|
|
243
|
+
let hasBytes = false;
|
|
244
|
+
for (const v of Object.values(channels)) {
|
|
245
|
+
if (v && typeof v.bytes === "number" && Number.isFinite(v.bytes)) {
|
|
246
|
+
total += v.bytes;
|
|
247
|
+
hasBytes = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (hasBytes) {
|
|
251
|
+
connection.browserChannelBytes = total;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
224
255
|
/**
|
|
225
256
|
* @param {string} sessionId
|
|
226
257
|
* @param {string} tag
|
|
@@ -228,7 +259,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
228
259
|
* @param {DataChannel} channel
|
|
229
260
|
* @returns {() => void} Stops the watch.
|
|
230
261
|
*/
|
|
231
|
-
|
|
262
|
+
const watchSendQueue = (sessionId, tag, label, channel) => {
|
|
232
263
|
let lowestSinceDrain = Number.POSITIVE_INFINITY;
|
|
233
264
|
let stuckSince = 0;
|
|
234
265
|
let previous = null;
|
|
@@ -243,6 +274,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
243
274
|
// would be cleared a second after the wedged channel set it and the line
|
|
244
275
|
// would print for every second of a 54-minute episode.
|
|
245
276
|
let wedgeSaid = false;
|
|
277
|
+
let lastStuckLogAt = 0;
|
|
246
278
|
let connection = connections.get(sessionId);
|
|
247
279
|
if (!connection) {
|
|
248
280
|
connection = {
|
|
@@ -257,7 +289,12 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
257
289
|
sentAt: 0,
|
|
258
290
|
sentBytes: 0,
|
|
259
291
|
bytesPerSecond: 0,
|
|
260
|
-
longestHealthyFlatMs: 0
|
|
292
|
+
longestHealthyFlatMs: 0,
|
|
293
|
+
// Browser-side channel bytes (sum of report.channels[].bytes). Transport's
|
|
294
|
+
// bytesReceived advances on SACKs (140 B/s on a wedge) and gives a false
|
|
295
|
+
// "peer still sending" every other tick — channel bytes are flat on a wedge.
|
|
296
|
+
browserChannelBytes: 0,
|
|
297
|
+
browserChannelBytesAtStuck: -1
|
|
261
298
|
};
|
|
262
299
|
connections.set(sessionId, connection);
|
|
263
300
|
}
|
|
@@ -395,6 +432,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
395
432
|
stuckSince = 0;
|
|
396
433
|
previous = null;
|
|
397
434
|
receivedWhenStuck = -1;
|
|
435
|
+
connection.browserChannelBytesAtStuck = -1;
|
|
398
436
|
wedgeSaid = false;
|
|
399
437
|
// The queue moved, so whatever was called a wedge has cleared. Let a
|
|
400
438
|
// later one be recorded too: one mistaken call must not spend the
|
|
@@ -409,7 +447,8 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
409
447
|
}
|
|
410
448
|
const snapshot = getTransportSnapshot?.(sessionId) ?? null;
|
|
411
449
|
if (!snapshot) {
|
|
412
|
-
if (now - stuckSince >= SEND_QUEUE_STUCK_MS) {
|
|
450
|
+
if (now - stuckSince >= SEND_QUEUE_STUCK_MS && now - lastStuckLogAt >= 30_000) {
|
|
451
|
+
lastStuckLogAt = now;
|
|
413
452
|
log(`[dc] Session ${tag} "${label}": send queue stuck at ${queued}B for ` +
|
|
414
453
|
`${Math.round((now - stuckSince) / 1000)}s — no transport to ask`);
|
|
415
454
|
}
|
|
@@ -421,6 +460,9 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
421
460
|
if (receivedWhenStuck < 0) {
|
|
422
461
|
receivedWhenStuck = snapshot.bytesReceived;
|
|
423
462
|
}
|
|
463
|
+
if (connection.browserChannelBytesAtStuck < 0) {
|
|
464
|
+
connection.browserChannelBytesAtStuck = connection.browserChannelBytes;
|
|
465
|
+
}
|
|
424
466
|
// The periodic line waits for {@link SEND_QUEUE_STUCK_MS}, because a
|
|
425
467
|
// queue that has merely not fallen for a second is ordinary and a line a
|
|
426
468
|
// second for it is noise. The WEDGE below does not wait for it: its own
|
|
@@ -428,15 +470,18 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
428
470
|
// on a fast link that is under a second. Holding the evidence back for a
|
|
429
471
|
// fixed five seconds would repeat, in miniature, the mistake that left
|
|
430
472
|
// both field captures without an onset in them.
|
|
431
|
-
|
|
473
|
+
// Rate-limit to 30 s — field 2026-09-06 printed 49 295 lines (31 % of log)
|
|
474
|
+
// for one 6 h 50 min wedge, one line per second per channel.
|
|
475
|
+
if (now - stuckSince >= SEND_QUEUE_STUCK_MS && now - lastStuckLogAt >= 30_000) {
|
|
476
|
+
lastStuckLogAt = now;
|
|
432
477
|
log(
|
|
433
478
|
`[dc] Session ${tag} "${label}": send queue stuck at ${queued}B for ` +
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
479
|
+
`${Math.round((now - stuckSince) / 1000)}s — transport ` +
|
|
480
|
+
`sent=${snapshot.bytesSent}${sentDelta === null ? "" : ` (+${sentDelta})`} ` +
|
|
481
|
+
`received=${snapshot.bytesReceived}${recvDelta === null ? "" : ` (+${recvDelta})`} ` +
|
|
482
|
+
`rtt=${snapshot.rtt}ms pc=${snapshot.state} ice=${snapshot.iceState} pair=${snapshot.pair}`
|
|
438
483
|
);
|
|
439
|
-
}
|
|
484
|
+
}
|
|
440
485
|
// Roadmap item 11: ask for the packet-level truth the moment the wedge is
|
|
441
486
|
// CERTAIN, not after a chosen delay. The three facts below are not
|
|
442
487
|
// ambiguous together — the queue has not fallen, the accepted-byte
|
|
@@ -452,7 +497,10 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
452
497
|
flatForMs,
|
|
453
498
|
longestHealthyFlatMs: connection.longestHealthyFlatMs
|
|
454
499
|
});
|
|
455
|
-
const
|
|
500
|
+
const hasBrowserBytes = Number.isFinite(connection.browserChannelBytes) && connection.browserChannelBytesAtStuck >= 0;
|
|
501
|
+
const peerStillSending = hasBrowserBytes
|
|
502
|
+
? connection.browserChannelBytes > connection.browserChannelBytesAtStuck
|
|
503
|
+
: snapshot.bytesReceived > receivedWhenStuck;
|
|
456
504
|
if (verdict.certain && peerStillSending && !wedgeSaid) {
|
|
457
505
|
wedgeSaid = true;
|
|
458
506
|
log(
|
|
@@ -470,7 +518,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
470
518
|
peerStillSending
|
|
471
519
|
) {
|
|
472
520
|
connection.captureStarted = true;
|
|
473
|
-
|
|
521
|
+
witness.maybeCapture({
|
|
474
522
|
sessionId,
|
|
475
523
|
tag,
|
|
476
524
|
label,
|
|
@@ -478,12 +526,6 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
478
526
|
queuedBytes: queued,
|
|
479
527
|
stuckForMs: now - stuckSince
|
|
480
528
|
});
|
|
481
|
-
if (!started) {
|
|
482
|
-
// Refused for now (no remote endpoint yet, capture already running
|
|
483
|
-
// elsewhere, cooldown): let the next tick try again rather than
|
|
484
|
-
// spending the one attempt per wedge on a refusal.
|
|
485
|
-
connection.captureStarted = false;
|
|
486
|
-
}
|
|
487
529
|
}
|
|
488
530
|
if (usrsctpState && verdict.certain && peerStillSending) {
|
|
489
531
|
// Its own single-flight and cooldown, independent of the witness's —
|
|
@@ -501,7 +543,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
|
|
|
501
543
|
return stop;
|
|
502
544
|
};
|
|
503
545
|
|
|
504
|
-
return { watchSendQueue, readDelivery };
|
|
546
|
+
return { watchSendQueue, readDelivery, noteBrowserReport };
|
|
505
547
|
}
|
|
506
548
|
|
|
507
549
|
/**
|
|
@@ -676,7 +718,7 @@ export function createDataChannelHandler({
|
|
|
676
718
|
/** Request id → its ASCII bytes; see {@link requestIdBytes}. */
|
|
677
719
|
const requestIdCache = new Map();
|
|
678
720
|
|
|
679
|
-
const { watchSendQueue, readDelivery } = makeSendQueueWatcher({
|
|
721
|
+
const { watchSendQueue, readDelivery, noteBrowserReport } = makeSendQueueWatcher({
|
|
680
722
|
log: (message) => log(message),
|
|
681
723
|
getTransportSnapshot,
|
|
682
724
|
witness,
|
|
@@ -884,6 +926,7 @@ export function createDataChannelHandler({
|
|
|
884
926
|
// on working through a freeze, so it arrives when nothing else does.
|
|
885
927
|
if (message.type === "probe-echo") {
|
|
886
928
|
deliveryProbe.noteEcho(sessionId, message);
|
|
929
|
+
noteBrowserReport(sessionId, message.report);
|
|
887
930
|
if (message.report && typeof message.report === "object") {
|
|
888
931
|
const report = message.report;
|
|
889
932
|
const channels = report.channels && typeof report.channels === "object"
|
|
@@ -1142,8 +1185,13 @@ export function createDataChannelHandler({
|
|
|
1142
1185
|
/**
|
|
1143
1186
|
* Resolve once the channel's outgoing buffer has drained below the low-water
|
|
1144
1187
|
* mark. No-op (resolves immediately) when the buffer is already small or the
|
|
1145
|
-
* channel does not expose buffer APIs.
|
|
1146
|
-
*
|
|
1188
|
+
* channel does not expose buffer APIs. The previous implementation resolved
|
|
1189
|
+
* after {@link DC_BUFFER_DRAIN_TIMEOUT_MS} even though the buffer was still
|
|
1190
|
+
* high, so the send loop kept queueing into a wedged channel and the queue
|
|
1191
|
+
* grew to 399 MB (field 2026-09-06). This version waits until the buffer
|
|
1192
|
+
* actually drains, polling as a fallback for a missed low-water event, and
|
|
1193
|
+
* never resolves while the channel is still holding bytes above the low-water
|
|
1194
|
+
* mark.
|
|
1147
1195
|
*
|
|
1148
1196
|
* @param {DataChannel} channel
|
|
1149
1197
|
* @returns {Promise<void>}
|
|
@@ -1155,21 +1203,35 @@ export function createDataChannelHandler({
|
|
|
1155
1203
|
resolve();
|
|
1156
1204
|
return;
|
|
1157
1205
|
}
|
|
1206
|
+
let poll = null;
|
|
1158
1207
|
let settled = false;
|
|
1159
1208
|
const done = () => {
|
|
1160
1209
|
if (settled) return;
|
|
1161
1210
|
settled = true;
|
|
1211
|
+
if (poll) clearInterval(poll);
|
|
1212
|
+
try { channel.onBufferedAmountLow(() => {}); } catch {}
|
|
1162
1213
|
resolve();
|
|
1163
1214
|
};
|
|
1164
1215
|
channel.setBufferedAmountLowThreshold(DC_BUFFER_LOW_WATER);
|
|
1165
1216
|
channel.onBufferedAmountLow(done);
|
|
1217
|
+
// Poll as a fallback for a missed low-water event — the previous
|
|
1218
|
+
// 5 s timeout resolved while the buffer was still high, which is the
|
|
1219
|
+
// defect. Polling waits until the condition is actually met.
|
|
1220
|
+
poll = setInterval(() => {
|
|
1221
|
+
try {
|
|
1222
|
+
if (channel.bufferedAmount() <= DC_BUFFER_LOW_WATER) {
|
|
1223
|
+
done();
|
|
1224
|
+
}
|
|
1225
|
+
} catch {
|
|
1226
|
+
done();
|
|
1227
|
+
}
|
|
1228
|
+
}, 50);
|
|
1229
|
+
if (typeof poll.unref === "function") poll.unref();
|
|
1166
1230
|
// Guard against a race where the buffer drained between the check above
|
|
1167
1231
|
// and registering the callback (the low-water event would never fire).
|
|
1168
1232
|
if (channel.bufferedAmount() <= DC_BUFFER_LOW_WATER) {
|
|
1169
1233
|
done();
|
|
1170
|
-
return;
|
|
1171
1234
|
}
|
|
1172
|
-
setTimeout(done, DC_BUFFER_DRAIN_TIMEOUT_MS);
|
|
1173
1235
|
} catch {
|
|
1174
1236
|
resolve();
|
|
1175
1237
|
}
|
|
@@ -372,11 +372,22 @@ export function createDeliveryProbe({
|
|
|
372
372
|
0
|
|
373
373
|
);
|
|
374
374
|
// Advancing SINCE THE PREVIOUS TICK, not since the connection began: the
|
|
375
|
-
// question is whether bytes are crossing now.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
connection.
|
|
375
|
+
// question is whether bytes are crossing now. Prefer channel bytes —
|
|
376
|
+
// transport's bytesReceived advances on SACKs (140 B/s on a wedge) and
|
|
377
|
+
// gives a false "advancing" every other tick, while channel bytes are flat.
|
|
378
|
+
let peerBytesAdvancing = null;
|
|
379
|
+
if (connection.peerChannelBytes !== null) {
|
|
380
|
+
peerBytesAdvancing =
|
|
381
|
+
connection.peerChannelBytesAtTick === null ||
|
|
382
|
+
connection.peerChannelBytes > connection.peerChannelBytesAtTick;
|
|
383
|
+
connection.peerChannelBytesAtTick = connection.peerChannelBytes;
|
|
384
|
+
// keep transport's tick in sync for fallback logging
|
|
385
|
+
connection.peerBytesAtTick = connection.peerBytes;
|
|
386
|
+
} else if (connection.peerBytes !== null) {
|
|
387
|
+
peerBytesAdvancing =
|
|
388
|
+
connection.peerBytesAtTick === null || connection.peerBytes > connection.peerBytesAtTick;
|
|
389
|
+
connection.peerBytesAtTick = connection.peerBytes;
|
|
390
|
+
}
|
|
380
391
|
const { verdict, detail } = readProbeState({
|
|
381
392
|
seq: connection.seq,
|
|
382
393
|
seen: connection.seen,
|
|
@@ -468,6 +479,13 @@ export function createDeliveryProbe({
|
|
|
468
479
|
peerBytes: null,
|
|
469
480
|
/** @type {number | null} */
|
|
470
481
|
peerBytesAtTick: null,
|
|
482
|
+
// Sum of per-channel bytes (report.channels[].bytes). Transport's
|
|
483
|
+
// bytesReceived advances on SACKs (140 B/s on a wedge) and gives a
|
|
484
|
+
// false "advancing" every other tick — channel bytes are flat on a wedge.
|
|
485
|
+
/** @type {number | null} */
|
|
486
|
+
peerChannelBytes: null,
|
|
487
|
+
/** @type {number | null} */
|
|
488
|
+
peerChannelBytesAtTick: null,
|
|
471
489
|
// The far end's own event-loop delay and tab state, as last reported.
|
|
472
490
|
// The lag is a term in every allowance below; the tab state is only
|
|
473
491
|
// printed, so that a wide allowance can be read back to its cause.
|
|
@@ -554,6 +572,21 @@ export function createDeliveryProbe({
|
|
|
554
572
|
if (Number.isFinite(peerBytes) && peerBytes >= 0) {
|
|
555
573
|
connection.peerBytes = peerBytes;
|
|
556
574
|
}
|
|
575
|
+
// Sum of per-channel bytes. Transport's bytesReceived advances on SACKs
|
|
576
|
+
// (140 B/s on a wedge) and gives a false "advancing" every other tick —
|
|
577
|
+
// channel bytes are flat on a wedge, so they are the correct signal.
|
|
578
|
+
const ch = echo?.report?.channels;
|
|
579
|
+
if (ch && typeof ch === "object") {
|
|
580
|
+
let total = 0;
|
|
581
|
+
let has = false;
|
|
582
|
+
for (const v of Object.values(ch)) {
|
|
583
|
+
if (v && typeof v.bytes === "number" && Number.isFinite(v.bytes)) {
|
|
584
|
+
total += v.bytes;
|
|
585
|
+
has = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (has) connection.peerChannelBytes = total;
|
|
589
|
+
}
|
|
557
590
|
// How far behind the far end's own event loop is running. A browser that
|
|
558
591
|
// cannot run its timers cannot answer a probe, and every allowance here
|
|
559
592
|
// is a wait the answer has to fit inside — so this is a term in the
|
|
@@ -135,6 +135,31 @@ export class CoverageMap {
|
|
|
135
135
|
this.#claims.delete(run);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* How many numbers between two are already made.
|
|
140
|
+
*
|
|
141
|
+
* What an encoder driving from one to the other would produce a SECOND time,
|
|
142
|
+
* and therefore what the swarm would be asked to fetch a second time. It is a
|
|
143
|
+
* term of when that encoder arrives, not a separate question about whether to
|
|
144
|
+
* move it: an encoder that has to re-make three hundred pieces on its way is
|
|
145
|
+
* simply slower to get there, and the model compares arrivals.
|
|
146
|
+
*
|
|
147
|
+
* @param {number} from - Inclusive.
|
|
148
|
+
* @param {number} to - Inclusive.
|
|
149
|
+
* @returns {number}
|
|
150
|
+
*/
|
|
151
|
+
madeBetween(from, to) {
|
|
152
|
+
const first = Number.isInteger(from) && from > 0 ? from : 0;
|
|
153
|
+
const last = Number.isInteger(to) ? to : -1;
|
|
154
|
+
let count = 0;
|
|
155
|
+
for (let index = first; index <= last; index += 1) {
|
|
156
|
+
if (this.isReady(index)) {
|
|
157
|
+
count += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return count;
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
/**
|
|
139
164
|
* @param {number} index
|
|
140
165
|
* @returns {boolean}
|
|
@@ -195,7 +220,7 @@ export class CoverageMap {
|
|
|
195
220
|
continue;
|
|
196
221
|
}
|
|
197
222
|
const maker = this.makerOf(at);
|
|
198
|
-
if (maker === null || maker
|
|
223
|
+
if (maker === null || CoverageMap.#isExcepted(maker, exceptRun)) {
|
|
199
224
|
return at;
|
|
200
225
|
}
|
|
201
226
|
}
|
|
@@ -226,7 +251,7 @@ export class CoverageMap {
|
|
|
226
251
|
continue;
|
|
227
252
|
}
|
|
228
253
|
const maker = this.makerOf(at);
|
|
229
|
-
if (maker !== null && maker
|
|
254
|
+
if (maker !== null && !CoverageMap.#isExcepted(maker, exceptRun)) {
|
|
230
255
|
at += 1;
|
|
231
256
|
continue;
|
|
232
257
|
}
|
|
@@ -235,6 +260,36 @@ export class CoverageMap {
|
|
|
235
260
|
return at - start;
|
|
236
261
|
}
|
|
237
262
|
|
|
263
|
+
/**
|
|
264
|
+
* How many numbers from `index` onward have NOT been made, claims ignored.
|
|
265
|
+
*
|
|
266
|
+
* The stretch an encoder placed here could work through before it would be
|
|
267
|
+
* re-making something that exists. Claims are deliberately left out: a claim
|
|
268
|
+
* says another encoder MEANS to make it, and where two encoders are being
|
|
269
|
+
* placed in one pass the claims of the moment are about to be re-cut — so
|
|
270
|
+
* asking about them here gives an answer that was true a step ago. Where one
|
|
271
|
+
* encoder's road ends because another begins is decided once, over all the
|
|
272
|
+
* placements together, after they are known.
|
|
273
|
+
*
|
|
274
|
+
* @param {number} index
|
|
275
|
+
* @returns {number} Zero when `index` is already made; the rest of the track
|
|
276
|
+
* when nothing ahead is; `Infinity` when the length is not yet known.
|
|
277
|
+
*/
|
|
278
|
+
unmadeRunFrom(index) {
|
|
279
|
+
const start = Number.isInteger(index) && index > 0 ? index : 0;
|
|
280
|
+
if (this.isReady(start)) {
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (this.#segmentCount <= 0) {
|
|
284
|
+
return Number.POSITIVE_INFINITY;
|
|
285
|
+
}
|
|
286
|
+
let end = start;
|
|
287
|
+
while (end < this.#segmentCount && !this.isReady(end)) {
|
|
288
|
+
end += 1;
|
|
289
|
+
}
|
|
290
|
+
return end - start;
|
|
291
|
+
}
|
|
292
|
+
|
|
238
293
|
/**
|
|
239
294
|
* How many numbers from `index` onwards are free — nobody has made them and
|
|
240
295
|
* nobody is making them.
|
|
@@ -273,7 +328,7 @@ export class CoverageMap {
|
|
|
273
328
|
break;
|
|
274
329
|
}
|
|
275
330
|
const maker = this.makerOf(at);
|
|
276
|
-
if (maker !== null && maker
|
|
331
|
+
if (maker !== null && !CoverageMap.#isExcepted(maker, exceptRun)) {
|
|
277
332
|
break;
|
|
278
333
|
}
|
|
279
334
|
at += 1;
|
|
@@ -293,6 +348,24 @@ export class CoverageMap {
|
|
|
293
348
|
* @param {object | null} exceptRun
|
|
294
349
|
* @returns {number | null}
|
|
295
350
|
*/
|
|
351
|
+
/**
|
|
352
|
+
* Is this claim one of the runs the caller is setting aside?
|
|
353
|
+
*
|
|
354
|
+
* A single run or a set of them: a pass that re-cuts several roads at once has
|
|
355
|
+
* to ask about all of them together, and asking once per run gave an answer
|
|
356
|
+
* true of no moment.
|
|
357
|
+
*
|
|
358
|
+
* @param {object} maker
|
|
359
|
+
* @param {object | Set<object> | null} except
|
|
360
|
+
* @returns {boolean}
|
|
361
|
+
*/
|
|
362
|
+
static #isExcepted(maker, except) {
|
|
363
|
+
if (except === null || except === undefined) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return except instanceof Set ? except.has(maker) : maker === except;
|
|
367
|
+
}
|
|
368
|
+
|
|
296
369
|
#firstCoveredFrom(index, exceptRun) {
|
|
297
370
|
let lowest = null;
|
|
298
371
|
for (const ready of this.#ready) {
|
|
@@ -301,7 +374,7 @@ export class CoverageMap {
|
|
|
301
374
|
}
|
|
302
375
|
}
|
|
303
376
|
for (const [run, span] of this.#claims) {
|
|
304
|
-
if (run
|
|
377
|
+
if (CoverageMap.#isExcepted(run, exceptRun)) {
|
|
305
378
|
continue;
|
|
306
379
|
}
|
|
307
380
|
// A claim that has already begun covers `index` itself.
|