@torrent-tv/proxy 2.30.1 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 2.31.0
|
|
2
|
+
|
|
3
|
+
- **Chore**: The encoder run's two status strings are gone; both are now outputs of the state table shipped in 2.23.0. `session.progress.state` was maintained by hand at seven sites and `session.state` at nine, and neither could answer on its own — the warm-up test had to read both under an `||`, because one said "starting" from the first spawn until something overwrote it while the other said it again on its own schedule. What the browser is told is computed where it is sent (`wireState(runState)`), and `session.state` is reduced to the session's own lifetime: it exists, or it has been disposed. That deletes the line in the spawn path that read `state === "disposed" ? "disposed" : "starting"` — two lifetimes in one variable, which is what it was there to paper over. Verified before the change that nothing in the browser reads the wire string, so the value set is unchanged and unobserved either way; the four values it can take are the same four as before.
|
|
4
|
+
|
|
5
|
+
## 2.30.2
|
|
6
|
+
|
|
7
|
+
- **Fix**: The cut-time shift of 2.28.0 is reverted — the field measured it and it moved the cuts OFF the source's keyframes rather than onto them. Of 75 pieces the picture produced afterwards, only **nine** began at a time the container's own table names, against **70 of 75** for the soundtrack, which the change never touched; the median distance from the playlist went from 0.04 s to 4.33 s. Before it, every piece began exactly on a named keyframe and it was the playlist that disagreed with them — which is the correction path's business, not the cut list's. The reasoning that produced the shift (that the muxer decides its cuts before the output is relabelled) was argued from ffmpeg's semantics rather than measured, and the measurement says otherwise.
|
|
8
|
+
- **Fix**: The steering line compared two different things. `steered onto N of M holders` summed the successes over every attempt of a wait while taking M from the last attempt alone, which is how the log came to read `steered onto 12 of 6 holders`. Both halves are now totals over the same attempts: `steered onto N of M asks (K peers held it)`.
|
|
9
|
+
|
|
1
10
|
## 2.30.1
|
|
2
11
|
|
|
3
12
|
- **Fix**: A seek was undone a second after it was made. Measured 2026-08-17: the viewer jumped to 2083.4 s, both runs restarted at segment #373 — correctly — and then a request for #371, issued by the player BEFORE the jump and reissued a second later, dragged the encoder back to #370. The viewer sat at #374 waiting for it to return. Two things let that happen, and both are fixed. The behind-head repair refuses a request that is behind the position the VIEWER themselves reported: its existing guard only holds while a seek is still settling, which by then it was not. And a segment request may no longer move the recorded viewer position BACKWARDS past a reported seek — playback only ever moves forward from one, so nothing legitimate is lost, while a stale request can no longer rewrite the viewer's own statement, which is how the repair came to believe it. A reported seek is the viewer stating where they are; a request is evidence about where the player is reading, and evidence may refine a statement forward, never contradict it backwards. Pinned by `test/stale-request-after-seek.test.js`, whose control case shows the same traffic still repairing a genuinely misplaced run when the viewer has said nothing.
|
package/package.json
CHANGED
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
ENCODE_RUN_STATE,
|
|
26
26
|
INITIAL_RUN_STATE,
|
|
27
27
|
nextState,
|
|
28
|
-
processCanBeSignalled
|
|
28
|
+
processCanBeSignalled,
|
|
29
|
+
wireState
|
|
29
30
|
} from "./encode-run-state.js";
|
|
30
31
|
import { ENCODE_EXIT, classifyEncodeExit } from "./encode-exit.js";
|
|
31
32
|
|
|
@@ -1854,17 +1855,23 @@ export class HlsSessionManager {
|
|
|
1854
1855
|
sourceMapKey,
|
|
1855
1856
|
fileName: logName,
|
|
1856
1857
|
dirPath: sessionDir,
|
|
1857
|
-
|
|
1858
|
+
// The SESSION's own lifetime, and nothing else: it exists, or it has been
|
|
1859
|
+
// disposed. It used to carry the encoder run's status as well, which is
|
|
1860
|
+
// why one line in the spawn path read `state === "disposed" ? "disposed"
|
|
1861
|
+
// : "starting"` — two lifetimes in one variable. The run's status lives
|
|
1862
|
+
// in `runState`.
|
|
1863
|
+
state: "live",
|
|
1858
1864
|
startedAt: Date.now(),
|
|
1859
1865
|
lastAccessedAt: Date.now(),
|
|
1860
1866
|
ffmpeg: null,
|
|
1861
1867
|
encodeRunGeneration: 0,
|
|
1862
1868
|
// What the ENCODER RUN is doing, as one control state from the table in
|
|
1863
|
-
// `encode-run-state.js`.
|
|
1864
|
-
//
|
|
1865
|
-
//
|
|
1866
|
-
// The
|
|
1867
|
-
//
|
|
1869
|
+
// `encode-run-state.js`. Every question about the run is now answered
|
|
1870
|
+
// from here: whether a process can be signalled, whether anything is
|
|
1871
|
+
// reading the input, what a missing segment is answered with, and what
|
|
1872
|
+
// the browser is told. The three representations it replaced — a status
|
|
1873
|
+
// string, a second status string on the wire, and a child-process handle
|
|
1874
|
+
// consulted at ten sites — could disagree with each other, and did.
|
|
1868
1875
|
runState: INITIAL_RUN_STATE,
|
|
1869
1876
|
lastError: "",
|
|
1870
1877
|
// Cold-start timing: entry timestamp + a once-guard so the first servable
|
|
@@ -2007,7 +2014,10 @@ export class HlsSessionManager {
|
|
|
2007
2014
|
seekFailureTarget: -1,
|
|
2008
2015
|
seekFailureCount: 0,
|
|
2009
2016
|
progress: {
|
|
2010
|
-
state
|
|
2017
|
+
// No `state` here. What the browser is told is `wireState(runState)`,
|
|
2018
|
+
// computed where it is sent — a Moore output rather than a field
|
|
2019
|
+
// maintained by hand at seven sites, which is how it came to be read
|
|
2020
|
+
// together with `session.state` under an `||`.
|
|
2011
2021
|
processedSeconds: 0,
|
|
2012
2022
|
startPositionSeconds: 0,
|
|
2013
2023
|
totalSeconds: hasDuration ? durationSeconds : null,
|
|
@@ -2099,7 +2109,7 @@ export class HlsSessionManager {
|
|
|
2099
2109
|
await this.waitUntilReady(session);
|
|
2100
2110
|
return session;
|
|
2101
2111
|
} catch (error) {
|
|
2102
|
-
if (session.
|
|
2112
|
+
if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
|
|
2103
2113
|
await this.disposeSession(session.id);
|
|
2104
2114
|
throw error;
|
|
2105
2115
|
}
|
|
@@ -3253,7 +3263,7 @@ export class HlsSessionManager {
|
|
|
3253
3263
|
if (
|
|
3254
3264
|
!session ||
|
|
3255
3265
|
session.state === "disposed" ||
|
|
3256
|
-
session.
|
|
3266
|
+
session.runState === ENCODE_RUN_STATE.ENDED_FAILED ||
|
|
3257
3267
|
!session.transcodeVideo ||
|
|
3258
3268
|
// Nothing is encoding, so there is no speed to judge. A variant the
|
|
3259
3269
|
// viewer has switched away from is left in exactly this state, and its
|
|
@@ -3567,30 +3577,22 @@ export class HlsSessionManager {
|
|
|
3567
3577
|
const gridCutTimes = explicitTimes && (!session.transcodeVideo || session.cutGrid === "keyframe")
|
|
3568
3578
|
? segmentCutTimesFrom(session.segmentBoundaries, safeIndex)
|
|
3569
3579
|
: null;
|
|
3570
|
-
//
|
|
3571
|
-
// timestamps, not against the labels we ask it to write. That branch keeps
|
|
3572
|
-
// the source's timestamps (`-copyts`) and re-labels the output 0-based with
|
|
3573
|
-
// `-output_ts_offset -sourceStartTime`; the cut list, being applied before
|
|
3574
|
-
// that relabelling, must therefore be stated in the SOURCE's terms.
|
|
3580
|
+
// Cut times are stated on the grid, for both branches.
|
|
3575
3581
|
//
|
|
3576
|
-
//
|
|
3577
|
-
//
|
|
3578
|
-
//
|
|
3579
|
-
//
|
|
3580
|
-
//
|
|
3581
|
-
//
|
|
3582
|
-
//
|
|
3583
|
-
//
|
|
3584
|
-
// playlist and the media drifted apart by a whole segment and the player
|
|
3585
|
-
// refetched what it could not place.
|
|
3582
|
+
// 2.28.0 added `sourceStartTime` to them on the copy branch, reasoning that
|
|
3583
|
+
// the muxer decides its cuts before the output is relabelled. The field
|
|
3584
|
+
// measured it the next session and the reasoning was wrong: of 75 pieces
|
|
3585
|
+
// the picture produced, only NINE began at a time the container's own
|
|
3586
|
+
// keyframe table names (the soundtrack, untouched by the change, scored 70
|
|
3587
|
+
// of 75). Before it, every piece began exactly on a named keyframe and it
|
|
3588
|
+
// was the PLAYLIST that disagreed with them. So the shift moved the cuts
|
|
3589
|
+
// OFF the keyframes rather than onto them, and it is gone.
|
|
3586
3590
|
//
|
|
3587
|
-
//
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
//
|
|
3591
|
-
const cutTimes = gridCutTimes
|
|
3592
|
-
? gridCutTimes.map((time) => Number((time + sourceStartTime).toFixed(6)))
|
|
3593
|
-
: gridCutTimes;
|
|
3591
|
+
// What remains true, and is what that measurement is really about: the
|
|
3592
|
+
// picture cuts where the source's keyframes are, and the playlist must be
|
|
3593
|
+
// built from those same times. That is the correction path's job, not the
|
|
3594
|
+
// cut list's.
|
|
3595
|
+
const cutTimes = gridCutTimes;
|
|
3594
3596
|
|
|
3595
3597
|
// A second chance for a predecessor that survived the escalation above —
|
|
3596
3598
|
// the first block is the one that does the work. Its exit is ignored
|
|
@@ -3820,8 +3822,6 @@ export class HlsSessionManager {
|
|
|
3820
3822
|
session.encodeStartIndex = safeIndex;
|
|
3821
3823
|
session.pendingRestartIndex = -1;
|
|
3822
3824
|
session.lastRestartAt = Date.now();
|
|
3823
|
-
session.state = session.state === "disposed" ? "disposed" : "starting";
|
|
3824
|
-
session.progress.state = "running";
|
|
3825
3825
|
session.progress.processedSeconds = startSeconds;
|
|
3826
3826
|
session.progress.startPositionSeconds = startSeconds;
|
|
3827
3827
|
session.progress.updatedAt = Date.now();
|
|
@@ -3920,7 +3920,6 @@ export class HlsSessionManager {
|
|
|
3920
3920
|
} else if (key === "speed") {
|
|
3921
3921
|
session.progress.speed = value;
|
|
3922
3922
|
} else if (key === "progress") {
|
|
3923
|
-
session.progress.state = value === "end" ? "ready" : "running";
|
|
3924
3923
|
}
|
|
3925
3924
|
const metrics = computeProgressMetrics(
|
|
3926
3925
|
session.progress.processedSeconds,
|
|
@@ -3956,9 +3955,7 @@ export class HlsSessionManager {
|
|
|
3956
3955
|
if (!this.#isCurrentRun(session, ffmpeg)) {
|
|
3957
3956
|
return;
|
|
3958
3957
|
}
|
|
3959
|
-
session.state = "failed";
|
|
3960
3958
|
session.lastError = error instanceof Error ? error.message : String(error);
|
|
3961
|
-
session.progress.state = "failed";
|
|
3962
3959
|
session.progress.updatedAt = Date.now();
|
|
3963
3960
|
this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_FAILED);
|
|
3964
3961
|
logger.error(`ffmpeg ${session.id} process error: ${session.lastError}`);
|
|
@@ -3997,9 +3994,7 @@ export class HlsSessionManager {
|
|
|
3997
3994
|
// segment nobody was making. So the claim is checked against the
|
|
3998
3995
|
// playlist we published, and a run that stopped short is a FAILURE that
|
|
3999
3996
|
// can be restarted, not a finished file.
|
|
4000
|
-
|
|
4001
|
-
session.progress.state = "failed";
|
|
4002
|
-
session.progress.updatedAt = Date.now();
|
|
3997
|
+
session.progress.updatedAt = Date.now();
|
|
4003
3998
|
session.lastError =
|
|
4004
3999
|
`input ended after segment #${producedThrough} of ${expectedLast} — ` +
|
|
4005
4000
|
"the source stopped delivering data";
|
|
@@ -4011,8 +4006,6 @@ export class HlsSessionManager {
|
|
|
4011
4006
|
return;
|
|
4012
4007
|
}
|
|
4013
4008
|
if (outcome === ENCODE_EXIT.COMPLETE) {
|
|
4014
|
-
session.state = "ready";
|
|
4015
|
-
session.progress.state = "ready";
|
|
4016
4009
|
session.progress.updatedAt = Date.now();
|
|
4017
4010
|
this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_COMPLETE);
|
|
4018
4011
|
logger.info(`transcode ${session.id} encode-run complete "${session.fileName}"`);
|
|
@@ -4074,11 +4067,9 @@ export class HlsSessionManager {
|
|
|
4074
4067
|
// stays for what it was built for, a target that genuinely cannot be
|
|
4075
4068
|
// encoded; it must not condemn a session whose data merely went away.
|
|
4076
4069
|
if (outcome === ENCODE_EXIT.INPUT_LOST) {
|
|
4077
|
-
session.state = "recovering";
|
|
4078
4070
|
// On the wire it is simply "not ready yet" — a state the browser has
|
|
4079
4071
|
// always known how to wait through. Only the proxy needs the
|
|
4080
4072
|
// distinction between waiting for data and having given up.
|
|
4081
|
-
session.progress.state = "starting";
|
|
4082
4073
|
session.progress.updatedAt = Date.now();
|
|
4083
4074
|
this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_INPUT_LOST);
|
|
4084
4075
|
session.inputRetryCount = (session.inputRetryCount ?? 0) + 1;
|
|
@@ -4093,7 +4084,7 @@ export class HlsSessionManager {
|
|
|
4093
4084
|
);
|
|
4094
4085
|
session.inputRetryTimer = setTimeout(() => {
|
|
4095
4086
|
session.inputRetryTimer = null;
|
|
4096
|
-
if (session.
|
|
4087
|
+
if (session.runState !== ENCODE_RUN_STATE.RETRY_WAIT) {
|
|
4097
4088
|
return;
|
|
4098
4089
|
}
|
|
4099
4090
|
this.#transitionRun(session, ENCODE_RUN_EVENT.RETRY_DUE);
|
|
@@ -4105,8 +4096,6 @@ export class HlsSessionManager {
|
|
|
4105
4096
|
session.inputRetryTimer.unref?.();
|
|
4106
4097
|
return;
|
|
4107
4098
|
}
|
|
4108
|
-
session.state = "failed";
|
|
4109
|
-
session.progress.state = "failed";
|
|
4110
4099
|
session.progress.updatedAt = Date.now();
|
|
4111
4100
|
this.#transitionRun(session, ENCODE_RUN_EVENT.EXITED_FAILED);
|
|
4112
4101
|
logger.error(
|
|
@@ -4587,10 +4576,9 @@ export class HlsSessionManager {
|
|
|
4587
4576
|
// Individual segments are long-polled by the segment route as ffmpeg
|
|
4588
4577
|
// produces them.
|
|
4589
4578
|
if (session.useSyntheticPlaylist) {
|
|
4590
|
-
if (session.
|
|
4579
|
+
if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
|
|
4591
4580
|
throw new Error(session.lastError || "ffmpeg failed to start HLS session.");
|
|
4592
4581
|
}
|
|
4593
|
-
session.state = "ready";
|
|
4594
4582
|
return;
|
|
4595
4583
|
}
|
|
4596
4584
|
|
|
@@ -4598,15 +4586,14 @@ export class HlsSessionManager {
|
|
|
4598
4586
|
const deadline = Date.now() + this.startupWaitMs;
|
|
4599
4587
|
|
|
4600
4588
|
while (Date.now() < deadline) {
|
|
4601
|
-
if (session.
|
|
4589
|
+
if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
|
|
4602
4590
|
throw new Error(session.lastError || "ffmpeg failed to start HLS session.");
|
|
4603
4591
|
}
|
|
4604
4592
|
try {
|
|
4605
4593
|
await access(playlistPath);
|
|
4606
4594
|
const text = await readFile(playlistPath, "utf8");
|
|
4607
4595
|
if (text.includes("#EXTM3U")) {
|
|
4608
|
-
|
|
4609
|
-
return;
|
|
4596
|
+
return;
|
|
4610
4597
|
}
|
|
4611
4598
|
} catch (_error) {
|
|
4612
4599
|
// Playlist is not ready yet.
|
|
@@ -5122,7 +5109,14 @@ export class HlsSessionManager {
|
|
|
5122
5109
|
const median = deviations.length > 0 ? deviations[Math.floor(deviations.length / 2)] : 0;
|
|
5123
5110
|
const landed = check.landedOnAnotherKeyframe ?? 0;
|
|
5124
5111
|
logger.info(
|
|
5125
|
-
|
|
5112
|
+
// The session id, because without it this line cannot be attributed. A
|
|
5113
|
+
// family produces one summary per member — the picture and each
|
|
5114
|
+
// soundtrack — and on 2026-08-17 the picture's was read as the sound's,
|
|
5115
|
+
// from a neighbouring log line, and a roadmap item was written against
|
|
5116
|
+
// the wrong half of the stream. The id is the only thing that says whose
|
|
5117
|
+
// reading this is.
|
|
5118
|
+
`keyframe-index ${session.id.slice(0, 8)} ${session.audioOnly === true ? "sound" : "picture"} ` +
|
|
5119
|
+
`${session.containerFormat || "unknown"} "${session.fileName}": ` +
|
|
5126
5120
|
`${check.disagreed} of ${check.checked} produced segments started away from the playlist, ` +
|
|
5127
5121
|
`median ${median.toFixed(3)}s worst ${check.maxDeviationSec.toFixed(3)}s` +
|
|
5128
5122
|
(check.firstDisagreementIndex >= 0 ? ` (first at #${check.firstDisagreementIndex})` : "") +
|
|
@@ -5382,7 +5376,7 @@ export class HlsSessionManager {
|
|
|
5382
5376
|
if (
|
|
5383
5377
|
!session ||
|
|
5384
5378
|
session.state === "disposed" ||
|
|
5385
|
-
session.
|
|
5379
|
+
session.runState === ENCODE_RUN_STATE.ENDED_FAILED ||
|
|
5386
5380
|
!session.ffmpeg ||
|
|
5387
5381
|
session.runState === ENCODE_RUN_STATE.SUSPENDED
|
|
5388
5382
|
) {
|
|
@@ -6931,13 +6925,13 @@ export class HlsSessionManager {
|
|
|
6931
6925
|
if (!session || !isSafeFileName(fileName, session.segmentFormat)) {
|
|
6932
6926
|
return { kind: "not-found" };
|
|
6933
6927
|
}
|
|
6934
|
-
if (session.
|
|
6928
|
+
if (session.runState === ENCODE_RUN_STATE.RETRY_WAIT) {
|
|
6935
6929
|
// The data went away and is being fetched again. Holding the request is
|
|
6936
6930
|
// the truthful answer: nothing is broken and there is nothing for the
|
|
6937
6931
|
// viewer to retry.
|
|
6938
6932
|
return { kind: "warming-up" };
|
|
6939
6933
|
}
|
|
6940
|
-
if (session.
|
|
6934
|
+
if (session.runState === ENCODE_RUN_STATE.ENDED_FAILED) {
|
|
6941
6935
|
return {
|
|
6942
6936
|
kind: "failed",
|
|
6943
6937
|
message: session.lastError || "ffmpeg failed for this transcode session."
|
|
@@ -7504,7 +7498,11 @@ export class HlsSessionManager {
|
|
|
7504
7498
|
session.lastAccessedAt = Date.now();
|
|
7505
7499
|
const warmupTotalSeconds = this.startupWaitMs / 1000;
|
|
7506
7500
|
const warmupElapsedSeconds = Math.max(0, (Date.now() - session.startedAt) / 1000);
|
|
7507
|
-
|
|
7501
|
+
// One question, one answer. This used to read BOTH strings with `||`
|
|
7502
|
+
// because neither could answer alone: `session.state` said "starting" from
|
|
7503
|
+
// the first spawn until something else overwrote it, and `progress.state`
|
|
7504
|
+
// said it again on its own schedule.
|
|
7505
|
+
const isWarmupPhase = wireState(session.runState) === "starting";
|
|
7508
7506
|
const warmupPercent = isWarmupPhase
|
|
7509
7507
|
? Math.max(0, Math.min(100, (warmupElapsedSeconds / warmupTotalSeconds) * 100))
|
|
7510
7508
|
: null;
|
|
@@ -7522,7 +7520,7 @@ export class HlsSessionManager {
|
|
|
7522
7520
|
// The id the caller asked about, not the variant it was answered from —
|
|
7523
7521
|
// the browser tracks its sessions by the id it was given.
|
|
7524
7522
|
sessionId: named.id,
|
|
7525
|
-
state: session.
|
|
7523
|
+
state: wireState(session.runState),
|
|
7526
7524
|
processedSeconds: session.progress.processedSeconds,
|
|
7527
7525
|
startPositionSeconds: session.progress.startPositionSeconds ?? 0,
|
|
7528
7526
|
totalSeconds: session.progress.totalSeconds,
|
|
@@ -7562,7 +7560,7 @@ export class HlsSessionManager {
|
|
|
7562
7560
|
expectedSessionCreateMs: this.expectedSessionCreateMs(),
|
|
7563
7561
|
expectedFirstSegmentMs: this.expectedFirstSegmentMs(),
|
|
7564
7562
|
updatedAt: session.progress.updatedAt,
|
|
7565
|
-
error: session.
|
|
7563
|
+
error: session.runState === ENCODE_RUN_STATE.ENDED_FAILED ? session.lastError : ""
|
|
7566
7564
|
};
|
|
7567
7565
|
}
|
|
7568
7566
|
|
|
@@ -87,7 +87,7 @@ function speedOf(wire) {
|
|
|
87
87
|
* @param {object} torrent
|
|
88
88
|
* @param {number} pieceIndex
|
|
89
89
|
* @param {number} [limit] - How many wires to push it onto.
|
|
90
|
-
* @returns {{ asked: number, considered: number, fastestBytesPerSecond: number }}
|
|
90
|
+
* @returns {{ asked: number, attempted: number, considered: number, fastestBytesPerSecond: number }}
|
|
91
91
|
* `asked` counts requests the library actually placed: it refuses when a
|
|
92
92
|
* wire's pipeline is full or when nothing can be reserved even with hotswap,
|
|
93
93
|
* and that refusal is information — a piece nobody can be asked for is
|
|
@@ -95,7 +95,7 @@ function speedOf(wire) {
|
|
|
95
95
|
*/
|
|
96
96
|
export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
|
|
97
97
|
if (!canPlaceRequests(torrent) || !Number.isInteger(pieceIndex) || pieceIndex < 0) {
|
|
98
|
-
return { asked: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
98
|
+
return { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
99
99
|
}
|
|
100
100
|
const candidates = wiresForPiece(torrent, pieceIndex);
|
|
101
101
|
let asked = 0;
|
|
@@ -116,6 +116,13 @@ export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
|
|
|
116
116
|
return {
|
|
117
117
|
asked,
|
|
118
118
|
considered: candidates.length,
|
|
119
|
+
// How many of the asks the library placed, against how many it was asked
|
|
120
|
+
// for. The caller sums these over the whole wait, and summing `asked`
|
|
121
|
+
// against a `considered` taken from the LAST attempt is how the field log
|
|
122
|
+
// came to read "steered onto 12 of 6 holders" — a ratio of two different
|
|
123
|
+
// things. Both halves are returned per attempt so the caller can add each
|
|
124
|
+
// to its own total.
|
|
125
|
+
attempted: Math.min(candidates.length, Math.max(1, limit)),
|
|
119
126
|
fastestBytesPerSecond: candidates.length > 0 ? speedOf(candidates[0]) : 0
|
|
120
127
|
};
|
|
121
128
|
}
|
|
@@ -517,12 +517,15 @@ export async function* readFragments({
|
|
|
517
517
|
// holder delivers — measured 2026-08-17, the swarm had a fivefold surplus
|
|
518
518
|
// of bandwidth and the reader still waited 1.0-4.5 s, 47 times in two
|
|
519
519
|
// minutes, on pieces five peers already had.
|
|
520
|
-
let pushed = { asked: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
520
|
+
let pushed = { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
521
521
|
const pushToFastest = () => {
|
|
522
522
|
try {
|
|
523
523
|
const result = askFastestWiresFor(torrent, pieceIndex);
|
|
524
524
|
pushed = {
|
|
525
525
|
asked: pushed.asked + result.asked,
|
|
526
|
+
// Summed like the successes, so the line compares two totals over
|
|
527
|
+
// the same attempts instead of a total against a snapshot.
|
|
528
|
+
attempted: (pushed.attempted ?? 0) + result.attempted,
|
|
526
529
|
considered: result.considered,
|
|
527
530
|
fastestBytesPerSecond: result.fastestBytesPerSecond
|
|
528
531
|
};
|
|
@@ -592,7 +595,7 @@ export async function* readFragments({
|
|
|
592
595
|
// What WE did about it, so the next session says whether steering
|
|
593
596
|
// the piece onto faster holders shortens the tail — by number
|
|
594
597
|
// rather than by impression.
|
|
595
|
-
`; steered onto ${pushed.asked} of ${pushed.considered}
|
|
598
|
+
`; steered onto ${pushed.asked} of ${pushed.attempted} asks (${pushed.considered} peers held it)` +
|
|
596
599
|
(pushed.fastestBytesPerSecond > 0
|
|
597
600
|
? `, fastest ${Math.round(pushed.fastestBytesPerSecond / 1024)}KB/s`
|
|
598
601
|
: "")
|
|
@@ -97,7 +97,7 @@ test("a build without the request entry is reported, not silently skipped", () =
|
|
|
97
97
|
assert.equal(canPlaceRequests({ wires: [] }), false);
|
|
98
98
|
assert.equal(canPlaceRequests({ wires: [], _request: () => true }), true);
|
|
99
99
|
const result = askFastestWiresFor({ wires: [wire({ speed: 1 })] }, 1);
|
|
100
|
-
assert.deepEqual(result, { asked: 0, considered: 0, fastestBytesPerSecond: 0 });
|
|
100
|
+
assert.deepEqual(result, { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test("a wire that cannot say how fast it is ranks last rather than throwing", () => {
|