@torrent-tv/proxy 2.70.0 → 2.71.1

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.
@@ -21,6 +21,8 @@
21
21
  */
22
22
 
23
23
  import { findSharedStore } from "../piece-store/shared-piece-store.js";
24
+ import { bytesOf, Urgency, urgencyName } from "../demand/index.js";
25
+ import { demandFor } from "../download/registry.js";
24
26
  import { logger } from "../../utils/logger.js";
25
27
  import {
26
28
  askFastestWiresFor,
@@ -75,9 +77,17 @@ const READ_WINDOW_BYTES = 32 * 1024 * 1024;
75
77
  * described above. Anything else — the default — alternates per read, so the two
76
78
  * accumulate side by side from real viewing and the log can compare them.
77
79
  */
78
- const READ_MODE_SETTING = process.env.TORRENT_TV_READ_MODE ?? "";
79
80
 
80
- const BAND_PRIORITIES = [4, 3, 2, 1];
81
+ /**
82
+ * Which level of urgency each band states.
83
+ *
84
+ * Not numbers handed to WebTorrent: measured against the vendored 2.8.5, the
85
+ * library keeps distinct non-zero priorities only until the first wire is
86
+ * served and round-robins them afterwards. The ordering is kept by WHAT is
87
+ * stated, in `services/demand/`, and these say which level each band belongs
88
+ * to (roadmap item 5).
89
+ */
90
+ const BAND_URGENCY = [Urgency.NEAR, Urgency.AHEAD, Urgency.AHEAD, Urgency.BEHIND];
81
91
 
82
92
  /**
83
93
  * Where each band sits, given the urgent one and how far the trailing bands have
@@ -91,10 +101,10 @@ const BAND_PRIORITIES = [4, 3, 2, 1];
91
101
  * picture they are watching.
92
102
  *
93
103
  * @param {{ urgent: { from: number, to: number }, pieceIndex: number, firstPiece: number, lastPiece: number, widths: { near: number, far: number } }} params
94
- * @returns {Array<{ from: number, to: number, priority: number }>}
104
+ * @returns {Array<{ from: number, to: number, urgency: number }>}
95
105
  */
96
106
  export function bandsFrom({ urgent, pieceIndex, firstPiece, lastPiece, widths }) {
97
- const bands = [{ from: urgent.from, to: urgent.to, priority: BAND_PRIORITIES[0] }];
107
+ const bands = [{ from: urgent.from, to: urgent.to, urgency: BAND_URGENCY[0] }];
98
108
  let edge = urgent.to;
99
109
  for (let index = 0; index < 2; index += 1) {
100
110
  if (edge >= lastPiece) {
@@ -106,13 +116,13 @@ export function bandsFrom({ urgent, pieceIndex, firstPiece, lastPiece, widths })
106
116
  }
107
117
  const from = edge + 1;
108
118
  const to = Math.min(lastPiece, edge + width);
109
- bands.push({ from, to, priority: BAND_PRIORITIES[index + 1] });
119
+ bands.push({ from, to, urgency: BAND_URGENCY[index + 1] });
110
120
  edge = to;
111
121
  }
112
122
  // Only once the lead has nothing left to cover: asking for the past while the
113
123
  // future is still missing would take capacity from the picture being watched.
114
124
  if (edge >= lastPiece && pieceIndex > firstPiece) {
115
- bands.push({ from: firstPiece, to: pieceIndex - 1, priority: BAND_PRIORITIES[3] });
125
+ bands.push({ from: firstPiece, to: pieceIndex - 1, urgency: BAND_URGENCY[3] });
116
126
  }
117
127
  return bands;
118
128
  }
@@ -172,7 +182,7 @@ export function sameBands(left, right) {
172
182
  return left.every((band, index) =>
173
183
  band.from === right[index].from &&
174
184
  band.to === right[index].to &&
175
- band.priority === right[index].priority);
185
+ band.urgency === right[index].urgency);
176
186
  }
177
187
 
