@torrent-tv/proxy 2.84.0 → 2.85.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 +13 -0
- package/package.json +1 -1
- package/routes/api/link-probe/get.js +51 -0
- package/routes/api/transcode-sessions/net-report/post.js +84 -64
- package/routes/transcode/session-file/get.js +268 -240
- package/server.js +3 -0
- package/services/data-channel-handler.js +6 -1
- package/services/delivery-probe.js +842 -828
- package/services/hls-session-manager.js +14 -32
- package/services/priority/PriorityOrchestrator.js +11 -7
- package/services/viewer/Viewer.js +122 -53
- package/services/viewer/report-intake.js +63 -0
- package/test/link-probe.test.js +56 -0
- package/test/plan-is-steady.test.js +225 -223
- package/test/priority-map-emptied.test.js +0 -2
- package/test/viewer-states.test.js +151 -0
- package/test/viewer.test.js +24 -3
|
@@ -23,7 +23,6 @@ import { speedFromReadings } from "./encoder-readings.js";
|
|
|
23
23
|
import { availableShareFrom } from "./available-share.js";
|
|
24
24
|
import { contentionPenalty } from "./encode/contention.js";
|
|
25
25
|
import { minimumBufferFrom } from "./supply-margin.js";
|
|
26
|
-
import { earliestUrgentSecond, runsOf } from "./priority/PriorityMap.js";
|
|
27
26
|
import { PriorityOrchestrator } from "./priority/PriorityOrchestrator.js";
|
|
28
27
|
import { baseDrawFrom, costPerMegabyteFrom } from "./torrent-cost.js";
|
|
29
28
|
import { medianOf, movedBeyondScatter, scatterOf } from "./learned-median.js";
|
|
@@ -90,6 +89,7 @@ import {
|
|
|
90
89
|
export { ffmpegSeconds, onKeyframeGridFor, seekLandingOffsetFor, segmentCutTimesFrom };
|
|
91
90
|
import { viewersOf } from "./viewer/Viewer.js";
|
|
92
91
|
import { earliestViewerSecondsOn, viewerSecondsOn, viewerSegmentsOn } from "./viewer/positions.js";
|
|
92
|
+
import { takeViewerReport } from "./viewer/report-intake.js";
|
|
93
93
|
import { Viewers } from "./viewer/Viewers.js";
|
|
94
94
|
import { LiveOutputs } from "./output/LiveOutputs.js";
|
|
95
95
|
import { variantHeightsFor } from "./output/ladder.js";
|
|
@@ -3070,40 +3070,22 @@ export class HlsSessionManager {
|
|
|
3070
3070
|
* @param {{ linkMbps: number, bufferedAheadSec: number, consumerId?: string, positionSeconds?: number }} report
|
|
3071
3071
|
* @returns {boolean}
|
|
3072
3072
|
*/
|
|
3073
|
-
recordNetReport(sessionId,
|
|
3073
|
+
recordNetReport(sessionId, report) {
|
|
3074
3074
|
const named = this.sessionsById.get(sessionId);
|
|
3075
3075
|
if (!named || named.state === "disposed") {
|
|
3076
3076
|
return false;
|
|
3077
3077
|
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
this.viewers
|
|
3090
|
-
.of(session, typeof consumerId === "string" && consumerId.length > 0 ? consumerId : "")
|
|
3091
|
-
.report({ linkMbps, bufferedAheadSec, positionSeconds, playing, onScreen, inPictureInPicture }, now);
|
|
3092
|
-
// A stale reading must not go on deciding for the viewers still here: a
|
|
3093
|
-
// report describes a link at a moment, and a viewer who seeked since then
|
|
3094
|
-
// is somewhere else entirely.
|
|
3095
|
-
//
|
|
3096
|
-
// Only the READING expires. Whether the person is still watching is a
|
|
3097
|
-
// different question with its own answer — their connection — and a viewer
|
|
3098
|
-
// who has simply stopped reporting is not thereby gone. Answering both from
|
|
3099
|
-
// this one place is what stopped a soundtrack's encoder on 2026-09-05.
|
|
3100
|
-
for (const viewer of viewersOf(session).values()) {
|
|
3101
|
-
const report = viewer.netReport;
|
|
3102
|
-
if (report !== null && now - report.at > LINK_REPORT_FRESH_MS) {
|
|
3103
|
-
viewer.netReport = null;
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
return true;
|
|
3078
|
+
const consumerId = typeof report?.consumerId === "string" ? report.consumerId : "";
|
|
3079
|
+
return takeViewerReport({
|
|
3080
|
+
viewers: this.viewers,
|
|
3081
|
+
// The stream on screen is the reporter's own: with two viewers on two
|
|
3082
|
+
// rungs, one report says nothing about the other's encoder.
|
|
3083
|
+
session: this.#activeVariant(named, consumerId),
|
|
3084
|
+
consumerId,
|
|
3085
|
+
report,
|
|
3086
|
+
now: Date.now(),
|
|
3087
|
+
linkFreshMs: LINK_REPORT_FRESH_MS
|
|
3088
|
+
});
|
|
3107
3089
|
}
|
|
3108
3090
|
|
|
3109
3091
|
/**
|
|
@@ -7377,7 +7359,7 @@ export class HlsSessionManager {
|
|
|
7377
7359
|
* earliest. Reading it off a viewer put a person inside the encoding layer,
|
|
7378
7360
|
* and then every question about WHICH person had to be answered a second time
|
|
7379
7361
|
* here: the earliest of two, the one on this rung, the one who has not
|
|
7380
|
-
* reported yet.
|
|
7362
|
+
* reported yet.
|
|
7381
7363
|
*
|
|
7382
7364
|
* The apparatus this replaced is worth naming so it is not rebuilt: three
|
|
7383
7365
|
* position sources ranked by priority, a function saying which had answered,
|
|
@@ -223,13 +223,17 @@ export class PriorityOrchestrator {
|
|
|
223
223
|
}
|
|
224
224
|
const stated = {
|
|
225
225
|
atSeconds: viewer.positionSeconds(now) ?? 0,
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
// WATCHING OR WAITING, not merely playing. Three states reach this
|
|
227
|
+
// one question: a viewer whose picture is advancing, a viewer
|
|
228
|
+
// blocked on material we owe them, and a viewer who stopped it
|
|
229
|
+
// themselves. The first two want the film in front of them NOW and
|
|
230
|
+
// share the upper band; only the third can wait. A page that is not
|
|
231
|
+
// on screen has its timers throttled and asks for nothing, which is
|
|
232
|
+
// indistinguishable from a full cushion, so it is not consuming
|
|
233
|
+
// either — told apart on the page, folded into one question here.
|
|
234
|
+
playing: typeof viewer.wantsFilmNow === "function"
|
|
235
|
+
? viewer.wantsFilmNow()
|
|
236
|
+
: viewer.playing === true || viewer.waiting === true
|
|
233
237
|
};
|
|
234
238
|
held.viewers.push(stated);
|
|
235
239
|
if (this.#watchedBy(session, viewer)) {
|
|
@@ -149,13 +149,35 @@ export class Viewer {
|
|
|
149
149
|
* @type {Set<string>}
|
|
150
150
|
*/
|
|
151
151
|
this.wantsCuesFor = new Set();
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
152
|
+
// WHETHER THE PICTURE IS MOVING, and separately WHETHER THIS VIEWER IS
|
|
153
|
+
// BLOCKED ON US. Two booleans, because there are three states and one
|
|
154
|
+
// boolean cannot hold them:
|
|
155
|
+
//
|
|
156
|
+
// playing the picture advances, a second of film per second;
|
|
157
|
+
// waiting it does not advance because we have not delivered;
|
|
158
|
+
// neither the viewer stopped it themselves.
|
|
159
|
+
//
|
|
160
|
+
// Collapsed into one, the middle state read as the third: a viewer frozen
|
|
161
|
+
// for want of a segment counted as somebody who had chosen to stop, so
|
|
162
|
+
// nothing in front of them fell due and the work went to whoever was
|
|
163
|
+
// playing. That is backwards — a viewer waiting on us is the most urgent
|
|
164
|
+
// there is. The page tells the two apart exactly (its element says whether
|
|
165
|
+
// the picture advances, and the component owns the viewer's own pause) and
|
|
166
|
+
// states both.
|
|
167
|
+
//
|
|
168
|
+
// `playing` STARTS FALSE, and that is the whole of the fault of
|
|
169
|
+
// 2026-09-14. It used to start true, and the position is carried forward by
|
|
170
|
+
// the clock at that rate, so a viewer who had never said anything walked
|
|
171
|
+
// 146 seconds into a film they had not begun: the tab was hidden, the page
|
|
172
|
+
// deliberately held start-up until it was shown, and this end assumed the
|
|
173
|
+
// film was running the entire time. A statement about somebody else's
|
|
174
|
+
// machine is theirs to make. Until they make it the rate is zero.
|
|
175
|
+
//
|
|
176
|
+
// `waiting` STARTS TRUE, which is what a viewer arriving is: they have
|
|
177
|
+
// asked for a film and hold none of it. So their work is made first from
|
|
178
|
+
// the instant they arrive, without the position ever being invented.
|
|
179
|
+
this.playing = false;
|
|
180
|
+
this.waiting = true;
|
|
159
181
|
// Whether the page carrying this viewer is ON SCREEN, and whether the
|
|
160
182
|
// picture has been pulled out of it.
|
|
161
183
|
//
|
|
@@ -171,7 +193,9 @@ export class Viewer {
|
|
|
171
193
|
// before it could say otherwise.
|
|
172
194
|
this.onScreen = true;
|
|
173
195
|
this.inPictureInPicture = false;
|
|
174
|
-
// Seconds of film held ahead of the picture, as the page last said.
|
|
196
|
+
// Seconds of film held ahead of the picture, as the page last said. It is
|
|
197
|
+
// measured at the moment the position beside it was, which is what makes it
|
|
198
|
+
// the bound on carrying that position forward — see `positionSeconds`.
|
|
175
199
|
this.bufferedSeconds = null;
|
|
176
200
|
}
|
|
177
201
|
|
|
@@ -192,6 +216,13 @@ export class Viewer {
|
|
|
192
216
|
return;
|
|
193
217
|
}
|
|
194
218
|
this.position = { seconds, at: now };
|
|
219
|
+
// WHAT THEY HELD AT THE PLACE THEY LEFT SAYS NOTHING ABOUT THE PLACE THEY
|
|
220
|
+
// ARRIVED AT. A seek empties the buffer by construction — the player
|
|
221
|
+
// discards what it holds and fetches from the new position — so carrying
|
|
222
|
+
// the old cushion over would license the position to run forward again from
|
|
223
|
+
// material that no longer exists. Zero is the truthful floor until they say
|
|
224
|
+
// otherwise, and a viewer who has just seeked says so within a tick.
|
|
225
|
+
this.bufferedSeconds = 0;
|
|
195
226
|
this.lastSeenAt = now;
|
|
196
227
|
}
|
|
197
228
|
|
|
@@ -205,13 +236,18 @@ export class Viewer {
|
|
|
205
236
|
* places, two of them on a session shared with other people.
|
|
206
237
|
*
|
|
207
238
|
* @param {object} report
|
|
208
|
-
* @param {number} report.linkMbps
|
|
239
|
+
* @param {number} [report.linkMbps] - What their link last measured. Absent
|
|
240
|
+
* until something measurable has crossed it, and then the rest of the
|
|
241
|
+
* report still stands: this is a statement about a viewer, not about a link.
|
|
209
242
|
* @param {number} report.bufferedAheadSec
|
|
210
243
|
* @param {number | null} [report.positionSeconds] - Null from a page that
|
|
211
244
|
* does not say; then the position stands as it was.
|
|
212
|
-
* @param {boolean} [report.playing] -
|
|
213
|
-
*
|
|
214
|
-
*
|
|
245
|
+
* @param {boolean} [report.playing] - Whether the picture is advancing.
|
|
246
|
+
* Absent means it was not stated, and nothing may be assumed about somebody
|
|
247
|
+
* else's machine: the rate is then zero.
|
|
248
|
+
* @param {boolean} [report.waiting] - Whether this viewer is blocked on
|
|
249
|
+
* material we owe them. Absent from a page one release behind; then it is
|
|
250
|
+
* derived from the two facts that page does state.
|
|
215
251
|
* @param {boolean} [report.onScreen] - Whether the page is visible, or the
|
|
216
252
|
* picture is in picture-in-picture. Absent means on screen.
|
|
217
253
|
* @param {boolean} [report.inPictureInPicture] - Whether the picture has been
|
|
@@ -224,47 +260,49 @@ export class Viewer {
|
|
|
224
260
|
bufferedAheadSec,
|
|
225
261
|
positionSeconds = null,
|
|
226
262
|
playing,
|
|
263
|
+
waiting,
|
|
227
264
|
onScreen,
|
|
228
265
|
inPictureInPicture
|
|
229
266
|
},
|
|
230
267
|
now = Date.now()
|
|
231
268
|
) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
const held = Number.isFinite(bufferedAheadSec) && bufferedAheadSec > 0 ? bufferedAheadSec : 0;
|
|
270
|
+
// A LINK FIGURE IS ONE FIELD OF THIS REPORT AND NOT ITS TICKET. A page that
|
|
271
|
+
// has transferred nothing measurable has nothing to say about its link and
|
|
272
|
+
// everything to say about its viewer, and that is the cold open exactly —
|
|
273
|
+
// the moment the position matters most. Held as a precondition, in three
|
|
274
|
+
// places at once, it silenced every statement of the session of 2026-09-14.
|
|
275
|
+
if (Number.isFinite(linkMbps) && linkMbps > 0) {
|
|
276
|
+
this.netReport = {
|
|
277
|
+
linkMbps,
|
|
278
|
+
bufferedAheadSec: held,
|
|
279
|
+
positionSeconds:
|
|
280
|
+
Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : null,
|
|
281
|
+
at: now
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// The position first, then the cushion: `moveTo` clears what was held at
|
|
285
|
+
// wherever they were before, and this report's own figure is measured at
|
|
286
|
+
// the position this report states.
|
|
243
287
|
if (Number.isFinite(positionSeconds) && positionSeconds >= 0) {
|
|
244
288
|
this.moveTo(/** @type {number} */ (positionSeconds), now);
|
|
245
289
|
}
|
|
290
|
+
this.bufferedSeconds = held;
|
|
291
|
+
this.playing = playing === true;
|
|
292
|
+
// A page that states `playing` and not `waiting` is one release behind this
|
|
293
|
+
// proxy, which is the ordinary state of a rolling pool — the proxy ships
|
|
294
|
+
// first by rule. It says `false` for both a viewer who stopped the picture
|
|
295
|
+
// and a viewer starved of material, so the two are told apart by the one
|
|
296
|
+
// quantity it does state: a stopped viewer holding film chose to stop, a
|
|
297
|
+
// stopped viewer holding nothing is waiting on us. It errs toward making
|
|
298
|
+
// material, which is the direction that costs an encoder rather than a
|
|
299
|
+
// viewer.
|
|
300
|
+
this.waiting = waiting === undefined ? this.playing === false && held === 0 : waiting === true;
|
|
301
|
+
this.inPictureInPicture = inPictureInPicture === undefined ? false : Boolean(inPictureInPicture);
|
|
302
|
+
this.onScreen = onScreen === undefined ? true : Boolean(onScreen);
|
|
246
303
|
this.seen(now);
|
|
247
304
|
}
|
|
248
305
|
|
|
249
|
-
/**
|
|
250
|
-
* When this viewer runs out of what they hold, in milliseconds.
|
|
251
|
-
*
|
|
252
|
-
* Film is consumed at one second per second while the picture moves, so the
|
|
253
|
-
* moment they run dry is now plus what they hold. Stopped, they consume
|
|
254
|
-
* nothing and there is no such moment — which is why a pause needs no rule of
|
|
255
|
-
* its own anywhere: it falls out of this as an absent deadline.
|
|
256
|
-
*
|
|
257
|
-
* @param {number} [now]
|
|
258
|
-
* @returns {number | null}
|
|
259
|
-
*/
|
|
260
|
-
deadlineAt(now = Date.now()) {
|
|
261
|
-
if (!this.playing) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
const held = Number.isFinite(this.bufferedSeconds) ? Math.max(0, this.bufferedSeconds) : 0;
|
|
265
|
-
return now + held * 1000;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
306
|
/**
|
|
269
307
|
* Whether this viewer is here.
|
|
270
308
|
*
|
|
@@ -284,20 +322,28 @@ export class Viewer {
|
|
|
284
322
|
* @returns {boolean}
|
|
285
323
|
*/
|
|
286
324
|
/**
|
|
287
|
-
* Whether this viewer
|
|
325
|
+
* Whether this viewer wants film NOW — either watching it or waiting for it.
|
|
288
326
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* due, so the work goes to whoever is watching — and both leave them a place
|
|
292
|
-
* in the priority map, because they are still there and will want it again.
|
|
327
|
+
* The one predicate every reading about urgency asks, and it is a union of
|
|
328
|
+
* two states rather than the negation of a pause:
|
|
293
329
|
*
|
|
330
|
+
* playing consuming, and will run dry when their cushion does;
|
|
331
|
+
* waiting consuming nothing only because we have delivered nothing;
|
|
332
|
+
* neither they stopped the picture themselves.
|
|
333
|
+
*
|
|
334
|
+
* A viewer who has stopped it keeps their place in the priority map — they
|
|
335
|
+
* are still there and will want the film in front of them again — but nothing
|
|
336
|
+
* of theirs falls due, so the work goes to whoever is watching or waiting.
|
|
337
|
+
*
|
|
338
|
+
* A page that is not on screen is not consuming: its timers are throttled and
|
|
339
|
+
* it asks for nothing, which is indistinguishable from a full cushion.
|
|
294
340
|
* Picture-in-picture is watching with the tab hidden, and the page folds that
|
|
295
|
-
* into `onScreen` before
|
|
341
|
+
* into `onScreen` before saying it, so it needs no case of its own here.
|
|
296
342
|
*
|
|
297
343
|
* @returns {boolean}
|
|
298
344
|
*/
|
|
299
|
-
|
|
300
|
-
return this.playing
|
|
345
|
+
wantsFilmNow() {
|
|
346
|
+
return (this.playing === true || this.waiting === true) && this.onScreen !== false;
|
|
301
347
|
}
|
|
302
348
|
|
|
303
349
|
isPresent() {
|
|
@@ -323,7 +369,29 @@ export class Viewer {
|
|
|
323
369
|
* since. A viewer whose picture is stopped covers nothing and stays where
|
|
324
370
|
* they are, which is the same formula with the rate at zero.
|
|
325
371
|
*
|
|
326
|
-
* position(now) = stated + rate * (now - statedAt),
|
|
372
|
+
* position(now) = stated + rate * min(now - statedAt, held),
|
|
373
|
+
* rate = playing ? 1 : 0
|
|
374
|
+
*
|
|
375
|
+
* **THE SECOND TERM IS BOUNDED BY WHAT THEY HELD**, and the bound is stated
|
|
376
|
+
* by the viewer rather than chosen here: nobody can play past the end of what
|
|
377
|
+
* they have. A viewer who said "I hold thirty seconds" and has said nothing
|
|
378
|
+
* for two minutes has played at most thirty of those seconds; the rest of the
|
|
379
|
+
* silence is a viewer who ran dry, or one we are no longer hearing from, and
|
|
380
|
+
* in both cases they are not where the clock alone would put them.
|
|
381
|
+
*
|
|
382
|
+
* Unbounded, it walks a viewer off the end of a film they never started. On
|
|
383
|
+
* 2026-09-14 the tab was hidden for 145 seconds while the page deliberately
|
|
384
|
+
* held start-up until it was shown, the rate was assumed to be one, and this
|
|
385
|
+
* end placed the viewer at 145.979 s — the exact `-ss` the soundtrack's
|
|
386
|
+
* encoder was given, while the browser was asking for segment #0 and getting
|
|
387
|
+
* 503 for sixty seconds. Their cushion was 0.0 s throughout, so this bound
|
|
388
|
+
* alone would have held them at zero whatever the rate said.
|
|
389
|
+
*
|
|
390
|
+
* The bound binds only where the clock has outrun the cushion — a report lost,
|
|
391
|
+
* or a viewer starving — and there it under-states the position. That is the
|
|
392
|
+
* safe direction and deliberately so: under-stating makes material a viewer
|
|
393
|
+
* has already passed, which costs an encoder; over-stating skips material
|
|
394
|
+
* they are about to need, which costs the viewer the picture.
|
|
327
395
|
*
|
|
328
396
|
* Held as a stored number instead, it was a staircase: flat for the ten
|
|
329
397
|
* seconds between reports and then a jump, so everything derived from it —
|
|
@@ -342,9 +410,10 @@ export class Viewer {
|
|
|
342
410
|
if (this.position === null) {
|
|
343
411
|
return null;
|
|
344
412
|
}
|
|
345
|
-
const rate = this.playing ? 1 : 0;
|
|
413
|
+
const rate = this.playing === true ? 1 : 0;
|
|
346
414
|
const elapsedSec = Math.max(0, (now - this.position.at) / 1000);
|
|
347
|
-
|
|
415
|
+
const held = Number.isFinite(this.bufferedSeconds) ? Math.max(0, this.bufferedSeconds) : 0;
|
|
416
|
+
return this.position.seconds + rate * Math.min(elapsedSec, held);
|
|
348
417
|
}
|
|
349
418
|
}
|
|
350
419
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A viewer's statement about itself, taken in.
|
|
3
|
+
*
|
|
4
|
+
* The VIEWER layer's own entry point, and the whole of what happens when a page
|
|
5
|
+
* says where it is: the statement reaches the person it is about, and readings
|
|
6
|
+
* that have gone stale stop deciding for the people still here.
|
|
7
|
+
*
|
|
8
|
+
* It lived in the session manager, which is a description of an OUTPUT, and the
|
|
9
|
+
* two questions were tangled there: whether this is a measurement of a LINK,
|
|
10
|
+
* and whether this is a statement by a VIEWER. Required to be both, a page that
|
|
11
|
+
* had measured nothing could say nothing — see `Viewer.report`.
|
|
12
|
+
*
|
|
13
|
+
* Given the session and the registry as plain arguments, so nothing here knows
|
|
14
|
+
* how a session is found, what a variant is, or that there is an encoder.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { viewersOf } from "./Viewer.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Take one report.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} params
|
|
23
|
+
* @param {{ of: (session: object, consumerId: string) => import("./Viewer.js").Viewer }} params.viewers
|
|
24
|
+
* @param {object} params.session - The output this viewer has on screen.
|
|
25
|
+
* @param {string} params.consumerId - Who is reporting; empty when the page
|
|
26
|
+
* does not name itself, and then every such page shares one viewer, which is
|
|
27
|
+
* exactly what one unnamed browser is.
|
|
28
|
+
* @param {object} params.report - What they said about themselves.
|
|
29
|
+
* @param {number} params.now
|
|
30
|
+
* @param {number} params.linkFreshMs - How long a link reading describes the
|
|
31
|
+
* link it was measured on.
|
|
32
|
+
* @returns {true}
|
|
33
|
+
*/
|
|
34
|
+
export function takeViewerReport({ viewers, session, consumerId, report, now, linkFreshMs }) {
|
|
35
|
+
viewers.of(session, consumerId).report(report, now);
|
|
36
|
+
forgetStaleLinkReadings({ session, now, linkFreshMs });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drop link readings older than the time they describe.
|
|
42
|
+
*
|
|
43
|
+
* ONLY THE READING EXPIRES. Whether the person is still watching is a different
|
|
44
|
+
* question with its own answer — their connection — and a viewer who has simply
|
|
45
|
+
* stopped reporting is not thereby gone. Answering both from this one place is
|
|
46
|
+
* what stopped a soundtrack's encoder on 2026-09-05, seconds after it started
|
|
47
|
+
* and before it had made the `init.mp4` the picture could not be played
|
|
48
|
+
* without.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} params
|
|
51
|
+
* @param {object} params.session
|
|
52
|
+
* @param {number} params.now
|
|
53
|
+
* @param {number} params.linkFreshMs
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
export function forgetStaleLinkReadings({ session, now, linkFreshMs }) {
|
|
57
|
+
for (const viewer of viewersOf(session).values()) {
|
|
58
|
+
const reading = viewer.netReport;
|
|
59
|
+
if (reading !== null && now - reading.at > linkFreshMs) {
|
|
60
|
+
viewer.netReport = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The route that hands a browser bytes to time its link with.
|
|
3
|
+
*
|
|
4
|
+
* Only this end's half. How big an ask should be is derived on the page, from
|
|
5
|
+
* what its last ask measured, and is checked where that function lives — a copy
|
|
6
|
+
* of it here would be two owners of one rule, which is the fault this
|
|
7
|
+
* repository keeps recording.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
|
|
13
|
+
import { handleApiLinkProbeGet, MAX_LINK_PROBE_BYTES } from "../routes/api/link-probe/get.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A reply that records what it was given.
|
|
17
|
+
*
|
|
18
|
+
* @returns {{ code: (n: number) => object, header: () => object, send: (b: unknown) => object, status: number | null, body: unknown }}
|
|
19
|
+
*/
|
|
20
|
+
function fakeReply() {
|
|
21
|
+
const reply = {
|
|
22
|
+
status: /** @type {number | null} */ (null),
|
|
23
|
+
body: /** @type {unknown} */ (null),
|
|
24
|
+
code(n) {
|
|
25
|
+
reply.status = n;
|
|
26
|
+
return reply;
|
|
27
|
+
},
|
|
28
|
+
header() {
|
|
29
|
+
return reply;
|
|
30
|
+
},
|
|
31
|
+
send(body) {
|
|
32
|
+
reply.body = body;
|
|
33
|
+
return reply;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return reply;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
test("the route answers exactly what was asked for, up to its own ceiling", async () => {
|
|
42
|
+
const small = fakeReply();
|
|
43
|
+
await handleApiLinkProbeGet({ query: { bytes: "65536" } }, small);
|
|
44
|
+
assert.equal(small.status, 200);
|
|
45
|
+
assert.equal(/** @type {Buffer} */ (small.body).length, 65536);
|
|
46
|
+
|
|
47
|
+
const huge = fakeReply();
|
|
48
|
+
await handleApiLinkProbeGet({ query: { bytes: "999999999" } }, huge);
|
|
49
|
+
assert.equal(/** @type {Buffer} */ (huge.body).length, MAX_LINK_PROBE_BYTES);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the route refuses an ask that is not a size", async () => {
|
|
53
|
+
const reply = fakeReply();
|
|
54
|
+
await handleApiLinkProbeGet({ query: { bytes: "none" } }, reply);
|
|
55
|
+
assert.equal(reply.status, 400);
|
|
56
|
+
});
|