@torrent-tv/proxy 2.79.0 → 2.80.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.
@@ -3,7 +3,14 @@
3
3
  *
4
4
  * Two responsibilities, deliberately kept apart from any storage:
5
5
  *
6
- * - **recency**, so the piece evicted is the one least likely to be wanted;
6
+ * - **how much a piece is wanted**, so the piece evicted is the one wanted
7
+ * least. That number comes from the priority map, which is the one place
8
+ * that knows where the viewers are; recency only separates pieces the map
9
+ * wants equally. Recency alone cannot answer it: a reader walking a film
10
+ * touches each piece once, so the piece the decoder will want in two seconds
11
+ * looks exactly as stale as one fetched forty minutes ago and never read
12
+ * again — and with the encoder running ahead of the viewer, the second kind
13
+ * is what fills the store;
7
14
  * - **pinning**, so a piece being read cannot be evicted at all.
8
15
  *
9
16
  * The second is not a refinement of the first. webtor's seeder relies on recency
@@ -28,18 +35,16 @@ export class PieceLru {
28
35
  /** Piece index → number of readers currently holding it. */
29
36
  #pins = new Map();
30
37
  /**
31
- * Reader id → the piece range it expects to read next.
38
+ * Claimant → the piece range it states, and how much it wants it.
32
39
  *
33
- * Recency alone does not describe this. A reader walking a film touches its
34
- * pieces once, so the piece the decoder will want in two seconds looks
35
- * exactly as stale as one fetched forty minutes ago and never read again
36
- * and with the encoder running ahead of the viewer, the second kind is what
37
- * fills the store. Measured 2026-08-04: the hit rate fell from 100% to 45.7%
38
- * with 221 pieces read back from disk in one session.
40
+ * The ranges are the priority map's zones, stated by whoever holds the map,
41
+ * plus the piece a read is stopped on. The number is the map's own: lower is
42
+ * more urgent, and a piece no range covers is wanted by nobody at all.
39
43
  *
40
44
  * A preference, not a pin. At the smallest budget the store guarantees only
41
- * two resident pieces, so a hard hold on a window would deadlock it; when
42
- * nothing unprotected is left, protection is ignored rather than obeyed.
45
+ * two resident pieces, so a hard hold on a zone would deadlock it; when
46
+ * everything resident is wanted, the least wanted of them goes rather than
47
+ * nothing going at all.
43
48
  */
44
49
  #protected = new Map();
45
50
  #capacity;
@@ -164,8 +169,8 @@ export class PieceLru {
164
169
  }
165
170
 
166
171
  /**
167
- * The least recently used piece that is free to go, or `null` when every
168
- * resident piece is pinned.
172
+ * The least wanted piece that is free to go, or `null` when every resident
173
+ * piece is pinned.
169
174
  *
170
175
  * Returning `null` rather than evicting a pinned piece is the whole point:
171
176
  * the caller must then wait or fail, never take memory out from under a
@@ -194,22 +199,63 @@ export class PieceLru {
194
199
  * victim is inside one, and -1 when no reader has declared anything.
195
200
  */
196
201
  evictionChoice() {
197
- // First choice: the least recently used piece nobody is reading and nobody
198
- // is about to read.
202
+ /** @type {{ index: number, want: number } | null} */
203
+ let victim = null;
204
+ // `#order` runs least-recently-used first, so among pieces the map wants
205
+ // equally the first one seen is the stalest — recency decides the tie and
206
+ // nothing else. A piece no zone covers is wanted by nobody, which is the
207
+ // most anything can be un-wanted, so the walk stops at the first of those:
208
+ // that is the ordinary case and it costs one step.
199
209
  for (const index of this.#order) {
200
- if (!this.#pins.has(index) && !this.#isProtected(index)) {
201
- return { index, protectionYielded: false, distance: this.#distanceToWindow(index) };
210
+ if (this.#pins.has(index)) {
211
+ continue;
212
+ }
213
+ const want = this.wantAt(index);
214
+ if (victim === null || want > victim.want) {
215
+ victim = { index, want };
216
+ if (want === Number.POSITIVE_INFINITY) {
217
+ break;
218
+ }
202
219
  }
203
220
  }
204
- // Nothing spare left. Protection yields — it is a preference, and refusing
205
- // here would leave the store unable to admit anything at all. Pins do not
206
- // yield: a piece being read now cannot have its memory taken away.
207
- for (const index of this.#order) {
208
- if (!this.#pins.has(index)) {
209
- return { index, protectionYielded: true, distance: this.#distanceToWindow(index) };
221
+ if (victim === null) {
222
+ // Every resident piece is being read. The caller must wait or fail; it
223
+ // may never take memory out from under a reader.
224
+ return { index: null, protectionYielded: false, distance: -1 };
225
+ }
226
+ return {
227
+ index: victim.index,
228
+ // The map wanted this piece and it is going anyway — the store is being
229
+ // asked to hold more than it has room for, and this piece comes back
230
+ // from disk. Reported so that thrashing is visible as thrashing rather
231
+ // than as ordinary work: 6565 spills and 7575 revivals in 44 minutes on
232
+ // 2026-09-02, with only 53.6 % of reads served from memory.
233
+ protectionYielded: victim.want !== Number.POSITIVE_INFINITY,
234
+ distance: this.#distanceToWindow(victim.index)
235
+ };
236
+ }
237
+
238
+ /**
239
+ * How much the priority map wants this piece, by the most urgent zone that
240
+ * covers it.
241
+ *
242
+ * Public because admission asks it too: whether an arriving piece displaces
243
+ * a resident one is the same comparison as which resident one goes, and
244
+ * answering them from two different quantities is how a store evicts what it
245
+ * has just decided to keep.
246
+ *
247
+ * @param {number} index
248
+ * @returns {number} Lower is more urgent. Infinity when no zone covers it,
249
+ * so a piece nobody asked for compares as less wanted than any zone.
250
+ */
251
+ wantAt(index) {
252
+ let want = Number.POSITIVE_INFINITY;
253
+ for (const range of this.#protected.values()) {
254
+ if (index >= range.from && index <= range.to && range.urgency < want) {
255
+ want = range.urgency;
210
256
  }
211
257
  }
212
- return { index: null, protectionYielded: false, distance: -1 };
258
+ return want;
213
259
  }
214
260
 
215
261
  /**
@@ -279,14 +325,20 @@ export class PieceLru {
279
325
  }
280
326
 
281
327
  /**
282
- * The piece that would be evicted next, and how long it will be waited for.
328
+ * The piece that would be evicted next, how much the map wants it, and how
329
+ * long it will be waited for.
330
+ *
331
+ * Both numbers, because that is the order admission compares them in: the
332
+ * map's level first, and the distance only between pieces the map wants
333
+ * equally.
283
334
  *
284
- * @returns {{ index: number | null, wait: number }}
335
+ * @returns {{ index: number | null, want: number, wait: number }}
285
336
  */
286
337
  nextVictim() {
287
338
  const choice = this.evictionChoice();
288
339
  return {
289
340
  index: choice.index,
341
+ want: choice.index === null ? Number.POSITIVE_INFINITY : this.wantAt(choice.index),
290
342
  wait: choice.index === null ? -1 : this.waitFor(choice.index)
291
343
  };
292
344
  }
@@ -342,20 +394,28 @@ export class PieceLru {
342
394
  }
343
395
 
344
396
  /**
345
- * Declare the pieces a reader expects to need next, replacing whatever it
346
- * declared before. Ranges from different readers add up.
397
+ * State a range of pieces and how much they are wanted, replacing whatever
398
+ * that claimant stated before. Ranges from different claimants add up.
347
399
  *
348
- * @param {string|number} readerId - Identity of the reader, so its own range
349
- * is replaced rather than accumulated.
400
+ * @param {string|number} readerId - Identity of the claimant, so its own
401
+ * range is replaced rather than accumulated.
350
402
  * @param {number} from - First piece, inclusive.
351
403
  * @param {number} to - Last piece, inclusive.
404
+ * @param {number} [urgency] - The priority map's own number, lower being more
405
+ * urgent. A caller that states none is treated as wanting these pieces
406
+ * least of everyone who did state one, so an unstated range can never
407
+ * displace a stated one.
352
408
  * @returns {void}
353
409
  */
354
- protect(readerId, from, to) {
410
+ protect(readerId, from, to, urgency) {
355
411
  if (!Number.isInteger(from) || !Number.isInteger(to) || to < from) {
356
412
  return;
357
413
  }
358
- this.#protected.set(readerId, { from, to });
414
+ this.#protected.set(readerId, {
415
+ from,
416
+ to,
417
+ urgency: Number.isFinite(urgency) ? Number(urgency) : Number.MAX_SAFE_INTEGER
418
+ });
359
419
  }
360
420
 
361
421
  /**