178
188
  /**
@@ -217,100 +227,6 @@ export function nextWindowPieces({ current, base, ceiling, waitedMs, waitThresho
217
227
  return Math.max(floor, now - 1);
218
228
  }
219
229
 
220
- /**
221
- * Add this reader's window to the download set as a stream selection.
222
- *
223
- * `_select`/`_deselect` with the stream flag are what WebTorrent's own
224
- * `FileIterator` uses; there is no public call for it, because the public
225
- * `select` produces the merging, interval-subtracted kind whose bookkeeping
226
- * cannot express "one of several readers wants this". Falls back to the public
227
- * call if a future version drops the private one.
228
- *
229
- * @param {import("webtorrent").Torrent} torrent
230
- * @param {{ from: number, to: number }} window
231
- * @param {number} [priority] - 1 for what a reader needs next, 0 for the
232
- * background fill of the rest of the file.
233
- * @returns {void}
234
- */
235
- function claimWindow(torrent, { from, to }, priority = 1) {
236
- try {
237
- if (typeof torrent._select === "function") {
238
- torrent._select(from, to, priority, null, true);
239
- } else if (typeof torrent.select === "function") {
240
- torrent.select(from, to, priority);
241
- }
242
- } catch {
243
- // Best effort — never fail a read because selection bookkeeping refused.
244
- }
245
- }
246
-
247
- /**
248
- * Take this reader's window back out of the download set.
249
- *
250
- * The bounds must match the ones given to {@link claimWindow} exactly: a stream
251
- * selection is removed by equality, not by overlap.
252
- *
253
- * @param {import("webtorrent").Torrent} torrent
254
- * @param {{ from: number, to: number }} window
255
- * @returns {void}
256
- */
257
- function releaseWindow(torrent, { from, to }) {
258
- try {
259
- if (typeof torrent._deselect === "function") {
260
- torrent._deselect(from, to, true);
261
- } else if (typeof torrent.deselect === "function") {
262
- torrent.deselect(from, to);
263
- }
264
- } catch {
265
- // Best effort.
266
- }
267
- }
268
-
269
- /**
270
- * Mark the piece a reader is blocked on, clearing the mark it set before.
271
- *
272
- * Criticality is never cleared by WebTorrent itself, so a reader that walked a
273
- * film would leave every piece of it marked. Only the indices this reader set
274
- * are cleared, so a second reader's mark on the same piece is not stolen — and
275
- * the flag is advisory anyway.
276
- *
277
- * @param {import("webtorrent").Torrent} torrent
278
- * @param {number} from
279
- * @param {number} to
280
- * @param {{ from: number, to: number } | null} previous
281
- * @returns {{ from: number, to: number } | null}
282
- */
283
- function markCritical(torrent, from, to, previous) {
284
- if (previous && previous.from === from && previous.to === to) {
285
- return previous;
286
- }
287
- if (previous) {
288
- clearCritical(torrent, previous);
289
- }
290
- try {
291
- torrent.critical?.(from, to);
292
- } catch {
293
- return null;
294
- }
295
- return { from, to };
296
- }
297
-
298
- /**
299
- * Drop critical marks this reader set.
300
- *
301
- * @param {import("webtorrent").Torrent} torrent
302
- * @param {{ from: number, to: number }} mark
303
- * @returns {void}
304
- */
305
- function clearCritical(torrent, { from, to }) {
306
- if (!Array.isArray(torrent._critical)) {
307
- return;
308
- }
309
- for (let index = from; index <= to; index += 1) {
310
- torrent._critical[index] = false;
311
- }
312
- }
313
-
314
230
  /**
315
231
  * Who is working on the piece a reader is blocked on, right now.
316
232
  *
@@ -512,58 +428,69 @@ export function supplyFiguresFor(infoHash, fileName, segmentSeconds) {
512
428
  const waitsBySteering = new Map();
513
429
 
514
430
  /**
515
- * Record one wait against whether anything was steered during it.
431
+ * Record one wait against the band the reader was stopped in.
432
+ *
433
+ * What this replaces: until 2026-09-02 each read was assigned at random to one
434
+ * of two ways of claiming — one band or four — and the waits were sorted by
435
+ * which. The comparison never decided anything, and could not: the split halved
436
+ * the sample for each arm, so on 2026-08-28 there were nine reads in one arm and
437
+ * three in the other and the ten-wait threshold was never reached in either; on
438
+ * 2026-08-29 the two arms printed together for the first and only time, as forty
439
+ * waits against one.
440
+ *
441
+ * This is the more useful question anyway. A wait belongs to a LEVEL, and the
442
+ * level says whether a width is wrong rather than whether the whole scheme is:
443
+ * long waits in the band being watched mean the urgent window is too narrow,
444
+ * long waits further out mean the lead is.
516
445
  *
517
446
  * @param {string} key
518
447
  * @param {number} waitedMs
519
- * @param {boolean} steered
448
+ * @param {number} urgency
520
449
  * @returns {void}
521
450
  */
