@torrent-tv/proxy 2.83.0 → 2.83.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.
- package/CHANGELOG.md +35 -0
- package/package.json +1 -1
- package/routes/api/sources/stats/get.js +11 -2
- package/routes/stream/get.js +74 -3
- package/services/delivery-probe.js +248 -43
- package/services/download/SwarmSelection.js +5 -5
- package/services/download/registry.js +20 -0
- package/services/encode/EncodeRun.js +1 -0
- package/services/encode/encode-exit.js +17 -0
- package/services/files/CompletedFiles.js +276 -0
- package/services/files/piece-from-whole-file.js +118 -0
- package/services/output/cut-grid.js +13 -3
- package/services/piece-store/piece-disk-store.js +72 -2
- package/services/piece-store/shared-piece-store.js +274 -27
- package/services/torrent-pool.js +238 -19
- package/services/torrent-worker/client.js +21 -0
- package/services/torrent-worker/protocol.js +9 -1
- package/services/torrent-worker/worker.js +183 -2
- package/test/completed-files.test.js +115 -0
- package/test/cuts-follow-published-grid.test.js +35 -0
- package/test/delivery-probe.test.js +114 -1
- package/test/encode-exit.test.js +18 -0
- package/test/piece-disk-store.test.js +26 -0
- package/test/piece-from-whole-file.test.js +129 -0
- package/test/piece-store-eviction.test.js +28 -15
- package/test/piece-store-never-refuses.test.js +153 -0
- package/test/piece-store-reservations.test.js +16 -3
- package/test/probe-wedge-certainty.test.js +3 -3
- package/test/shared-piece-store.test.js +27 -13
- package/test/stream-route.test.js +41 -0
- package/test/swarm-follows-readers.test.js +126 -0
- package/test/swarm-reach.test.js +5 -0
- package/test/upload-hurry.test.js +27 -0
|
@@ -306,10 +306,19 @@ export class SharedPieceStore {
|
|
|
306
306
|
#counters = {
|
|
307
307
|
fromMemory: 0,
|
|
308
308
|
fromDisk: 0,
|
|
309
|
+
// Reads answered from the file this torrent has been assembled into. Not a
|
|
310
|
+
// miss and not a spill: the bytes were never lost, they are simply kept as
|
|
311
|
+
// a file now.
|
|
312
|
+
fromWholeFile: 0,
|
|
309
313
|
spills: 0,
|
|
310
314
|
revivals: 0,
|
|
311
315
|
blockedByPins: 0,
|
|
312
316
|
waitedForPins: 0,
|
|
317
|
+
// Pieces that went to disk because no block could be had for them. Not a
|
|
318
|
+
// failure — the piece is kept and a later read revives it — but it is the
|
|
319
|
+
// measure of how often the store is asked to hold more than it may, and
|
|
320
|
+
// before 2026-09-11 this case ended the torrent instead of being counted.
|
|
321
|
+
admittedWithoutSlot: 0,
|
|
313
322
|
evictedOnRevise: 0,
|
|
314
323
|
spillFailures: 0,
|
|
315
324
|
// Whether the store is doing its job or being asked to hold more than it
|
|
@@ -375,8 +384,35 @@ export class SharedPieceStore {
|
|
|
375
384
|
#freeBlocks = [];
|
|
376
385
|
/** Blocks that exist at all: free plus holding a piece. */
|
|
377
386
|
#blocksAllocated = 0;
|
|
378
|
-
/**
|
|
379
|
-
|
|
387
|
+
/**
|
|
388
|
+
* The widest window any reader has declared here, in pieces.
|
|
389
|
+
*
|
|
390
|
+
* The floor this store asks for between readers: one window is what the next
|
|
391
|
+
* read will ask for, and it is a measurement rather than a chosen number.
|
|
392
|
+
* See `wantedBytes`.
|
|
393
|
+
*/
|
|
394
|
+
#widestSeenPieces = 0;
|
|
395
|
+
/** The torrent's files in order, passed through to whoever can read them. @type {object[]} */
|
|
396
|
+
#files = [];
|
|
397
|
+
|
|
398
|
+
/** What the whole torrent weighs, so a last piece is not read past its end. */
|
|
399
|
+
#torrentLength = 0;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* How to read a piece neither tier has, or null when there is nowhere else.
|
|
403
|
+
*
|
|
404
|
+
* @type {((where: object) => Promise<Uint8Array | null>) | null}
|
|
405
|
+
*/
|
|
406
|
+
#readElsewhere = null;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Whether a piece can be had elsewhere, asked without reading it — so a
|
|
410
|
+
* spilled copy that has become a duplicate can be dropped.
|
|
411
|
+
*
|
|
412
|
+
* @type {((where: object) => boolean) | null}
|
|
413
|
+
*/
|
|
414
|
+
#isElsewhere = null;
|
|
415
|
+
|
|
380
416
|
/** Whether the last revision had to exceed the machine's share. */
|
|
381
417
|
#beyondTheMachine = false;
|
|
382
418
|
/**
|
|
@@ -432,6 +468,20 @@ export class SharedPieceStore {
|
|
|
432
468
|
this.#growthCeiling = openingCeiling;
|
|
433
469
|
this.#lru = new PieceLru(openingCeiling);
|
|
434
470
|
this.#name = options.name ?? "pieces";
|
|
471
|
+
// THE LAST PLACE A PIECE CAN COME FROM, handed in as two plain functions.
|
|
472
|
+
// Where those bytes are, and what a file is, is not this store's business:
|
|
473
|
+
// it holds pieces, and these say whether one can be had elsewhere and how.
|
|
474
|
+
this.#readElsewhere = typeof options.readPieceElsewhere === "function"
|
|
475
|
+
? options.readPieceElsewhere
|
|
476
|
+
: null;
|
|
477
|
+
this.#isElsewhere = typeof options.isPieceElsewhere === "function"
|
|
478
|
+
? options.isPieceElsewhere
|
|
479
|
+
: null;
|
|
480
|
+
// Plain data the layer above needs to answer those two, and which this
|
|
481
|
+
// store is handed anyway: where each file of the torrent begins and how
|
|
482
|
+
// long the whole of it is.
|
|
483
|
+
this.#files = Array.isArray(options.files) ? options.files : [];
|
|
484
|
+
this.#torrentLength = totalLength;
|
|
435
485
|
// `options.disk` exists so a test can hold a write open or make one fail on
|
|
436
486
|
// purpose. Four of the defects fixed here live in what happens when the
|
|
437
487
|
// disk tier does not answer immediately or at all, and none of them is
|
|
@@ -471,6 +521,13 @@ export class SharedPieceStore {
|
|
|
471
521
|
// is never returned is invisible until the store cannot admit anything,
|
|
472
522
|
// and by then the reason is long gone. At rest this is zero.
|
|
473
523
|
outstanding: this.#outstandingPieces,
|
|
524
|
+
// How long since ANYTHING in this store moved — a write finishing, a
|
|
525
|
+
// piece admitted, a pin released. A claim gives up after
|
|
526
|
+
// `PINNED_WAIT_MS` of exactly this, so it is the figure that says whether
|
|
527
|
+
// a refusal was the store being busy or the store being stuck, and until
|
|
528
|
+
// 2026-09-11 nothing printed it: the field had 152 refusals in 104
|
|
529
|
+
// seconds and no way to tell which.
|
|
530
|
+
stillMs: this.#lastProgressAt > 0 ? Date.now() - this.#lastProgressAt : 0,
|
|
474
531
|
spilled: this.#disk.size,
|
|
475
532
|
// What the spill file ACTUALLY weighs, piece by piece, rather than the
|
|
476
533
|
// piece count times a full piece length. The last piece of a torrent is
|
|
@@ -568,7 +625,7 @@ export class SharedPieceStore {
|
|
|
568
625
|
get wantedBytes() {
|
|
569
626
|
const demand = this.#lru.demand();
|
|
570
627
|
if (demand.readers > 0) {
|
|
571
|
-
this.#
|
|
628
|
+
this.#widestSeenPieces = Math.max(this.#widestSeenPieces, demand.widestPieces);
|
|
572
629
|
const pieces = Math.max(MIN_RESIDENT_PIECES, demand.unionPieces, demand.widestPieces);
|
|
573
630
|
// Plus room to absorb what arrives while a write is finishing. Asking for
|
|
574
631
|
// exactly what the readers want leaves no free place ever, so every
|
|
@@ -577,16 +634,23 @@ export class SharedPieceStore {
|
|
|
577
634
|
// that followed.
|
|
578
635
|
return (pieces + this.slackPieces()) * this.#chunkLength;
|
|
579
636
|
}
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
637
|
+
// WITH NO READER, THE FLOOR IS ONE READER'S WINDOW — the widest this store
|
|
638
|
+
// has actually been asked for, which is measured rather than chosen, and
|
|
639
|
+
// `MIN_RESIDENT_PIECES` only until it has been asked for anything.
|
|
640
|
+
//
|
|
641
|
+
// It used to fall to the minimum the moment the last reader went, and the
|
|
642
|
+
// allowance is re-derived once a minute (`STORE_REPORT_INTERVAL_MS`) while
|
|
643
|
+
// a claim gives up after five seconds. A reader arriving into that minute
|
|
644
|
+
// met a store of two or three blocks: field 2026-09-11, the viewer switched
|
|
645
|
+
// to another file of the same torrent and the store was at
|
|
646
|
+
// `0/3 (0MB of 12MB allowed)` with the machine offering 4.3 GB.
|
|
647
|
+
//
|
|
648
|
+
// Holding one window's worth between readers is not waste: it is what the
|
|
649
|
+
// next read will ask for within seconds, and the pieces in it are the ones
|
|
650
|
+
// that reader left off at. A store that has never been read keeps nothing
|
|
651
|
+
// either — its pieces are arriving for a reader on the way, and they have
|
|
652
|
+
// the disk.
|
|
653
|
+
return (Math.max(MIN_RESIDENT_PIECES, this.#widestSeenPieces) + this.slackPieces()) * this.#chunkLength;
|
|
590
654
|
}
|
|
591
655
|
|
|
592
656
|
reviseGrowthCeiling(allowedBytes) {
|
|
@@ -720,6 +784,28 @@ export class SharedPieceStore {
|
|
|
720
784
|
* @returns {Promise<() => void>} The release, which the caller MUST call in a
|
|
721
785
|
* `finally`. Calling it twice is harmless.
|
|
722
786
|
*/
|
|
787
|
+
/**
|
|
788
|
+
* The same reservation, answering null instead of throwing.
|
|
789
|
+
*
|
|
790
|
+
* For callers that have somewhere else to go: an arriving piece has the disk,
|
|
791
|
+
* so it writes through rather than failing. Keeping the throwing form for
|
|
792
|
+
* callers that genuinely have no alternative is what makes the difference
|
|
793
|
+
* visible at the call site rather than in a catch.
|
|
794
|
+
*
|
|
795
|
+
* @returns {Promise<(() => void) | null>}
|
|
796
|
+
*/
|
|
797
|
+
async #claimSlotOrNull() {
|
|
798
|
+
try {
|
|
799
|
+
return await this.#claimSlot();
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (this.#closed) {
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
this.#counters.admittedWithoutSlot += 1;
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
723
809
|
async #claimSlot() {
|
|
724
810
|
// How long THIS claim has been trying, kept here and not on the store.
|
|
725
811
|
// It was a field, `#pinnedWaitStartedAt`, shared by the two waits inside
|
|
@@ -736,8 +822,8 @@ export class SharedPieceStore {
|
|
|
736
822
|
if (this.#closed) {
|
|
737
823
|
throw new Error("Piece store is closed.");
|
|
738
824
|
}
|
|
739
|
-
const
|
|
740
|
-
if (
|
|
825
|
+
const isReserved = await this.#claimSlotOnce(waitingSince);
|
|
826
|
+
if (isReserved) {
|
|
741
827
|
let released = false;
|
|
742
828
|
return () => {
|
|
743
829
|
if (released) {
|
|
@@ -1028,6 +1114,68 @@ export class SharedPieceStore {
|
|
|
1028
1114
|
}
|
|
1029
1115
|
}
|
|
1030
1116
|
|
|
1117
|
+
/**
|
|
1118
|
+
* One piece, from wherever else it can be had.
|
|
1119
|
+
*
|
|
1120
|
+
* @param {number} index
|
|
1121
|
+
* @returns {Promise<Buffer | null>}
|
|
1122
|
+
*/
|
|
1123
|
+
async #fromWholeFiles(index) {
|
|
1124
|
+
return this.#readElsewhere
|
|
1125
|
+
? this.#readElsewhere({
|
|
1126
|
+
index,
|
|
1127
|
+
pieceLength: this.#chunkLength,
|
|
1128
|
+
length: this.#torrentLength,
|
|
1129
|
+
files: this.#files
|
|
1130
|
+
})
|
|
1131
|
+
: null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Whether a spilled piece is now a duplicate of bytes held elsewhere.
|
|
1136
|
+
*
|
|
1137
|
+
* @param {number} index
|
|
1138
|
+
* @returns {boolean}
|
|
1139
|
+
*/
|
|
1140
|
+
isInWholeFiles(index) {
|
|
1141
|
+
return this.#isElsewhere
|
|
1142
|
+
? this.#isElsewhere({
|
|
1143
|
+
index,
|
|
1144
|
+
pieceLength: this.#chunkLength,
|
|
1145
|
+
length: this.#torrentLength,
|
|
1146
|
+
files: this.#files
|
|
1147
|
+
}) === true
|
|
1148
|
+
: false;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Drop spilled pieces that are now duplicates of bytes held elsewhere.
|
|
1153
|
+
*
|
|
1154
|
+
* A film assembled into a file is on the disk twice until this runs: once as
|
|
1155
|
+
* the file and once as the pieces it was built from. Field 2026-09-11, one
|
|
1156
|
+
* episode: 1417 MB of segments beside 1424 MB of spilled pieces. Nothing is
|
|
1157
|
+
* lost by dropping the second — a read that wants one of those pieces is
|
|
1158
|
+
* answered from the file.
|
|
1159
|
+
*
|
|
1160
|
+
* @returns {number} How many were dropped.
|
|
1161
|
+
*/
|
|
1162
|
+
dropDuplicatesHeldElsewhere() {
|
|
1163
|
+
if (!this.#isElsewhere) {
|
|
1164
|
+
return 0;
|
|
1165
|
+
}
|
|
1166
|
+
let dropped = 0;
|
|
1167
|
+
for (const index of this.#disk.indexes()) {
|
|
1168
|
+
if (this.#buffers.has(index) || this.#evicting.has(index)) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (this.isInWholeFiles(index)) {
|
|
1172
|
+
this.#disk.forget(index);
|
|
1173
|
+
dropped += 1;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return dropped;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1031
1179
|
#wake() {
|
|
1032
1180
|
const waiting = this.#waiters;
|
|
1033
1181
|
this.#waiters = [];
|
|
@@ -1064,7 +1212,15 @@ export class SharedPieceStore {
|
|
|
1064
1212
|
//
|
|
1065
1213
|
// So when the disk is what the store is waiting for, it waits. A completing
|
|
1066
1214
|
// write calls `#noteProgress`, which wakes whoever is here.
|
|
1067
|
-
|
|
1215
|
+
// THE SAME QUANTITY THE GRANT ABOVE TESTS. It used to ask whether
|
|
1216
|
+
// `blocksInUse` alone had reached the ceiling, while what reaches the
|
|
1217
|
+
// ceiling is `blocksInUse + outstanding` — so a store held at its ceiling
|
|
1218
|
+
// by reservations fell past this branch and into the eviction below, which
|
|
1219
|
+
// has nothing to evict when nothing is resident. Field 2026-09-11:
|
|
1220
|
+
// `blocks=2 (0 spare)` with `outstanding=2` against a ceiling of 3, two
|
|
1221
|
+
// writes in flight each of which would have returned a block, and the claim
|
|
1222
|
+
// threw rather than waiting the moment out.
|
|
1223
|
+
if (this.#blocksInFlight() > 0 && this.#blocksInUse() + this.#outstandingPieces >= this.#growthCeiling) {
|
|
1068
1224
|
if (stillFor() < PINNED_WAIT_MS) {
|
|
1069
1225
|
this.#counters.waitedForDisk += 1;
|
|
1070
1226
|
return false;
|
|
@@ -1090,8 +1246,19 @@ export class SharedPieceStore {
|
|
|
1090
1246
|
return false;
|
|
1091
1247
|
}
|
|
1092
1248
|
this.#counters.blockedByPins += 1;
|
|
1249
|
+
// SAY WHICH STATE REFUSED, with the numbers it was read from. The old
|
|
1250
|
+
// text named one state — every resident piece pinned — and the field case
|
|
1251
|
+
// of 2026-09-11 was a different one: `resident=0 pinned=0`, nothing to
|
|
1252
|
+
// evict because nothing was in memory at all, the ceiling held by
|
|
1253
|
+
// reservations and by two writes on their way to disk. A message that
|
|
1254
|
+
// names a state that did not exist costs a day of reading the wrong code.
|
|
1093
1255
|
throw new Error(
|
|
1094
|
-
`
|
|
1256
|
+
`No block can be had for ${PINNED_WAIT_MS}ms: ` +
|
|
1257
|
+
`${this.#buffers.size} resident (${this.#lru.pinnedCount} pinned, ` +
|
|
1258
|
+
`${this.#lru.protectedCount} range(s) declared), ` +
|
|
1259
|
+
`${this.#blocksInUse()} block(s) in use of ${this.#growthCeiling} allowed, ` +
|
|
1260
|
+
`${this.#blocksInFlight()} on their way to disk, ` +
|
|
1261
|
+
`${this.#outstandingPieces} reservation(s) outstanding.`
|
|
1095
1262
|
);
|
|
1096
1263
|
}
|
|
1097
1264
|
|
|
@@ -1135,7 +1302,25 @@ export class SharedPieceStore {
|
|
|
1135
1302
|
}
|
|
1136
1303
|
|
|
1137
1304
|
if (!this.#disk.has(index)) {
|
|
1138
|
-
|
|
1305
|
+
// Not spilled, but the file it belongs to may be here whole — which is
|
|
1306
|
+
// the ordinary case once a film has been assembled and its spilled copy
|
|
1307
|
+
// dropped as the duplicate it had become.
|
|
1308
|
+
const whole = await this.#fromWholeFiles(index);
|
|
1309
|
+
if (!whole) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
this.#counters.fromWholeFile += 1;
|
|
1313
|
+
const release = await this.#claimSlotOrNull();
|
|
1314
|
+
if (release === null) {
|
|
1315
|
+
// No block to put it in. The caller gets the bytes anyway; what it
|
|
1316
|
+
// loses is only that the next read pays for this one again.
|
|
1317
|
+
return whole;
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
return this.#registerPiece(index, this.#copyIntoNewBuffer(index, whole));
|
|
1321
|
+
} finally {
|
|
1322
|
+
release();
|
|
1323
|
+
}
|
|
1139
1324
|
}
|
|
1140
1325
|
|
|
1141
1326
|
const release = await this.#claimSlot();
|
|
@@ -1382,8 +1567,8 @@ export class SharedPieceStore {
|
|
|
1382
1567
|
}
|
|
1383
1568
|
|
|
1384
1569
|
this.#noteArrival();
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1570
|
+
const isDeclared = this.#lru.wants(index);
|
|
1571
|
+
if (isDeclared) {
|
|
1387
1572
|
this.#counters.admittedInsideWindow += 1;
|
|
1388
1573
|
} else {
|
|
1389
1574
|
this.#counters.admittedOutsideWindow += 1;
|
|
@@ -1407,7 +1592,7 @@ export class SharedPieceStore {
|
|
|
1407
1592
|
//
|
|
1408
1593
|
// Only when SOMETHING is stated: before that there is no basis for
|
|
1409
1594
|
// calling one piece more wanted than another.
|
|
1410
|
-
const
|
|
1595
|
+
const isWorseThanTheVictim = () => {
|
|
1411
1596
|
const victim = this.#lru.nextVictim();
|
|
1412
1597
|
if (victim.index === null) {
|
|
1413
1598
|
return false;
|
|
@@ -1420,14 +1605,32 @@ export class SharedPieceStore {
|
|
|
1420
1605
|
return arriving >= 0 && victim.wait >= 0 && arriving > victim.wait;
|
|
1421
1606
|
};
|
|
1422
1607
|
if (this.#lru.protectedCount > 0 && this.#isFullNow()
|
|
1423
|
-
&& (!
|
|
1608
|
+
&& (!isDeclared || isWorseThanTheVictim())) {
|
|
1424
1609
|
this.#counters.admittedToDisk += 1;
|
|
1425
1610
|
await this.#writeThrough(index, bytes);
|
|
1426
1611
|
this.#noteProgress();
|
|
1427
1612
|
return;
|
|
1428
1613
|
}
|
|
1429
1614
|
|
|
1430
|
-
|
|
1615
|
+
// MEMORY IS NEVER A REASON TO REFUSE A PIECE. The piece is verified and
|
|
1616
|
+
// the disk will take it; failing here fails the torrent client's own
|
|
1617
|
+
// write, and the client answers that by destroying the torrent — field
|
|
1618
|
+
// 2026-09-11, `WebTorrent client error: Every resident piece is pinned`,
|
|
1619
|
+
// after which every read of that torrent answered `File 1 not found` for
|
|
1620
|
+
// the life of the process and the viewer could not open anything.
|
|
1621
|
+
//
|
|
1622
|
+
// The branch above already writes through when the store is full and the
|
|
1623
|
+
// arrival is wanted less than what it would displace. It cannot fire
|
|
1624
|
+
// where nothing is declared (`protectedCount > 0`), which is exactly the
|
|
1625
|
+
// state a store between readers is in — and that state is where the
|
|
1626
|
+
// refusal happened.
|
|
1627
|
+
const release = await this.#claimSlotOrNull();
|
|
1628
|
+
if (release === null) {
|
|
1629
|
+
this.#counters.admittedToDisk += 1;
|
|
1630
|
+
await this.#writeThrough(index, bytes);
|
|
1631
|
+
this.#noteProgress();
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1431
1634
|
try {
|
|
1432
1635
|
this.#registerPiece(index, this.#copyIntoNewBuffer(index, bytes));
|
|
1433
1636
|
await this.#forgetOnDisk(index);
|
|
@@ -1461,11 +1664,43 @@ export class SharedPieceStore {
|
|
|
1461
1664
|
return Buffer.from(Buffer.from(buffer, offset, length));
|
|
1462
1665
|
}
|
|
1463
1666
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1667
|
+
// NOT revived into memory. This entry point is the torrent client's: it
|
|
1668
|
+
// reads to answer a peer, and a peer asks for 16 KB while a piece here is
|
|
1669
|
+
// megabytes. Reviving would take a whole block for bytes nobody is going
|
|
1670
|
+
// to read again — and it is the ONE read that can be answered without a
|
|
1671
|
+
// block at all, since the caller is handed its own copy either way.
|
|
1672
|
+
//
|
|
1673
|
+
// Field 2026-09-11, and this is the whole reason the store could not
|
|
1674
|
+
// serve anybody: an upload capped at 512 KB/s produced 63 416 reads of
|
|
1675
|
+
// which 78.4 % missed memory, 49 696 pieces revived whole, and every one
|
|
1676
|
+
// of those revivals competed for a ceiling of three blocks. Claiming a
|
|
1677
|
+
// block for an upload is also how this path reached `#claimSlot`, whose
|
|
1678
|
+
// failure the torrent client turns into a destroyed torrent.
|
|
1679
|
+
// A spill that is still being written owns the only copy there is, so
|
|
1680
|
+
// wait for it exactly as `#revive` does before asking the disk.
|
|
1681
|
+
const spill = this.#evicting.get(index);
|
|
1682
|
+
if (spill) {
|
|
1683
|
+
await spill.catch(() => undefined);
|
|
1467
1684
|
}
|
|
1468
|
-
|
|
1685
|
+
const resident = this.#buffers.get(index);
|
|
1686
|
+
if (resident !== undefined) {
|
|
1687
|
+
this.#lru.touch(index);
|
|
1688
|
+
this.#counters.fromMemory += 1;
|
|
1689
|
+
return Buffer.from(Buffer.from(resident, offset, length));
|
|
1690
|
+
}
|
|
1691
|
+
if (this.#disk.has(index)) {
|
|
1692
|
+
const want = Math.max(0, Math.min(length, this.#lengthOf(index) - offset));
|
|
1693
|
+
const target = Buffer.allocUnsafe(want);
|
|
1694
|
+
await this.#disk.read(index, target, offset);
|
|
1695
|
+
this.#counters.fromDisk += 1;
|
|
1696
|
+
return target;
|
|
1697
|
+
}
|
|
1698
|
+
const whole = await this.#fromWholeFiles(index);
|
|
1699
|
+
if (whole) {
|
|
1700
|
+
this.#counters.fromWholeFile += 1;
|
|
1701
|
+
return Buffer.from(whole.subarray(offset, offset + length));
|
|
1702
|
+
}
|
|
1703
|
+
throw new Error(`Piece ${index} is not in the store.`);
|
|
1469
1704
|
};
|
|
1470
1705
|
|
|
1471
1706
|
fetch().then((bytes) => done(null, bytes), (error) => done(error));
|
|
@@ -1484,6 +1719,18 @@ export class SharedPieceStore {
|
|
|
1484
1719
|
*/
|
|
1485
1720
|
protectRange(readerId, from, to, urgency) {
|
|
1486
1721
|
this.#lru.protect(readerId, from, to, urgency);
|
|
1722
|
+
// A READER DECLARES ITSELF IN A MOMENT; the allowance was re-derived once a
|
|
1723
|
+
// minute. Between the two a read met whatever the store had shrunk to while
|
|
1724
|
+
// nobody was reading, and a claim gives up after five seconds — twelve
|
|
1725
|
+
// times sooner than the store could have grown (field 2026-09-11).
|
|
1726
|
+
//
|
|
1727
|
+
// DELIBERATELY NOT raising the ceiling here. A reader declaring a window
|
|
1728
|
+
// wider than the store may hold is a shortage to absorb, not an instruction
|
|
1729
|
+
// to take more memory: with several viewers on several films the unions add
|
|
1730
|
+
// up across stores, and each store's floor already wins over the machine's
|
|
1731
|
+
// share at the next revision. What the shortage must not do is fail a read,
|
|
1732
|
+
// and that is answered where it arises — no path in this store refuses for
|
|
1733
|
+
// want of memory any more.
|
|
1487
1734
|
}
|
|
1488
1735
|
|
|
1489
1736
|
protectedRanges() {
|