@torrent-tv/proxy 2.9.71 → 2.9.73

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.
@@ -1,171 +1,179 @@
1
- /**
2
- * @file `TorrentPool`'s interface, served from the worker thread.
3
- *
4
- * The routes, the planner, the health report and the session manager all reach
5
- * for a torrent pool and use it the same handful of ways. Rather than rewrite
6
- * every one of them to thread a `sourceKey` through and await what used to be
7
- * immediate, this presents the shape they already expect and does the thread
8
- * hop behind it. Swapping the implementation is then a one-line change at
9
- * construction, and the call sites are untouched — which is what keeps a change
10
- * of this size reviewable.
11
- *
12
- * Two accommodations are needed, and both are deliberate:
13
- *
14
- * - **`acquireFile` and `prioritizeByteRange` stay synchronous.** They return
15
- * nothing the caller inspects, so the command is dispatched and not awaited.
16
- * `acquireFile` hands back a release function exactly as before, which sends
17
- * its own command when called. Awaiting them would mean touching every call
18
- * site for no observable gain.
19
- * - **`getTorrent` needs a `sourceKey`.** Torrent objects cannot cross a
20
- * thread, so the worker keys them. Callers that have one pass it; the rest
21
- * get one derived from the source itself, so the identity stays stable
22
- * across calls for the same torrent.
23
- */
24
-
25
- import crypto from "node:crypto";
26
- import { TorrentWorkerClient } from "./client.js";
27
-
28
- /**
29
- * Stable key for a source, matching how the worker keys its torrents.
30
- *
31
- * Derived from the source itself rather than handed out per request, so two
32
- * routes asking for the same torrent name the same thing on the worker side.
33
- *
34
- * @param {"magnet" | "torrent"} sourceType
35
- * @param {string} source
36
- * @returns {string}
37
- */
38
- function deriveSourceKey(sourceType, source) {
39
- return `${sourceType}:${crypto.createHash("sha1").update(source).digest("hex")}`;
40
- }
41
-
42
- /**
43
- * A torrent pool whose work happens on another thread.
44
- *
45
- * See `protocol.js` for why: the torrent was taking ~85% of the main thread and
46
- * everything owed to a viewer queued behind it.
47
- */
48
- export class WorkerTorrentPool {
49
- #client;
50
- /** Stand-ins by source key, so repeat calls return the same object. */
51
- #torrents = new Map();
52
-
53
- /**
54
- * @param {{ maxDiskBytes?: number }} [options]
55
- */
56
- constructor(options = {}) {
57
- this.#client = new TorrentWorkerClient(options);
58
- }
59
-
60
- /**
61
- * Load (or join) a torrent and return a stand-in for it.
62
- *
63
- * @param {"magnet" | "torrent"} sourceType
64
- * @param {string} source
65
- * @returns {Promise<object>}
66
- */
67
- async getTorrent(sourceType, source) {
68
- const sourceKey = deriveSourceKey(sourceType, source);
69
- const existing = this.#torrents.get(sourceKey);
70
- if (existing) {
71
- return existing;
72
- }
73
- const torrent = await this.#client.getTorrent({ sourceKey, sourceType, source });
74
- this.#torrents.set(sourceKey, torrent);
75
- return torrent;
76
- }
77
-
78
- /**
79
- * Claim a file for reading; the returned function releases it.
80
- *
81
- * Synchronous by design — see the file header.
82
- *
83
- * @param {object} torrent - A stand-in from {@link getTorrent}.
84
- * @param {number} fileIndex
85
- * @returns {() => void}
86
- */
87
- acquireFile(torrent, fileIndex) {
88
- const sourceKey = torrent?.sourceKey;
89
- if (!sourceKey) {
90
- return () => undefined;
91
- }
92
- void this.#client.acquireFile(sourceKey, fileIndex).catch(() => undefined);
93
- let released = false;
94
- return () => {
95
- if (released) {
96
- return;
97
- }
98
- released = true;
99
- void this.#client.releaseFile(sourceKey, fileIndex).catch(() => undefined);
100
- };
101
- }
102
-
103
- /**
104
- * Live download figures for the progress display.
105
- *
106
- * @param {object} torrent
107
- * @param {number | null} [fileIndex]
108
- * @param {{ resumeAnchorByteStart?: number | null }} [options]
109
- * @returns {Promise<object | null>}
110
- */
111
- async getFileStats(torrent, fileIndex = null, options = {}) {
112
- const sourceKey = torrent?.sourceKey;
113
- if (!sourceKey) {
114
- return null;
115
- }
116
- return this.#client.getFileStats({
117
- sourceKey,
118
- fileIndex,
119
- resumeAnchorByteStart: options?.resumeAnchorByteStart ?? null
120
- });
121
- }
122
-
123
- /**
124
- * Reorder piece selection around a read position.
125
- *
126
- * Synchronous by design — see the file header.
127
- *
128
- * @param {object} torrent
129
- * @param {number} fileIndex
130
- * @param {number} byteStart
131
- * @param {number} [windowBytes]
132
- * @returns {void}
133
- */
134
- prioritizeByteRange(torrent, fileIndex, byteStart, windowBytes) {
135
- const sourceKey = torrent?.sourceKey;
136
- if (!sourceKey) {
137
- return;
138
- }
139
- void this.#client
140
- .prioritizeByteRange({ sourceKey, fileIndex, byteStart, windowBytes })
141
- .catch(() => undefined);
142
- }
143
-
144
- /**
145
- * Pre-fetch the head and tail the codec probe needs.
146
- *
147
- * @param {object} torrent
148
- * @param {number} fileIndex
149
- * @param {number} [headBytes]
150
- * @param {number} [tailBytes]
151
- * @param {number} [timeoutMs]
152
- * @returns {Promise<unknown>}
153
- */
154
- async prefetchFileEdges(torrent, fileIndex, headBytes, tailBytes, timeoutMs) {
155
- const sourceKey = torrent?.sourceKey;
156
- if (!sourceKey) {
157
- return null;
158
- }
159
- return this.#client.prefetchFileEdges({ sourceKey, fileIndex, headBytes, tailBytes, timeoutMs });
160
- }
161
-
162
- /**
163
- * Shut the torrent client down and stop the thread.
164
- *
165
- * @returns {Promise<void>}
166
- */
167
- async destroyAll() {
168
- this.#torrents.clear();
169
- await this.#client.destroyAll();
170
- }
171
- }
1
+ /**
2
+ * @file `TorrentPool`'s interface, served from the worker thread.
3
+ *
4
+ * The routes, the planner, the health report and the session manager all reach
5
+ * for a torrent pool and use it the same handful of ways. Rather than rewrite
6
+ * every one of them to thread a `sourceKey` through and await what used to be
7
+ * immediate, this presents the shape they already expect and does the thread
8
+ * hop behind it. Swapping the implementation is then a one-line change at
9
+ * construction, and the call sites are untouched — which is what keeps a change
10
+ * of this size reviewable.
11
+ *
12
+ * Two accommodations are needed, and both are deliberate:
13
+ *
14
+ * - **`acquireFile` and `prioritizeByteRange` stay synchronous.** They return
15
+ * nothing the caller inspects, so the command is dispatched and not awaited.
16
+ * `acquireFile` hands back a release function exactly as before, which sends
17
+ * its own command when called. Awaiting them would mean touching every call
18
+ * site for no observable gain.
19
+ * - **`getTorrent` needs a `sourceKey`.** Torrent objects cannot cross a
20
+ * thread, so the worker keys them. Callers that have one pass it; the rest
21
+ * get one derived from the source itself, so the identity stays stable
22
+ * across calls for the same torrent.
23
+ */
24
+
25
+ import crypto from "node:crypto";
26
+ import { TorrentWorkerClient } from "./client.js";
27
+
28
+ /**
29
+ * Stable key for a source, matching how the worker keys its torrents.
30
+ *
31
+ * Derived from the source itself rather than handed out per request, so two
32
+ * routes asking for the same torrent name the same thing on the worker side.
33
+ *
34
+ * @param {"magnet" | "torrent"} sourceType
35
+ * @param {string} source
36
+ * @returns {string}
37
+ */
38
+ function deriveSourceKey(sourceType, source) {
39
+ return `${sourceType}:${crypto.createHash("sha1").update(source).digest("hex")}`;
40
+ }
41
+
42
+ /**
43
+ * A torrent pool whose work happens on another thread.
44
+ *
45
+ * See `protocol.js` for why: the torrent was taking ~85% of the main thread and
46
+ * everything owed to a viewer queued behind it.
47
+ */
48
+ export class WorkerTorrentPool {
49
+ #client;
50
+ /** Stand-ins by source key, so repeat calls return the same object. */
51
+ #torrents = new Map();
52
+
53
+ /**
54
+ * @param {{ maxDiskBytes?: number }} [options]
55
+ */
56
+ constructor(options = {}) {
57
+ this.#client = new TorrentWorkerClient(options);
58
+ }
59
+
60
+ /**
61
+ * Load (or join) a torrent and return a stand-in for it.
62
+ *
63
+ * @param {"magnet" | "torrent"} sourceType
64
+ * @param {string} source
65
+ * @returns {Promise<object>}
66
+ */
67
+ async getTorrent(sourceType, source) {
68
+ const sourceKey = deriveSourceKey(sourceType, source);
69
+ const existing = this.#torrents.get(sourceKey);
70
+ if (existing) {
71
+ return existing;
72
+ }
73
+ const torrent = await this.#client.getTorrent({ sourceKey, sourceType, source });
74
+ this.#torrents.set(sourceKey, torrent);
75
+ return torrent;
76
+ }
77
+
78
+ /**
79
+ * Claim a file for reading; the returned function releases it.
80
+ *
81
+ * Synchronous by design — see the file header.
82
+ *
83
+ * @param {object} torrent - A stand-in from {@link getTorrent}.
84
+ * @param {number} fileIndex
85
+ * @returns {() => void}
86
+ */
87
+ acquireFile(torrent, fileIndex) {
88
+ const sourceKey = torrent?.sourceKey;
89
+ if (!sourceKey) {
90
+ return () => undefined;
91
+ }
92
+ // Dispatched, not awaited — callers use the result immediately and inspect
93
+ // nothing. But the release MUST NOT overtake it: both are ordinary messages
94
+ // to the worker, and if release arrives first the reader count drops to zero
95
+ // while a read is still running. The idle sweep then removes the torrent AND
96
+ // its downloaded data out from under the encoder — field 2026-08-02:
97
+ // "removed idle torrent ... and its store" mid-playback, after which every
98
+ // read hung and ffmpeg saw an empty input ("Stream ends prematurely at 0").
99
+ // Chaining the release onto the acquire keeps them in order.
100
+ const acquired = this.#client.acquireFile(sourceKey, fileIndex).catch(() => undefined);
101
+ let released = false;
102
+ return () => {
103
+ if (released) {
104
+ return;
105
+ }
106
+ released = true;
107
+ void acquired.then(() => this.#client.releaseFile(sourceKey, fileIndex)).catch(() => undefined);
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Live download figures for the progress display.
113
+ *
114
+ * @param {object} torrent
115
+ * @param {number | null} [fileIndex]
116
+ * @param {{ resumeAnchorByteStart?: number | null }} [options]
117
+ * @returns {Promise<object | null>}
118
+ */
119
+ async getFileStats(torrent, fileIndex = null, options = {}) {
120
+ const sourceKey = torrent?.sourceKey;
121
+ if (!sourceKey) {
122
+ return null;
123
+ }
124
+ return this.#client.getFileStats({
125
+ sourceKey,
126
+ fileIndex,
127
+ resumeAnchorByteStart: options?.resumeAnchorByteStart ?? null
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Reorder piece selection around a read position.
133
+ *
134
+ * Synchronous by design — see the file header.
135
+ *
136
+ * @param {object} torrent
137
+ * @param {number} fileIndex
138
+ * @param {number} byteStart
139
+ * @param {number} [windowBytes]
140
+ * @returns {void}
141
+ */
142
+ prioritizeByteRange(torrent, fileIndex, byteStart, windowBytes) {
143
+ const sourceKey = torrent?.sourceKey;
144
+ if (!sourceKey) {
145
+ return;
146
+ }
147
+ void this.#client
148
+ .prioritizeByteRange({ sourceKey, fileIndex, byteStart, windowBytes })
149
+ .catch(() => undefined);
150
+ }
151
+
152
+ /**
153
+ * Pre-fetch the head and tail the codec probe needs.
154
+ *
155
+ * @param {object} torrent
156
+ * @param {number} fileIndex
157
+ * @param {number} [headBytes]
158
+ * @param {number} [tailBytes]
159
+ * @param {number} [timeoutMs]
160
+ * @returns {Promise<unknown>}
161
+ */
162
+ async prefetchFileEdges(torrent, fileIndex, headBytes, tailBytes, timeoutMs) {
163
+ const sourceKey = torrent?.sourceKey;
164
+ if (!sourceKey) {
165
+ return null;
166
+ }
167
+ return this.#client.prefetchFileEdges({ sourceKey, fileIndex, headBytes, tailBytes, timeoutMs });
168
+ }
169
+
170
+ /**
171
+ * Shut the torrent client down and stop the thread.
172
+ *
173
+ * @returns {Promise<void>}
174
+ */
175
+ async destroyAll() {
176
+ this.#torrents.clear();
177
+ await this.#client.destroyAll();
178
+ }
179
+ }