522
- function noteReadMode(key, waitedMs, mode) {
523
- let split = waitsByMode.get(key);
524
- if (!split) {
525
- split = { flat: [], bands: [] };
526
- waitsByMode.set(key, split);
451
+ function noteWaitLevel(key, waitedMs, urgency) {
452
+ let byLevel = waitsByLevel.get(key);
453
+ if (!byLevel) {
454
+ byLevel = new Map();
455
+ waitsByLevel.set(key, byLevel);
527
456
  }
528
- const into = mode === "bands" ? split.bands : split.flat;
529
- into.push(waitedMs);
530
- while (into.length > SUPPLY_WAIT_HISTORY) {
531
- into.shift();
457
+ const waits = byLevel.get(urgency) ?? [];
458
+ waits.push(waitedMs);
459
+ while (waits.length > SUPPLY_WAIT_HISTORY) {
460
+ waits.shift();
532
461
  }
462
+ byLevel.set(urgency, waits);
533
463
  }
534
464
 
535
465
  /**
536
- * What the two ways of claiming amount to, or null while either has no sample.
537
- *
538
- * This is the whole of the comparison: the same file, the same swarm, the same
539
- * viewer, waits sorted by which arm was in force. It appears in the periodic
540
- * summary so a session can be read without collecting lines by hand.
466
+ * Where the waits fell, by level, or null while nothing has waited.
541
467
  *
542
468
  * @param {string} key
543
469
  * @returns {string | null}
544
470
  */
545
- function describeReadModes(key) {
546
- const split = waitsByMode.get(key);
547
- if (!split || split.flat.length === 0 || split.bands.length === 0) {
471
+ function describeWaitLevels(key) {
472
+ const byLevel = waitsByLevel.get(key);
473
+ if (!byLevel || byLevel.size === 0) {
548
474
  return null;
549
475
  }
550
476
  const middle = (values) => {
551
477
  const sorted = [...values].sort((left, right) => left - right);
552
478
  return sorted[Math.floor(sorted.length / 2)];
553
479
  };
554
- const worst = (values) => Math.max(...values);
555
- return (
556
- `flat ${split.flat.length} waits median ${middle(split.flat)}ms worst ${worst(split.flat)}ms, ` +
557
- `bands ${split.bands.length} waits median ${middle(split.bands)}ms worst ${worst(split.bands)}ms`
558
- );
480
+ return [...byLevel.entries()]
481
+ .sort(([left], [right]) => left - right)
482
+ .map(([urgency, waits]) =>
483
+ `${urgencyName(urgency)} ${waits.length} waits median ${middle(waits)}ms ` +
484
+ `worst ${Math.max(...waits)}ms`)
485
+ .join(", ");
559
486
  }
560
487
 
561
488
  /**
562
- * Waits split by which way the reader was claiming.
489
+ * Waits split by the level the reader was stopped in.
563
490
  *
564
- * @type {Map<string, { flat: number[], bands: number[] }>}
491
+ * @type {Map<string, Map<number, number[]>>}
565
492
  */
566
- const waitsByMode = new Map();
493
+ const waitsByLevel = new Map();
567
494
 
568
495
  function noteSteeringOutcome(key, waitedMs, steered) {
569
496
  let split = waitsBySteering.get(key);
@@ -712,7 +639,7 @@ function noteSupplyWait(key, label, waitedMs) {
712
639
  // comparison with one side empty is not a comparison.
713
640
  (describeSteering(key) ? ` — ${describeSteering(key)}` : "") +
714
641
  // The comparison this release exists to make. Read it first.
715
- (describeReadModes(key) ? ` — ${describeReadModes(key)}` : "")
642
+ (describeWaitLevels(key) ? ` — ${describeWaitLevels(key)}` : "")
716
643
  );
717
644
  }
718
645
 
@@ -735,6 +662,10 @@ export async function* readFragments({
735
662
  if (!store) {
736
663
  throw new Error("This torrent is not backed by a shared piece store.");
737
664
  }
665
+ // What this reader wants goes here and nowhere else. `SwarmSelection` is the
666
+ // only thing that turns any of it into a request to the swarm, so a reader
667
+ // can no longer contradict the background fill or the pool.
668
+ const { register, selection } = demandFor(torrent);
738
669
 
739
670
  const file = torrent.files?.[fileIndex];
740
671
  if (!file) {
@@ -762,22 +693,6 @@ export async function* readFragments({
762
693
  // `bytes 0-<EOF>` left a permanent selection over the entire file, and no
763
694
  // later prioritisation could outrank it.
764
695
  const basePieces = Math.max(1, Math.ceil(Math.max(1, windowBytes) / pieceLength));
765
- /**
766
- * How this reader claims what it wants: `flat` is one band of equal urgency,
767
- * which is what every release before this did; `bands` puts the pieces the
768
- * viewer is about to reach above the fill behind them, anchored at the first
769
- * piece not already held.
770
- *
771
- * Alternated per read unless the deployment names one, so the comparison
772
- * accumulates from real viewing instead of from a synthetic swarm — three
773
- * such experiments in one day measured regimes the levers were not for and
774
- * cost more than they settled.
775
- *
776
- * @type {"flat" | "bands"}
777
- */
778
- const readMode = READ_MODE_SETTING === "flat" || READ_MODE_SETTING === "bands"
779
- ? READ_MODE_SETTING
780
- : (Math.random() < 0.5 ? "flat" : "bands");
781
696
  // What the window is RIGHT NOW. It starts at what the caller sized in seconds
782
697
  // of playback and grows while the reader keeps being made to wait — see
783
698
  // `nextWindowPieces`.
@@ -800,7 +715,7 @@ export async function* readFragments({
800
715
  /** @type {{ from: number, to: number } | null} */
801
716
  let window = null;
802
717
  /** @type {{ from: number, to: number } | null} */
803
- let criticalMark = null;
718
+ let blockedStated = false;
804
719
  /**
805
720
  * Drops the pin of the fragment currently in the consumer's hands, if it
806
721
  * still holds one. See where it is assigned.
@@ -889,42 +804,70 @@ export async function* readFragments({
889
804
  return lastWidths;
890
805
  };
891
806
 
807
+ /**
808
+ * State one band as a need, in bytes.
809
+ *
810
+ * The claimant carries the level, so the four bands of one reader are four
811
+ * claimants and each is replaced on its own — restating the near band does
812
+ * not disturb what was said about the tail.
813
+ *
814
+ * @param {{ from: number, to: number, urgency: number }} band
815
+ * @returns {void}
816
+ */
817
+ const stateBand = (band) => {
818
+ const range = bytesOf({
819
+ fileOffset: Number(file.offset),
820
+ fileLength: Number(file.length),
821
+ from: band.from,
822
+ to: band.to,
823
+ pieceLength
824
+ });
825
+ if (!range) {
826
+ return;
827
+ }
828
+ register.state({
829
+ claimant: `${readerId}:${urgencyName(band.urgency)}`,
830
+ fileIndex,
831
+ byteStart: range.byteStart,
832
+ byteEnd: range.byteEnd,
833
+ urgency: band.urgency
834
+ });
835
+ };
836
+
892
837
  const moveWindowTo = (pieceIndex) => {
893
- const anchor = readMode === "bands" ? firstMissingFrom(pieceIndex) : pieceIndex;
838
+ const anchor = firstMissingFrom(pieceIndex);
894
839
  const next = readWindowFor({ pieceIndex: anchor, lastPiece, windowPieces });
895
840
  const sameUrgent = window && window.from === next.from && window.to === next.to;
896
- if (sameUrgent && readMode === "flat") {
897
- return;
898
- }
899
841
  const isJump = !window || next.from > window.to || next.from < window.from;
900
842
  // A seek makes every width behind the urgent band meaningless: they were
901
843
  // grown against a position the viewer has left, and what lies beyond the
902
844
  // new one has to be earned from a standing start.
903
- const wanted = readMode === "bands"
904
- ? bandsFrom({
905
- urgent: next,
906
- pieceIndex,
907
- firstPiece,
908
- lastPiece,
909
- widths: bandWidths()
910
- })
911
- : [{ ...next, priority: BAND_PRIORITIES[0] }];
845
+ const wanted = bandsFrom({
846
+ urgent: next,
847
+ pieceIndex,
848
+ firstPiece,
849
+ lastPiece,
850
+ widths: bandWidths()
851
+ });
912
852
  if (sameUrgent && sameBands(claimed, wanted)) {
913
853
  return;
914
854
  }
855
+ // Withdrawn by name, so a band that is no longer wanted goes and the rest
856
+ // stay. The swarm is told nothing here — `reconcile` is the only thing that
857
+ // speaks to the library, and it reads what is stated.
915
858
  for (const band of claimed) {
916
- releaseWindow(torrent, band);
859
+ register.withdraw(`${readerId}:${urgencyName(band.urgency)}`);
917
860
  }
918
861
  for (const band of wanted) {
919
- claimWindow(torrent, band, band.priority);
862
+ stateBand(band);
920
863
  }
864
+ // One call, and it does both: the swarm is told what to fetch and the store
865
+ // is told what will be read soon, from the same stated needs. The store used
866
+ // to be told separately here, which made the same intent two lists that
867
+ // could drift.
868
+ selection.reconcile();
921
869
  claimed = wanted;
922
870
  window = next;
923
- // Tell the store these pieces are wanted, so it evicts something else.
924
- // Without it the piece the decoder reads next looks exactly as stale as one
925
- // the encoder fetched forty minutes ahead, and the second kind is what
926
- // fills the store while the encoder runs ahead of the viewer.
927
- store.protectRange?.(readerId, next.from, next.to);
928
871
  if (isJump) {
929
872
  waitBelongsToJump = true;
930
873
  // A jump — a seek, not the window sliding along — can land on pieces that
@@ -966,7 +909,12 @@ export async function* readFragments({
966
909
  // REQUESTED RANGE, which for ffmpeg's input means every piece to the
967
910
  // end of the file — hundreds of them, at which point the flag says
968
911
  // nothing. A window is what a reader genuinely needs next.
969
- criticalMark = markCritical(torrent, pieceIndex, window.to, criticalMark);
912
+ // Stated as its own need at the level that is being waited on, which
913
+ // is what carries the permission to take a block from a slow peer.
914
+ // Nothing here calls the library: `reconcile` reads what is stated.
915
+ stateBand({ from: pieceIndex, to: window.to, urgency: Urgency.BLOCKED });
916
+ blockedStated = true;
917
+ selection.reconcile();
970
918
  }
971
919
 
972
920
  const waitStartedAt = Date.now();
@@ -1009,6 +957,8 @@ export async function* readFragments({
1009
957
  }
1010
958
  pushed = {
1011
959
  asked: pushed.asked + result.asked,
960
+ refusedWhileReserved:
961
+ (pushed.refusedWhileReserved ?? 0) + (result.refusedWhileReserved ?? 0),
1012
962
  // Summed like the successes, so the line compares two totals over
1013
963
  // the same attempts instead of a total against a snapshot.
1014
964
  attempted: (pushed.attempted ?? 0) + result.attempted,
@@ -1078,7 +1028,15 @@ export async function* readFragments({
1078
1028
  } else {
1079
1029
  const supplyKey = `${torrent?.infoHash ?? "?"}/${file?.name ?? "?"}`;
1080
1030
  noteSteeringOutcome(supplyKey, waitedMs, pushed.asked > 0 || duplicated > 0);
1081
- noteReadMode(supplyKey, waitedMs, readMode);
1031
+ // Which band the reader was stopped in. A wait belongs to a level, and
1032
+ // the level says whether that band is too narrow rather than whether
1033
+ // banding is the wrong idea.
1034
+ noteWaitLevel(
1035
+ supplyKey,
1036
+ waitedMs,
1037
+ claimed.find((band) => pieceIndex >= band.from && pieceIndex <= band.to)?.urgency
1038
+ ?? Urgency.BLOCKED
1039
+ );
1082
1040
  waitCount += 1;
1083
1041
  waitedTotalMs += waitedMs;
1084
1042
  noteSupplyWait(supplyKey, file?.name ?? "", waitedMs);
@@ -1118,6 +1076,15 @@ export async function* readFragments({
1118
1076
  // the piece onto faster holders shortens the tail — by number
1119
1077
  // rather than by impression.
1120
1078
  `; steered onto ${pushed.asked} of ${pushed.attempted} asks (${pushed.considered} peers held it)` +
1079
+ // How often a fast peer we picked was refused because every block
1080
+ // was already spoken for and the library declined to take one from
1081
+ // a slow holder. Its thresholds are constants, not settings: the
1082
+ // asker must be above 16 KB/s, the holder below 48 KB/s and twice
1083
+ // as slow. A number here is what would justify replacing that rule;
1084
+ // a zero says the thresholds are not what we are short of.
1085
+ ((pushed.refusedWhileReserved ?? 0) > 0
1086
+ ? `; ${pushed.refusedWhileReserved} refused with every block reserved`
1087
+ : "") +
1121
1088
  (pushed.fastestBytesPerSecond > 0
1122
1089
  ? `, fastest ${Math.round(pushed.fastestBytesPerSecond / 1024)}KB/s`
1123
1090
  : "") +
@@ -1137,16 +1104,13 @@ export async function* readFragments({
1137
1104
  // What we did about the tail, so the next session says by number
1138
1105
  // whether a second copy of those blocks shortens the wait.
1139
1106
  (duplicated > 0 ? `; duplicated ${duplicated} blocks` : "") +
1140
- // Which way the reader was claiming while this wait happened, and
1141
- // the bands themselves. Without it a number in the log belongs to
1142
- // neither arm and the comparison cannot be made afterwards.
1143
- `; mode=${readMode}` +
1144
- (readMode === "bands"
1145
- ? ` ${claimed.map((band) => `p${band.priority}:${band.from}-${band.to}`).join(" ")}` +
1146
- (lastWidths.measured
1147
- ? ` (near ${lastWidths.near} far ${lastWidths.far} pieces, from the measured wait and surplus)`
1148
- : " (widths not measured yet)")
1149
- : "")
1107
+ // The bands this reader was claiming while the wait happened, by
1108
+ // level. A wait belongs to a level, and that is what says whether a
1109
+ // width is wrong rather than whether the whole scheme is.
1110
+ ` ${claimed.map((band) => `${urgencyName(band.urgency)}:${band.from}-${band.to}`).join(" ")}` +
1111
+ (lastWidths.measured
1112
+ ? ` (near ${lastWidths.near} far ${lastWidths.far} pieces, from the measured wait and surplus)`
1113
+ : " (widths not measured yet)")
1150
1114
  );
1151
1115
  }
1152
1116
 
@@ -1221,7 +1185,7 @@ export async function* readFragments({
1221
1185
  const readSeconds = (Date.now() - readStartedAt) / 1000;
1222
1186
  if (deliveredBytes > 0) {
1223
1187
  logger.info(
1224
- `read "${String(file?.name ?? "?").slice(0, 40)}" mode=${readMode} ` +
1188
+ `read "${String(file?.name ?? "?").slice(0, 40)}" ` +
1225
1189
  `delivered=${(deliveredBytes / 1e6).toFixed(1)}MB in ${readSeconds.toFixed(1)}s ` +
1226
1190
  `waits=${waitCount} waited=${(waitedTotalMs / 1000).toFixed(1)}s`
1227
1191
  );
@@ -1230,11 +1194,13 @@ export async function* readFragments({
1230
1194
  // stops iterating — a band left behind would keep the swarm fetching for a
1231
1195
  // reader that no longer exists.
1232
1196
  for (const band of claimed) {
1233
- releaseWindow(torrent, band);
1197
+ register.withdraw(`${readerId}:${urgencyName(band.urgency)}`);
1234
1198
  }
1235
- store.releaseProtection?.(readerId);
1236
- if (criticalMark) {
1237
- clearCritical(torrent, criticalMark);
1199
+ if (blockedStated) {
1200
+ register.withdraw(`${readerId}:${urgencyName(Urgency.BLOCKED)}`);
1238
1201
  }
1202
+ // Once, after everything has been withdrawn — and it releases this reader's
1203
+ // hold on memory as well, because both views come from the same statement.
1204
+ selection.reconcile();
1239
1205
  }
1240
1206
  }
@@ -32,6 +32,26 @@ import { CONTAINER_HEAD_BYTES, containerTracksOf } from "./container-tracks.js";
32
32
  import { fillFileInBackground } from "./background-fill.js";
33
33
  import { Command, Event } from "./protocol.js";
34
34
  import { startMemoryReport, WORKER_MEMORY_SAMPLE_MS } from "../memory-report.js";
35
+ import { forwardLogsTo, logger } from "../../utils/logger.js";
36
+
37
+ /**
38
+ * Everything this thread logs goes to the main thread, which owns the file.
39
+ *
40
+ * Two threads cannot both write it — they would race on the rotation and could
41
+ * interleave mid-line — so there is one writer and this is how everyone else
42
+ * reaches it. Set before anything else runs, because a line written before this
43
+ * point reaches the console only, and the console is destroyed by every
44
+ * release.
45
+ *
46
+ * Until 2026-09-02 only the `log` function below took this route. Modules that
47
+ * called `logger.*` directly — the piece reader, the torrent pool, the
48
+ * background fill, the container track reader, the subtitle walk — wrote into a
49
+ * copy of the logger that had no file, and every one of their lines was lost:
50
+ * measured over a whole 49 938-line file, not one of them was in it.
51
+ */
52
+ forwardLogsTo((_level, message) => {
53
+ parentPort.postMessage({ type: Event.LOG, message });
54
+ });
35
55
 
36
56
  // Imported dynamically, and that is load-bearing: static imports are RESOLVED
37
57
  // during linking, before any module body runs, so a statically imported pool
@@ -71,14 +91,14 @@ const fileClaims = createFileClaims();
71
91
  const readsById = new Map();
72
92
 
73
93
  /**
74
- * Forward a log line to the main thread, so worker output is not lost or
75
- * interleaved separately from everything else.
94
+ * Shorthand for this file. The same path as `logger.info` anywhere else in the
95
+ * thread kept only because it reads better at the hundred call sites here.
76
96
  *
77
97
  * @param {string} message
78
98
  * @returns {void}
79
99
  */
80
100
  function log(message) {
81
- parentPort.postMessage({ type: Event.LOG, message });
101
+ logger.info(message);
82
102
  }
83
103
 
84
104
  /**