@torrent-tv/proxy 2.9.96 → 2.9.97

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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.9.97
2
+
3
+ - **Fix**: The generous upload of 2.9.96 did not actually reach the moment it was written for. Only torrents with a registered reader were shown to the upload policy, and the first thing done with a new torrent — fetching the file's head and tail for the codec probe — reads through `createReadStream` without registering one. So for the whole of that wait, 8.36 s of the 11.46 s before playback in the measured session, the torrent looked unused and the upload stayed at the near-silent idle floor, during the exact seconds peers decide whether to serve us. A torrent in a hurry now counts whether or not anything is reading it. The selection is a named function of its own so it can be tested without a live swarm — the fault was in which torrents were considered, not in what was decided about them.
4
+
1
5
  ## 2.9.96
2
6
 
3
7
  - **New**: The proxy uploads generously at the two moments a viewer is provably waiting — when a torrent is added, and when the viewer seeks — for 25 s, which is two of BitTorrent's unchoke cycles. Peers serve those who serve them: each re-ranks its takers about every 10 s and opens a few slots to whoever uploaded most, plus one at random, so uploading a token 8-50 KB/s means being picked at random, one slot per cycle. Measured on a session where 96 peers were already connected within 2 s: 64 KB/s after 2 s, 1.6 MB/s after 4 s, 4.8 MB/s after 8 s — and the 16 MB the codec probe needs took **8.36 s of the 11.46 s** before playback could start. The existing reciprocity boost could not help, because it waits for the download to be all but dead (below 200 KB/s) with peers visibly choking us, and a ramp is neither: in that same session it first moved the limit 13.3 s after the torrent was added and reached the generous rate at 43.7 s, both after the wait they were meant to shorten. Seeding policy is otherwise unchanged — near-silence when nothing is being watched, a token upload while reading.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.96",
3
+ "version": "2.9.97",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -115,6 +115,34 @@ const UPLOAD_HURRY_MS = 25_000;
115
115
  * @param {{ floor?: number, idleFloor?: number, boost?: number, starvingSpeed?: number, chokedThreshold?: number, now?: number }} [opts]
116
116
  * @returns {{ bytesPerSec: number, reason: string }}
117
117
  */
118
+ /**
119
+ * The torrents the upload policy is allowed to see.
120
+ *
121
+ * A torrent with a reader qualifies for the obvious reason. A torrent in a
122
+ * HURRY qualifies even without one, and that case is the important one: the
123
+ * first thing done with a new torrent is fetching the file's head and tail for
124
+ * the codec probe, and that read goes straight to `createReadStream` without
125
+ * registering a reader. Judged by readers alone the torrent looks unused for
126
+ * the whole of that wait — 8.36 s of the 11.46 s before playback in the session
127
+ * measured 2026-08-04 — so the upload stayed at the near-silent idle floor
128
+ * during the exact seconds peers were deciding whether to serve us.
129
+ *
130
+ * @param {Iterable<{ hurryUntil?: number }>} torrents
131
+ * @param {Map<object, { size: number }>} usageByTorrent - fileIndex sets, keyed by torrent.
132
+ * @param {number} now
133
+ * @returns {object[]}
134
+ */
135
+ export function torrentsForUploadPolicy(torrents, usageByTorrent, now) {
136
+ const chosen = [];
137
+ for (const torrent of torrents) {
138
+ const usage = usageByTorrent?.get?.(torrent);
139
+ if ((usage && usage.size > 0) || (torrent?.hurryUntil ?? 0) > now) {
140
+ chosen.push(torrent);
141
+ }
142
+ }
143
+ return chosen;
144
+ }
145
+
118
146
  export function decideUploadLimit(activeTorrents, opts = {}) {
119
147
  const floor = opts.floor ?? UPLOAD_FLOOR_BYTES;
120
148
  const idleFloor = opts.idleFloor ?? UPLOAD_IDLE_FLOOR_BYTES;
@@ -465,10 +493,11 @@ export class TorrentPool {
465
493
  if (!this.client || this.client.destroyed || typeof this.client.throttleUpload !== "function") {
466
494
  return;
467
495
  }
468
- const active = [...this.torrents.values()].filter((torrent) => {
469
- const usage = this.fileUsageByTorrent.get(torrent);
470
- return usage && usage.size > 0;
471
- });
496
+ const active = torrentsForUploadPolicy(
497
+ this.torrents.values(),
498
+ this.fileUsageByTorrent,
499
+ Date.now()
500
+ );
472
501
  const { bytesPerSec, reason } = decideUploadLimit(active);
473
502
  if (bytesPerSec === this.#uploadLimit) {
474
503
  return;
@@ -17,7 +17,7 @@
17
17
 
18
18
  import test from "node:test";
19
19
  import assert from "node:assert/strict";
20
- import { decideUploadLimit } from "../services/torrent-pool.js";
20
+ import { decideUploadLimit, torrentsForUploadPolicy } from "../services/torrent-pool.js";
21
21
 
22
22
  const NOW = 1_000_000;
23
23
  const healthy = (extra = {}) => ({
@@ -64,3 +64,33 @@ test("the reciprocity boost still works when no hurry is on", () => {
64
64
  assert.equal(decision.bytesPerSec, 512 * 1024);
65
65
  assert.match(decision.reason, /earn unchoke/);
66
66
  });
67
+
68
+ test("a torrent with no reader still reaches the policy while it is in a hurry", () => {
69
+ // The moment that matters: a torrent has just been added and its head and
70
+ // tail are being fetched for the codec probe. That read goes straight to
71
+ // `createReadStream`, so no reader is registered — and the selection only
72
+ // ever kept torrents that had one, which is where the gap was.
73
+ const hurrying = { name: "film.mkv", hurryUntil: NOW + 20_000 };
74
+ const idle = { name: "other.mkv" };
75
+ const usage = new Map();
76
+
77
+ assert.deepEqual(
78
+ torrentsForUploadPolicy([hurrying, idle], usage, NOW),
79
+ [hurrying],
80
+ "a torrent with no reader yet was ignored"
81
+ );
82
+
83
+ assert.deepEqual(
84
+ torrentsForUploadPolicy([{ ...hurrying, hurryUntil: NOW - 1 }, idle], usage, NOW),
85
+ [],
86
+ "and stops counting once the rush is over"
87
+ );
88
+
89
+ const read = { name: "watched.mkv" };
90
+ usage.set(read, new Set([0]));
91
+ assert.deepEqual(
92
+ torrentsForUploadPolicy([read], usage, NOW),
93
+ [read],
94
+ "a torrent being read still counts, hurry or not"
95
+ );
96
+ });