@torrent-tv/proxy 2.83.2 → 2.83.4
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 +24 -0
- package/package.json +1 -1
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -0
- package/research/priority-map-is-the-truth-2026-09-12.md +207 -0
- package/routes/api/sources/files/get.js +23 -9
- package/routes/api/sources/warm/post.js +26 -22
- package/routes/api/transcode-sessions/post.js +1 -49
- package/routes/stream/get.js +2 -36
- package/services/controllers/SubtitleController.js +0 -3
- package/services/hls-session-manager.js +3 -68
- package/services/playback-planner.js +12 -13
- package/services/priority/PriorityOrchestrator.js +40 -6
- package/services/torrent/Contents.js +324 -0
- package/services/torrent/files.js +8 -1
- package/services/torrent-pool.js +378 -187
- package/services/torrent-worker/client.js +0 -24
- package/services/torrent-worker/pool-adapter.js +3 -33
- package/services/torrent-worker/protocol.js +0 -4
- package/services/torrent-worker/worker.js +52 -59
- package/test/file-edges.test.js +191 -0
- package/test/priority-map-emptied.test.js +207 -0
- package/test/source-files-route.test.js +103 -0
- package/test/stream-route.test.js +4 -8
- package/test/swarm-follows-readers.test.js +107 -101
- package/test/torrent-contents.test.js +235 -0
- package/test/upload-hurry.test.js +66 -28
- package/services/torrent-worker/file-claims.js +0 -91
- package/test/file-claims.test.js +0 -64
package/services/torrent-pool.js
CHANGED
|
@@ -24,6 +24,9 @@ import { deriveSourceKey } from "./torrent-source-key.js";
|
|
|
24
24
|
/** How a window stated from the priority map names itself. */
|
|
25
25
|
const MAP_CLAIMANT = "priority-map";
|
|
26
26
|
|
|
27
|
+
/** How the two ends of a file name themselves. */
|
|
28
|
+
const EDGES_CLAIMANT = "file-edges";
|
|
29
|
+
|
|
27
30
|
// The DHT's entry points. Two of the three the library ships answer nothing —
|
|
28
31
|
// measured 2026-08-21 from the addon host: `router.bittorrent.com` and
|
|
29
32
|
// `router.utorrent.com` did not reply to a hand-written `ping` at all, while a
|
|
@@ -331,29 +334,25 @@ function describeSwarmDemand(torrent) {
|
|
|
331
334
|
/**
|
|
332
335
|
* The torrents the upload policy is allowed to see.
|
|
333
336
|
*
|
|
334
|
-
* A torrent
|
|
335
|
-
* HURRY qualifies even without
|
|
336
|
-
* first
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* measured 2026-08-04 — so the upload stayed at the near-silent idle floor
|
|
341
|
-
* during the exact seconds peers were deciding whether to serve us.
|
|
337
|
+
* A torrent something is wanted of qualifies for the obvious reason. A torrent
|
|
338
|
+
* in a HURRY qualifies even without that, and that case is the important one:
|
|
339
|
+
* the first seconds of a new torrent are when peers decide whether to serve us,
|
|
340
|
+
* and an upload held at the near-silent idle floor through them costs the whole
|
|
341
|
+
* ramp — 8.36 s of the 11.46 s before playback in the session measured
|
|
342
|
+
* 2026-08-04.
|
|
342
343
|
*
|
|
343
344
|
* @param {Iterable<{ hurryUntil?: number }>} torrents
|
|
344
|
-
* @param {Map<object, { size: number }>} usageByTorrent - fileIndex sets, keyed by torrent.
|
|
345
345
|
* @param {number} now
|
|
346
346
|
* @returns {object[]}
|
|
347
347
|
*/
|
|
348
|
-
export function torrentsForUploadPolicy(torrents,
|
|
348
|
+
export function torrentsForUploadPolicy(torrents, now) {
|
|
349
349
|
const chosen = [];
|
|
350
350
|
for (const torrent of torrents) {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
if (hasReader || (torrent?.hurryUntil ?? 0) > now) {
|
|
351
|
+
const wanted = isWanted(torrent);
|
|
352
|
+
if (wanted || (torrent?.hurryUntil ?? 0) > now) {
|
|
354
353
|
// Recorded so the policy can tell "nothing is arriving and somebody is
|
|
355
354
|
// waiting" from "nothing is arriving because nobody asked".
|
|
356
|
-
torrent.
|
|
355
|
+
torrent.isWanted = wanted;
|
|
357
356
|
// Whether anybody is still short of bytes of it. Upload is bought with
|
|
358
357
|
// reciprocity and reciprocity is only worth buying while something is
|
|
359
358
|
// missing; a torrent whose declared windows are all present wants nothing
|
|
@@ -398,13 +397,13 @@ export function decideUploadLimit(activeTorrents, opts = {}) {
|
|
|
398
397
|
// iterate the piece array and throw on webtorrent 3.x when a piece is null
|
|
399
398
|
// (deselected / mid-verify), which would crash this timer every cycle.
|
|
400
399
|
const notDone = torrent?.done !== true;
|
|
401
|
-
// A torrent
|
|
402
|
-
// looks. The encoder is held back once it is far enough ahead of the
|
|
400
|
+
// A torrent nothing is wanted of is not starving, however still its
|
|
401
|
+
// download looks. The encoder is held back once it is far enough ahead of the
|
|
403
402
|
// viewer, and while it is held nothing is requested — measured
|
|
404
403
|
// 2026-08-04: four cycles of 512 -> 50 KB/s in three minutes, each
|
|
405
404
|
// reported as `earn unchoke ... down=0KB/s`, all of them raising the
|
|
406
405
|
// upload at moments when no byte was wanted by anyone.
|
|
407
|
-
const starving = notDone && torrent?.
|
|
406
|
+
const starving = notDone && torrent?.isWanted !== false
|
|
408
407
|
&& torrent?.hasUnmetDemand !== false && downloadSpeed < starvingSpeed;
|
|
409
408
|
if (starving && chokedInterested >= chokedThreshold) {
|
|
410
409
|
const name = typeof torrent?.name === "string" ? torrent.name : "?";
|
|
@@ -428,6 +427,83 @@ export function decideUploadLimit(activeTorrents, opts = {}) {
|
|
|
428
427
|
return { bytesPerSec: floor, reason: "active readers, not choke-starved" };
|
|
429
428
|
}
|
|
430
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Let go of the swarm of a torrent whose last reader has just left.
|
|
432
|
+
*
|
|
433
|
+
* A PROXY THAT NEEDS NOTHING FROM A SWARM SHOULD NOT BE IN THAT SWARM. While a
|
|
434
|
+
* file is being read every connection is worth keeping — the one that has
|
|
435
|
+
* delivered nothing yet may deliver next — so this is not a limit on
|
|
436
|
+
* connections and never fires during playback.
|
|
437
|
+
*
|
|
438
|
+
* What that state cost until now: WebTorrent enforces `maxConns` only on peers
|
|
439
|
+
* it dials (`_drain`), while `_addIncomingPeer` checks that the torrent is
|
|
440
|
+
* neither destroyed nor paused and registers the peer. A proxy with its port
|
|
441
|
+
* mapped is reachable, so connections arrive and are never turned away — field
|
|
442
|
+
* 2026-09-11, 249 connected at the start of one viewing and 596 at the end,
|
|
443
|
+
* 15 862 more queued, on a file complete for three quarters of an hour.
|
|
444
|
+
*
|
|
445
|
+
* CALLED FROM THE DEPARTURE ITSELF, not from a pass over every torrent. The
|
|
446
|
+
* first version asked every five seconds whether anybody was reading, and a
|
|
447
|
+
* torrent added three seconds earlier — its edges still being read, its
|
|
448
|
+
* playback plan still being built — answered no. It was taken out of its swarm
|
|
449
|
+
* with 741 connections let go, and nothing rejoined it: rejoining waits for a
|
|
450
|
+
* reader, and the reader was waiting for the header the swarm had been
|
|
451
|
+
* fetching. Playback did not start at all (2.83.1).
|
|
452
|
+
*
|
|
453
|
+
* Asked at the moment the last claim is released, leaving cannot happen
|
|
454
|
+
* spontaneously: it is a consequence of a departure, and a torrent nobody has
|
|
455
|
+
* read yet has nothing to depart from.
|
|
456
|
+
*
|
|
457
|
+
* `paused` is the library's own word for this and the path it already checks,
|
|
458
|
+
* so nothing here fights it. The pause alone does not close what is already
|
|
459
|
+
* open, so the peers are let go by hand; the data stays exactly where it is.
|
|
460
|
+
*
|
|
461
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
462
|
+
* @returns {number} Connections let go.
|
|
463
|
+
*/
|
|
464
|
+
export function leaveSwarm(torrent) {
|
|
465
|
+
if (!torrent || torrent.destroyed || torrent.paused === true) {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
torrent.pause();
|
|
469
|
+
const open = Array.isArray(torrent.wires) ? torrent.wires.length : 0;
|
|
470
|
+
let closed = 0;
|
|
471
|
+
for (const peer of [...(torrent._peers?.values?.() ?? [])]) {
|
|
472
|
+
try {
|
|
473
|
+
peer.destroy();
|
|
474
|
+
closed += 1;
|
|
475
|
+
} catch {
|
|
476
|
+
// A peer already going: nothing to do, and nothing worth failing for.
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
logger.info(
|
|
480
|
+
`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] left the swarm — its last reader went, ` +
|
|
481
|
+
`${open} connection(s) open, ${closed} let go; the data stays and the next reader rejoins`
|
|
482
|
+
);
|
|
483
|
+
return closed;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Take the swarm back for a torrent a reader has just arrived at.
|
|
488
|
+
*
|
|
489
|
+
* The other half of the pair: a departure lets a swarm go, an arrival takes it
|
|
490
|
+
* back. Both are acted on at the moment they happen, and neither is discovered
|
|
491
|
+
* by asking.
|
|
492
|
+
*
|
|
493
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
494
|
+
* @returns {boolean} Whether it had to be taken back.
|
|
495
|
+
*/
|
|
496
|
+
export function rejoinSwarm(torrent) {
|
|
497
|
+
if (!torrent || torrent.destroyed || torrent.paused !== true) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
torrent.resume();
|
|
501
|
+
logger.info(
|
|
502
|
+
`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] rejoined the swarm — a reader arrived`
|
|
503
|
+
);
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
431
507
|
/**
|
|
432
508
|
* Compute the default disk cap: the smaller of a fixed 10 GB and half of the
|
|
433
509
|
* currently free space on the store's filesystem (so a tiny host is never
|
|
@@ -712,6 +788,176 @@ export function dhtNodeCount(client) {
|
|
|
712
788
|
// without building a pool, a torrent client or a thread.
|
|
713
789
|
const lastMapSaid = new WeakMap();
|
|
714
790
|
|
|
791
|
+
/**
|
|
792
|
+
* Take back what the priority map had stated for one file, from a band onwards.
|
|
793
|
+
*
|
|
794
|
+
* Every band of a map is a claimant of its own, numbered, so a map that has
|
|
795
|
+
* shrunk — or emptied — is applied by withdrawing the numbers it no longer
|
|
796
|
+
* reaches. Written once because both callers need exactly it: the ordinary
|
|
797
|
+
* application, where the count is how many bands there now are, and a file
|
|
798
|
+
* nobody wants any more, where it is none.
|
|
799
|
+
*
|
|
800
|
+
* A function of a torrent rather than of the pool, like `leaveSwarm` beside it:
|
|
801
|
+
* it needs nothing the pool holds, and it can be exercised without building one.
|
|
802
|
+
*
|
|
803
|
+
* @param {object} torrent
|
|
804
|
+
* @param {number} fileIndex
|
|
805
|
+
* @param {number} [keepBelow] - Bands numbered below this are left alone.
|
|
806
|
+
* @returns {number} How many were withdrawn.
|
|
807
|
+
*/
|
|
808
|
+
function withdrawPriorityMap(torrent, fileIndex, keepBelow = 0) {
|
|
809
|
+
const { register } = demandFor(torrent);
|
|
810
|
+
const prefix = `${MAP_CLAIMANT}:${fileIndex}:`;
|
|
811
|
+
let withdrawn = 0;
|
|
812
|
+
for (const window of register.windows()) {
|
|
813
|
+
const claimant = String(window.claimant);
|
|
814
|
+
if (!claimant.startsWith(prefix)) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const index = Number(claimant.split(":").pop());
|
|
818
|
+
if (!(index < keepBelow)) {
|
|
819
|
+
register.withdraw(claimant);
|
|
820
|
+
withdrawn += 1;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (withdrawn > 0 && keepBelow === 0) {
|
|
824
|
+
// Said, because it is the one statement that cannot be read from what
|
|
825
|
+
// follows it: nothing further is stated about this file, so from here on
|
|
826
|
+
// silence is exactly what it looks like.
|
|
827
|
+
logger.info(
|
|
828
|
+
`torrent-pool: [${String(torrent?.infoHash ?? "?").slice(0, 8)}] nobody wants file ${fileIndex} ` +
|
|
829
|
+
`any more — ${withdrawn} band(s) of the map withdrawn`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
return withdrawn;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* WHERE A FILE SAYS WHAT IT IS: the piece at each of its ends.
|
|
837
|
+
*
|
|
838
|
+
* A container keeps its directory at one end or the other — `ftyp` and an EBML
|
|
839
|
+
* header at the front, and for an MP4 that was not written for streaming the
|
|
840
|
+
* `moov` at the very back. Nothing can be read of such a file until those have
|
|
841
|
+
* arrived, and they stay wanted for as long as it is open: a player asks for
|
|
842
|
+
* the file's shape again at every seek.
|
|
843
|
+
*
|
|
844
|
+
* **The size is not chosen, and it cannot be.** One byte is claimed at each
|
|
845
|
+
* end, and the piece is what the swarm delivers — so one byte at each end IS
|
|
846
|
+
* one piece at each end, with nothing rounded up by a number anybody picked.
|
|
847
|
+
* The 256 KB and 2 MB the prefetch reads are its own affair; where a directory
|
|
848
|
+
* is bigger than the piece it starts in, the read that needs it says so itself,
|
|
849
|
+
* at the level of a reader that is stopped.
|
|
850
|
+
*
|
|
851
|
+
* **Stated by the FILE, not by the read that happens to want it.** A read
|
|
852
|
+
* withdraws what it stated the moment it finishes, so until now the two ends of
|
|
853
|
+
* an open film were held by nothing at all once the codec probe was done — and
|
|
854
|
+
* they are exactly the pieces a seek needs and eviction is free to take.
|
|
855
|
+
*
|
|
856
|
+
* A function of a torrent rather than of the pool, like `leaveSwarm` beside it,
|
|
857
|
+
* so both can be exercised without building one.
|
|
858
|
+
*
|
|
859
|
+
* @param {object} torrent
|
|
860
|
+
* @param {number} fileIndex
|
|
861
|
+
* @param {number} urgency - NEAR while somebody is waiting for them, TAIL to
|
|
862
|
+
* keep them afterwards.
|
|
863
|
+
* @returns {boolean} Whether anything was stated.
|
|
864
|
+
*/
|
|
865
|
+
export function stateFileEdges(torrent, fileIndex, urgency, { lower = false } = {}) {
|
|
866
|
+
const file = Array.isArray(torrent?.files) ? torrent.files[fileIndex] : null;
|
|
867
|
+
const length = Number(file?.length);
|
|
868
|
+
if (!file || !(length > 0)) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
const { register } = demandFor(torrent);
|
|
872
|
+
const ends = [
|
|
873
|
+
{ claimant: `${EDGES_CLAIMANT}:${fileIndex}:head`, byteStart: 0, byteEnd: 0 },
|
|
874
|
+
{ claimant: `${EDGES_CLAIMANT}:${fileIndex}:tail`, byteStart: length - 1, byteEnd: length - 1 }
|
|
875
|
+
];
|
|
876
|
+
for (const end of ends) {
|
|
877
|
+
// RAISED, NEVER LOWERED, unless the caller is the one putting them back
|
|
878
|
+
// down. Two things ask for the same ends — a warm-up that nobody is waiting
|
|
879
|
+
// for and a playback plan that somebody is — and the warm-up's sidecars are
|
|
880
|
+
// fired off without being awaited, so it can arrive second. Taking the
|
|
881
|
+
// plan's urgency away there would leave a person watching a loading screen
|
|
882
|
+
// behind a film somebody else is watching.
|
|
883
|
+
const stated = register.windows().find((window) => String(window.claimant) === end.claimant);
|
|
884
|
+
const level = !lower && stated ? Math.min(stated.urgency, urgency) : urgency;
|
|
885
|
+
register.state({ ...end, fileIndex, urgency: level });
|
|
886
|
+
}
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Give up the ends of a file nobody has any use for.
|
|
892
|
+
*
|
|
893
|
+
* @param {object} torrent
|
|
894
|
+
* @param {number} fileIndex
|
|
895
|
+
* @returns {number} How many were withdrawn.
|
|
896
|
+
*/
|
|
897
|
+
export function withdrawFileEdges(torrent, fileIndex) {
|
|
898
|
+
const { register } = demandFor(torrent);
|
|
899
|
+
let withdrawn = 0;
|
|
900
|
+
for (const end of ["head", "tail"]) {
|
|
901
|
+
const claimant = `${EDGES_CLAIMANT}:${fileIndex}:${end}`;
|
|
902
|
+
if (register.windows().some((window) => String(window.claimant) === claimant)) {
|
|
903
|
+
register.withdraw(claimant);
|
|
904
|
+
withdrawn += 1;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return withdrawn;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* IS ANYTHING WANTED OF THIS TORRENT — the one question asked about a torrent's
|
|
912
|
+
* life, and it is answered by what has been stated, never by counting who is
|
|
913
|
+
* reading.
|
|
914
|
+
*
|
|
915
|
+
* Everything that wants bytes says so in the register: the priority map for
|
|
916
|
+
* every file somebody is watching, the ends of a file that is open, the
|
|
917
|
+
* background fill, and a read that is stopped on a piece. So "nothing is
|
|
918
|
+
* stated" is the whole of "nobody wants this", and it becomes true exactly when
|
|
919
|
+
* the last viewer leaves — because that is when the map is published with
|
|
920
|
+
* nothing in it.
|
|
921
|
+
*
|
|
922
|
+
* **A torrent that cannot yet be stated about is wanted.** Until its metadata
|
|
923
|
+
* arrives it has no files, so nothing can name a byte of it, and it is being
|
|
924
|
+
* fetched precisely because somebody asked for it. Judged by the register alone
|
|
925
|
+
* it would look abandoned three seconds after it was added, which is the
|
|
926
|
+
* failure of 2.83.1 exactly.
|
|
927
|
+
*
|
|
928
|
+
* @param {object} torrent
|
|
929
|
+
* @returns {boolean}
|
|
930
|
+
*/
|
|
931
|
+
export function isWanted(torrent) {
|
|
932
|
+
if (!torrent || torrent.destroyed) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
if (!Array.isArray(torrent.files) || torrent.files.length === 0) {
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
return demandFor(torrent).register.size > 0;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* WHAT TO DO ABOUT A TORRENT'S SWARM, from the two facts that decide it.
|
|
943
|
+
*
|
|
944
|
+
* Separated from the doing because this is the rule that has failed twice in
|
|
945
|
+
* the field, and a rule that can only be exercised by building a pool with a
|
|
946
|
+
* live WebTorrent client is a rule nothing checks. Both failures are cases of
|
|
947
|
+
* it: 2.83.1 let a swarm go for a torrent that had never been wanted, and the
|
|
948
|
+
* version before that never let one go at all.
|
|
949
|
+
*
|
|
950
|
+
* @param {{ wanted: boolean, everWanted: boolean }} facts
|
|
951
|
+
* @returns {{ swarm: "take" | "let go" | "leave alone", onTheClock: boolean }}
|
|
952
|
+
* What to do with the swarm, and whether the idle clock should be running.
|
|
953
|
+
*/
|
|
954
|
+
export function swarmDecisionFor({ wanted, everWanted }) {
|
|
955
|
+
if (wanted) {
|
|
956
|
+
return { swarm: "take", onTheClock: false };
|
|
957
|
+
}
|
|
958
|
+
return { swarm: everWanted ? "let go" : "leave alone", onTheClock: true };
|
|
959
|
+
}
|
|
960
|
+
|
|
715
961
|
export class TorrentPool {
|
|
716
962
|
/**
|
|
717
963
|
* In-flight `client.add()` promises keyed by the same key as `torrents`.
|
|
@@ -907,7 +1153,6 @@ export class TorrentPool {
|
|
|
907
1153
|
*
|
|
908
1154
|
* @type {WeakMap<import("webtorrent").Torrent, Map<number, number>>}
|
|
909
1155
|
*/
|
|
910
|
-
this.fileUsageByTorrent = new WeakMap();
|
|
911
1156
|
|
|
912
1157
|
this.client.on("error", (error) => {
|
|
913
1158
|
logger.error(`WebTorrent client error: ${error.message}`);
|
|
@@ -938,88 +1183,6 @@ export class TorrentPool {
|
|
|
938
1183
|
this.#uploadAdjustTimer.unref?.();
|
|
939
1184
|
}
|
|
940
1185
|
|
|
941
|
-
/**
|
|
942
|
-
* Leave the swarm of a torrent nobody is reading, and rejoin it when
|
|
943
|
-
* somebody is.
|
|
944
|
-
*
|
|
945
|
-
* A PROXY THAT NEEDS NOTHING FROM A SWARM SHOULD NOT BE IN THAT SWARM. While
|
|
946
|
-
* a file is being watched every connection is worth keeping — the one that
|
|
947
|
-
* has delivered nothing yet may deliver next — so this is not a limit on
|
|
948
|
-
* connections and never fires during playback. It fires when NOTHING is
|
|
949
|
-
* stated about this torrent at all: no window from any reader, no file held.
|
|
950
|
-
*
|
|
951
|
-
* What that state cost until now: WebTorrent enforces `maxConns` only on
|
|
952
|
-
* peers it dials (`_drain`), while `_addIncomingPeer` checks that the torrent
|
|
953
|
-
* is neither destroyed nor paused and registers the peer. A proxy with its
|
|
954
|
-
* port mapped is reachable, so connections arrive and are never turned away —
|
|
955
|
-
* field 2026-09-11, 249 connected at the start of one viewing and 596 at the
|
|
956
|
-
* end, 15 862 more queued, on a file complete for three quarters of an hour,
|
|
957
|
-
* each of them served by reading a 4 MB piece off the disk for every 16 KB
|
|
958
|
-
* sent.
|
|
959
|
-
*
|
|
960
|
-
* `paused` is the library's own word for this and the path it already checks,
|
|
961
|
-
* so nothing here fights it. The pause alone does not close what is already
|
|
962
|
-
* open, so the peers are let go by hand; the data stays exactly where it is,
|
|
963
|
-
* and the next reader resumes the torrent rather than fetching it again.
|
|
964
|
-
*
|
|
965
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
966
|
-
* @returns {void}
|
|
967
|
-
*/
|
|
968
|
-
followTheReaders(torrent) {
|
|
969
|
-
if (!torrent || torrent.destroyed) {
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
const usage = this.fileUsageByTorrent.get(torrent);
|
|
973
|
-
const isRead = Boolean(usage && usage.size > 0);
|
|
974
|
-
if (isRead) {
|
|
975
|
-
// Recorded on the torrent, as `hasActiveReader` and `hasUnmetDemand`
|
|
976
|
-
// beside it are: it is a fact about this torrent and it lives as long as
|
|
977
|
-
// the torrent does.
|
|
978
|
-
torrent.hasBeenRead = true;
|
|
979
|
-
}
|
|
980
|
-
const isWanted = isRead || demandFor(torrent).register.windows().length > 0;
|
|
981
|
-
// A TORRENT NOBODY HAS READ YET IS BEING SET UP, NOT ABANDONED. Field
|
|
982
|
-
// 2026-09-11, and it broke playback outright: a torrent was added at
|
|
983
|
-
// 20:57:17, its first peer connected at 20:57:18, and this pass took it out
|
|
984
|
-
// of the swarm at 20:57:21 — while the read of the file's edges was still
|
|
985
|
-
// in flight and the playback plan was being built. Nothing rejoined it,
|
|
986
|
-
// because rejoining waits for a reader and a reader cannot arrive: the
|
|
987
|
-
// header it needs is downloaded by the swarm that was just let go.
|
|
988
|
-
//
|
|
989
|
-
// The condition is a state and not a period: having been read at least once
|
|
990
|
-
// is what tells a film somebody left from a film nobody has opened yet. One
|
|
991
|
-
// that is never read at all goes by the pool's own idle removal, which is
|
|
992
|
-
// where that belongs.
|
|
993
|
-
if (!isWanted && torrent.hasBeenRead !== true) {
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
if (isWanted && torrent.paused === true) {
|
|
997
|
-
torrent.resume();
|
|
998
|
-
logger.info(
|
|
999
|
-
`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] rejoined the swarm — somebody is reading it again`
|
|
1000
|
-
);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (isWanted || torrent.paused === true) {
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
torrent.pause();
|
|
1007
|
-
const open = Array.isArray(torrent.wires) ? torrent.wires.length : 0;
|
|
1008
|
-
let closed = 0;
|
|
1009
|
-
for (const peer of [...(torrent._peers?.values?.() ?? [])]) {
|
|
1010
|
-
try {
|
|
1011
|
-
peer.destroy();
|
|
1012
|
-
closed += 1;
|
|
1013
|
-
} catch {
|
|
1014
|
-
// A peer already going: nothing to do, and nothing worth failing for.
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
logger.info(
|
|
1018
|
-
`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] left the swarm — nobody is reading it, ` +
|
|
1019
|
-
`${open} connection(s) open, ${closed} let go; the data stays and the next reader rejoins`
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
1186
|
/**
|
|
1024
1187
|
* Re-evaluate and apply the client-wide upload limit from current swarm state
|
|
1025
1188
|
* (see {@link decideUploadLimit}). Runs on a timer; only calls into WebTorrent
|
|
@@ -1120,13 +1283,14 @@ export class TorrentPool {
|
|
|
1120
1283
|
*/
|
|
1121
1284
|
#stateBackgroundFill(torrent) {
|
|
1122
1285
|
const { register } = demandFor(torrent);
|
|
1123
|
-
const usage = this.fileUsageByTorrent.get(torrent);
|
|
1124
1286
|
const pieceLength = Number(torrent.pieceLength);
|
|
1125
1287
|
if (!Number.isFinite(pieceLength) || pieceLength <= 0) {
|
|
1126
1288
|
return;
|
|
1127
1289
|
}
|
|
1128
|
-
for
|
|
1129
|
-
|
|
1290
|
+
// The files anything is stated for — which is the same list the reader
|
|
1291
|
+
// counts used to give and is one fact rather than two.
|
|
1292
|
+
for (const fileIndex of register.files()) {
|
|
1293
|
+
const file = torrent.files?.[fileIndex] ?? null;
|
|
1130
1294
|
const claimant = `background-fill:${fileIndex}`;
|
|
1131
1295
|
if (!file) {
|
|
1132
1296
|
register.withdraw(claimant);
|
|
@@ -1174,6 +1338,22 @@ export class TorrentPool {
|
|
|
1174
1338
|
* @param {number} durationSeconds
|
|
1175
1339
|
*/
|
|
1176
1340
|
applyPriorityMap(torrent, fileIndex, zones, durationSeconds) {
|
|
1341
|
+
// A MAP WITH NOTHING IN IT IS A STATEMENT, and it is answered before
|
|
1342
|
+
// anything else is looked at. It says nobody wants any of this file, and
|
|
1343
|
+
// that is true whatever the file's length is or whether the torrent still
|
|
1344
|
+
// holds a list — so making it wait on either would be making the departure
|
|
1345
|
+
// depend on facts that are gone by the time it is said.
|
|
1346
|
+
if (Array.isArray(zones) && zones.length === 0) {
|
|
1347
|
+
withdrawPriorityMap(torrent, fileIndex);
|
|
1348
|
+
// The ends of the file go with it. They are kept for as long as the file
|
|
1349
|
+
// is open, and this is what says it is not.
|
|
1350
|
+
withdrawFileEdges(torrent, fileIndex);
|
|
1351
|
+
// And this may have been the last thing anybody wanted of this torrent.
|
|
1352
|
+
// Optional because these two are functions of a torrent and are exercised
|
|
1353
|
+
// as such; called on the pool, the pool also acts on what they said.
|
|
1354
|
+
this?.followTheDemand?.(torrent);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1177
1357
|
const file = Array.isArray(torrent?.files) ? torrent.files[fileIndex] : null;
|
|
1178
1358
|
const length = Number(file?.length);
|
|
1179
1359
|
const duration = Number(durationSeconds);
|
|
@@ -1248,15 +1428,7 @@ export class TorrentPool {
|
|
|
1248
1428
|
});
|
|
1249
1429
|
// Bands the map no longer has: a viewer moved on, and what they had wanted
|
|
1250
1430
|
// is not wanted by anybody now.
|
|
1251
|
-
|
|
1252
|
-
if (!String(window.claimant).startsWith(`${MAP_CLAIMANT}:${fileIndex}:`)) {
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
const index = Number(String(window.claimant).split(":").pop());
|
|
1256
|
-
if (!(index < ordered.length)) {
|
|
1257
|
-
register.withdraw(window.claimant);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1431
|
+
withdrawPriorityMap(torrent, fileIndex, ordered.length);
|
|
1260
1432
|
// WHAT THE SWARM WAS ACTUALLY TOLD, said on change and never on a timer.
|
|
1261
1433
|
// The map was applied in silence: that it had been BUILT was visible in the
|
|
1262
1434
|
// encoding's own line, and that the download had received it was visible
|
|
@@ -1280,6 +1452,9 @@ export class TorrentPool {
|
|
|
1280
1452
|
.sort((left, right) => right[0] - left[0])
|
|
1281
1453
|
.map(([level, held]) => `${urgencyName(level)} ${held.zones} zone(s) ${held.megabytes.toFixed(0)}MB`)
|
|
1282
1454
|
.join(", ");
|
|
1455
|
+
// Something is wanted of this torrent, which may be news: a map published
|
|
1456
|
+
// for a file of a torrent whose swarm was let go is what takes it back.
|
|
1457
|
+
this?.followTheDemand?.(torrent);
|
|
1283
1458
|
const said = `${fileIndex}:${shape}`;
|
|
1284
1459
|
if (lastMapSaid.get(torrent) !== said) {
|
|
1285
1460
|
lastMapSaid.set(torrent, said);
|
|
@@ -1293,8 +1468,7 @@ export class TorrentPool {
|
|
|
1293
1468
|
#reportStalledDownloads() {
|
|
1294
1469
|
const now = Date.now();
|
|
1295
1470
|
for (const torrent of this.torrents.values()) {
|
|
1296
|
-
|
|
1297
|
-
if (!usage || usage.size === 0 || torrent?.done === true) {
|
|
1471
|
+
if (!isWanted(torrent) || torrent?.done === true) {
|
|
1298
1472
|
continue;
|
|
1299
1473
|
}
|
|
1300
1474
|
const speed = typeof torrent.downloadSpeed === "number" ? torrent.downloadSpeed : 0;
|
|
@@ -1334,27 +1508,25 @@ export class TorrentPool {
|
|
|
1334
1508
|
if (!this.client || this.client.destroyed || typeof this.client.throttleUpload !== "function") {
|
|
1335
1509
|
return;
|
|
1336
1510
|
}
|
|
1337
|
-
const active = torrentsForUploadPolicy(
|
|
1338
|
-
this.torrents.values(),
|
|
1339
|
-
this.fileUsageByTorrent,
|
|
1340
|
-
Date.now()
|
|
1341
|
-
);
|
|
1511
|
+
const active = torrentsForUploadPolicy(this.torrents.values(), Date.now());
|
|
1342
1512
|
// The one place the swarm is told anything: it reads what everybody has
|
|
1343
1513
|
// stated and works out for itself what to ask for, including whether the
|
|
1344
1514
|
// speculative levels may be stated at all — which is a question about every
|
|
1345
1515
|
// torrent at once, because they share the link.
|
|
1346
1516
|
reconcileAll();
|
|
1347
1517
|
for (const torrent of this.torrents.values()) {
|
|
1348
|
-
|
|
1349
|
-
if (!usage || usage.size === 0 || torrent?.done === true) {
|
|
1518
|
+
if (!isWanted(torrent) || torrent?.done === true) {
|
|
1350
1519
|
continue;
|
|
1351
1520
|
}
|
|
1352
1521
|
this.#stateBackgroundFill(torrent);
|
|
1522
|
+
// And act on what that just said. The fill withdraws itself once nothing
|
|
1523
|
+
// else wants the file, so a torrent everybody has left can be held by the
|
|
1524
|
+
// fill's own claim until this pass — and without this line the moment it
|
|
1525
|
+
// goes would be noticed by nobody, leaving the torrent in its swarm with
|
|
1526
|
+
// no idle clock running.
|
|
1527
|
+
this.followTheDemand(torrent);
|
|
1353
1528
|
}
|
|
1354
1529
|
this.#reportStalledDownloads();
|
|
1355
|
-
for (const torrent of this.torrents.values()) {
|
|
1356
|
-
this.followTheReaders(torrent);
|
|
1357
|
-
}
|
|
1358
1530
|
const { bytesPerSec, reason } = decideUploadLimit(active);
|
|
1359
1531
|
if (bytesPerSec === this.#uploadLimit) {
|
|
1360
1532
|
return;
|
|
@@ -1393,12 +1565,10 @@ export class TorrentPool {
|
|
|
1393
1565
|
if (used <= this.#maxDiskBytes) {
|
|
1394
1566
|
return;
|
|
1395
1567
|
}
|
|
1396
|
-
// Candidates: pooled torrents
|
|
1568
|
+
// Candidates: pooled torrents nothing is wanted of, the longest unwanted
|
|
1569
|
+
// first.
|
|
1397
1570
|
const candidates = [...this.torrents.values()]
|
|
1398
|
-
.filter((torrent) =>
|
|
1399
|
-
const usage = this.fileUsageByTorrent.get(torrent);
|
|
1400
|
-
return !usage || usage.size === 0;
|
|
1401
|
-
})
|
|
1571
|
+
.filter((torrent) => !isWanted(torrent))
|
|
1402
1572
|
.sort(
|
|
1403
1573
|
(earlier, later) =>
|
|
1404
1574
|
(this.#lastAccess.get(earlier) ?? 0) - (this.#lastAccess.get(later) ?? 0)
|
|
@@ -1666,7 +1836,6 @@ export class TorrentPool {
|
|
|
1666
1836
|
this.torrents.delete(otherKey);
|
|
1667
1837
|
}
|
|
1668
1838
|
}
|
|
1669
|
-
this.fileUsageByTorrent.delete(existing);
|
|
1670
1839
|
this.#lastAccess.delete(existing);
|
|
1671
1840
|
this.#readPositionByTorrent.delete(existing);
|
|
1672
1841
|
this.client.remove(existing, { destroyStore: true }, () => {
|
|
@@ -1730,6 +1899,11 @@ export class TorrentPool {
|
|
|
1730
1899
|
// leave the survivor unwatched. Attaching twice costs nothing — the
|
|
1731
1900
|
// guard makes the second call a no-op when it is the same object.
|
|
1732
1901
|
this.#attachSwarmDiagnostics(readyTorrent);
|
|
1902
|
+
// On the clock from the moment it exists. A torrent nobody ever states
|
|
1903
|
+
// anything about — a file list fetched and never played — used to be
|
|
1904
|
+
// held for the life of the process, because the only thing that ever
|
|
1905
|
+
// started the idle timer was a reader letting go.
|
|
1906
|
+
this.followTheDemand(readyTorrent);
|
|
1733
1907
|
resolve(readyTorrent);
|
|
1734
1908
|
});
|
|
1735
1909
|
// Attached to what `add` returns, NOT inside its callback. That callback
|
|
@@ -1748,62 +1922,61 @@ export class TorrentPool {
|
|
|
1748
1922
|
}
|
|
1749
1923
|
|
|
1750
1924
|
/**
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1925
|
+
* Torrents something has been wanted of at least once, which is what tells a
|
|
1926
|
+
* departure from a beginning.
|
|
1927
|
+
*
|
|
1928
|
+
* @type {WeakSet<object>}
|
|
1929
|
+
*/
|
|
1930
|
+
#wasWanted = new WeakSet();
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* ACT ON WHAT IS WANTED OF THIS TORRENT, and on nothing else.
|
|
1934
|
+
*
|
|
1935
|
+
* Called wherever what is wanted changes — a priority map applied or emptied,
|
|
1936
|
+
* the ends of a file stated — because those are the moments, and a pass that
|
|
1937
|
+
* asks every torrent every few seconds has the wrong answer between two of
|
|
1938
|
+
* them. That is not a theory: 2.83.1 asked such a question every five seconds
|
|
1939
|
+
* and a torrent added three seconds earlier answered "nobody", so it left its
|
|
1940
|
+
* swarm with 741 connections let go and playback never started.
|
|
1941
|
+
*
|
|
1942
|
+
* What it does NOT do is decide whether a torrent is wanted. That is
|
|
1943
|
+
* {@link isWanted}, which reads what has been stated, and the only reason
|
|
1944
|
+
* this is a method at all is that the idle timer and the eviction order are
|
|
1945
|
+
* the pool's own.
|
|
1754
1946
|
*
|
|
1755
1947
|
* @param {import("webtorrent").Torrent} torrent
|
|
1756
|
-
* @
|
|
1757
|
-
* @returns {() => void} Release function — call it once when done streaming.
|
|
1948
|
+
* @returns {void}
|
|
1758
1949
|
*/
|
|
1759
|
-
|
|
1760
|
-
if (!torrent ||
|
|
1761
|
-
return
|
|
1950
|
+
followTheDemand(torrent) {
|
|
1951
|
+
if (!torrent || torrent.destroyed) {
|
|
1952
|
+
return;
|
|
1762
1953
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1954
|
+
const { swarm, onTheClock } = swarmDecisionFor({
|
|
1955
|
+
wanted: isWanted(torrent),
|
|
1956
|
+
everWanted: this.#wasWanted.has(torrent)
|
|
1957
|
+
});
|
|
1958
|
+
if (swarm === "take") {
|
|
1959
|
+
this.#wasWanted.add(torrent);
|
|
1960
|
+
this.#cancelIdleRemoval(torrent);
|
|
1961
|
+
// When it was last wanted, which is the order the disk cap evicts in.
|
|
1962
|
+
this.#lastAccess.set(torrent, Date.now());
|
|
1963
|
+
rejoinSwarm(torrent);
|
|
1767
1964
|
}
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
torrent.resume();
|
|
1777
|
-
logger.info(
|
|
1778
|
-
`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] rejoined the swarm — a reader arrived`
|
|
1779
|
-
);
|
|
1965
|
+
// A PROXY THAT NEEDS NOTHING FROM A SWARM SHOULD NOT BE IN THAT SWARM. The
|
|
1966
|
+
// data stays on disk; the next thing that wants a byte of it takes the
|
|
1967
|
+
// swarm back.
|
|
1968
|
+
if (swarm === "let go") {
|
|
1969
|
+
leaveSwarm(torrent);
|
|
1970
|
+
}
|
|
1971
|
+
if (onTheClock) {
|
|
1972
|
+
this.#scheduleIdleRemoval(torrent);
|
|
1780
1973
|
}
|
|
1781
|
-
usage.set(fileIndex, (usage.get(fileIndex) ?? 0) + 1);
|
|
1782
|
-
|
|
1783
|
-
let released = false;
|
|
1784
|
-
return () => {
|
|
1785
|
-
if (released) {
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
released = true;
|
|
1789
|
-
const nextCount = (usage.get(fileIndex) ?? 0) - 1;
|
|
1790
|
-
if (nextCount > 0) {
|
|
1791
|
-
usage.set(fileIndex, nextCount);
|
|
1792
|
-
} else {
|
|
1793
|
-
usage.delete(fileIndex);
|
|
1794
|
-
}
|
|
1795
|
-
if (usage.size === 0) {
|
|
1796
|
-
this.fileUsageByTorrent.delete(torrent);
|
|
1797
|
-
// No active readers — schedule removal (with store) after an idle TTL.
|
|
1798
|
-
this.#scheduleIdleRemoval(torrent);
|
|
1799
|
-
}
|
|
1800
|
-
};
|
|
1801
1974
|
}
|
|
1802
1975
|
|
|
1803
1976
|
/**
|
|
1804
1977
|
* Schedule removal of a torrent (with its on-disk store) after
|
|
1805
|
-
* {@link TORRENT_IDLE_TTL_MS} of
|
|
1806
|
-
* any existing timer for the torrent.
|
|
1978
|
+
* {@link TORRENT_IDLE_TTL_MS} of nothing being wanted of it. Idempotent —
|
|
1979
|
+
* replaces any existing timer for the torrent.
|
|
1807
1980
|
*
|
|
1808
1981
|
* @param {import("webtorrent").Torrent} torrent
|
|
1809
1982
|
* @returns {void}
|
|
@@ -1818,11 +1991,13 @@ export class TorrentPool {
|
|
|
1818
1991
|
logger.info(`torrent-pool: scheduling idle removal for "${name}" [${infoHashShort}] in ${TORRENT_IDLE_TTL_MS / 1000}s`);
|
|
1819
1992
|
const timer = setTimeout(() => {
|
|
1820
1993
|
this.#idleTimers.delete(torrent);
|
|
1821
|
-
// Re-check:
|
|
1822
|
-
//
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1994
|
+
// Re-check: anything stated since would have cancelled this timer, but
|
|
1995
|
+
// guard anyway against a race.
|
|
1996
|
+
if (isWanted(torrent)) {
|
|
1997
|
+
logger.info(
|
|
1998
|
+
`torrent-pool: idle timer fired for "${name}" [${infoHashShort}] but ` +
|
|
1999
|
+
`${demandFor(torrent).register.size} thing(s) are stated for it — keep`
|
|
2000
|
+
);
|
|
1826
2001
|
return;
|
|
1827
2002
|
}
|
|
1828
2003
|
logger.warn(`torrent-pool: idle TTL fired for "${name}" [${infoHashShort}] — removing torrent (reason=idle-ttl caller=scheduleIdleRemoval)`);
|
|
@@ -1882,7 +2057,6 @@ export class TorrentPool {
|
|
|
1882
2057
|
return;
|
|
1883
2058
|
}
|
|
1884
2059
|
forgetTorrent(torrent);
|
|
1885
|
-
this.fileUsageByTorrent.delete(torrent);
|
|
1886
2060
|
this.#lastAccess.delete(torrent);
|
|
1887
2061
|
this.#readPositionByTorrent.delete(torrent);
|
|
1888
2062
|
const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
|
|
@@ -1915,8 +2089,7 @@ export class TorrentPool {
|
|
|
1915
2089
|
}
|
|
1916
2090
|
const infoHash = String(torrent.infoHash ?? "?").slice(0, 8);
|
|
1917
2091
|
const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
|
|
1918
|
-
const
|
|
1919
|
-
const refcount = usage ? usage.size : 0;
|
|
2092
|
+
const stated = demandFor(torrent).register.size;
|
|
1920
2093
|
const hasData = (() => { try { return torrentDownloadedBytes(torrent); } catch { return -1; } })();
|
|
1921
2094
|
// Capture caller for diagnostics — not for control flow.
|
|
1922
2095
|
const caller = new Error().stack?.split("\n")[2]?.trim() ?? "";
|
|
@@ -1927,10 +2100,9 @@ export class TorrentPool {
|
|
|
1927
2100
|
break;
|
|
1928
2101
|
}
|
|
1929
2102
|
}
|
|
1930
|
-
this.fileUsageByTorrent.delete(torrent);
|
|
1931
2103
|
this.#lastAccess.delete(torrent);
|
|
1932
2104
|
this.#readPositionByTorrent.delete(torrent);
|
|
1933
|
-
logger.warn(`torrent-pool: removing torrent "${name}" [${infoHash}] reason=${reason}
|
|
2105
|
+
logger.warn(`torrent-pool: removing torrent "${name}" [${infoHash}] reason=${reason} stated=${stated} downloaded=${hasData}B caller=${caller}`);
|
|
1934
2106
|
try {
|
|
1935
2107
|
torrent.destroy({ destroyStore: true }, () => {
|
|
1936
2108
|
logger.info(`torrent-pool: removed torrent "${name}" [${infoHash}] reason=${reason} and its store`);
|
|
@@ -2158,16 +2330,31 @@ export class TorrentPool {
|
|
|
2158
2330
|
* @param {number} [options.headBytes=262144] - Leading bytes to fetch (default 256 KB).
|
|
2159
2331
|
* @param {number} [options.tailBytes=2097152] - Trailing bytes to fetch (default 2 MB).
|
|
2160
2332
|
* @param {number} [options.timeoutMs=300000] - Maximum wait time in milliseconds (default 5 min).
|
|
2333
|
+
* @param {boolean} [options.awaited=false] - Whether somebody is waiting for
|
|
2334
|
+
* these bytes right now. The playback plan is: it cannot answer until the
|
|
2335
|
+
* file has said what is in it, and a person is watching a loading screen
|
|
2336
|
+
* meanwhile. The warm-up is NOT, by its whole purpose — it happens while
|
|
2337
|
+
* the viewer is still choosing, so it must not outrank another film that
|
|
2338
|
+
* somebody is watching on this proxy this minute.
|
|
2161
2339
|
* @returns {Promise<void>}
|
|
2162
2340
|
*/
|
|
2163
2341
|
async prefetchFileEdges(
|
|
2164
2342
|
torrent,
|
|
2165
2343
|
fileIndex,
|
|
2166
|
-
{ headBytes = 256 * 1024, tailBytes = 2 * 1024 * 1024, timeoutMs = 300_000 } = {}
|
|
2344
|
+
{ headBytes = 256 * 1024, tailBytes = 2 * 1024 * 1024, timeoutMs = 300_000, awaited = false } = {}
|
|
2167
2345
|
) {
|
|
2168
2346
|
if (!torrent || !Array.isArray(torrent.files)) {
|
|
2169
2347
|
return;
|
|
2170
2348
|
}
|
|
2349
|
+
// THE ENDS OF THIS FILE ARE WANTED, and they go on being wanted after this
|
|
2350
|
+
// read is over: the file's own directory lives there, and a seek asks for
|
|
2351
|
+
// it again. Stated before the read rather than by it, and kept afterwards
|
|
2352
|
+
// at the level of something nobody is waiting for.
|
|
2353
|
+
stateFileEdges(torrent, fileIndex, awaited ? Urgency.NEAR : Urgency.TAIL);
|
|
2354
|
+
// The first thing ever stated about a torrent being opened, and therefore
|
|
2355
|
+
// what keeps it in its swarm through the seconds when nothing else can say
|
|
2356
|
+
// anything about it.
|
|
2357
|
+
this.followTheDemand(torrent);
|
|
2171
2358
|
// Two callers can ask for the same edges at once: the warm-up that starts
|
|
2172
2359
|
// when a torrent is picked, and the playback plan a moment later. Reading
|
|
2173
2360
|
// the same two pieces twice costs nothing in bandwidth — the torrent
|
|
@@ -2184,6 +2371,10 @@ export class TorrentPool {
|
|
|
2184
2371
|
return await prefetch;
|
|
2185
2372
|
} finally {
|
|
2186
2373
|
this.#edgePrefetches.delete(inFlightKey);
|
|
2374
|
+
// Nobody is waiting for them now. Said with `lower`, because this is the
|
|
2375
|
+
// one caller entitled to put them back down: the read it belongs to is
|
|
2376
|
+
// over, and there is no other — a file's edges are fetched once at a time.
|
|
2377
|
+
stateFileEdges(torrent, fileIndex, Urgency.TAIL, { lower: true });
|
|
2187
2378
|
}
|
|
2188
2379
|
}
|
|
2189
2380
|
|