@torrent-tv/proxy 2.9.72 → 2.9.74

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,222 +1,238 @@
1
- /**
2
- * @file The transport both sides of the torrent worker share.
3
- *
4
- * One place holds the request/reply bookkeeping and the streaming rules, so the
5
- * worker and its main-thread client cannot drift apart on the details that
6
- * matter: which side transfers, which side acknowledges, and when a stream is
7
- * allowed to keep going.
8
- *
9
- * See `protocol.js` for why the design is what it is — every number in it came
10
- * from a measurement, not a preference.
11
- */
12
-
13
- import { Event, STREAM_HIGH_WATER_CHUNKS } from "./protocol.js";
14
-
15
- /**
16
- * Issue request ids that stay unique for the life of a thread.
17
- *
18
- * @returns {() => number}
19
- */
20
- export function createRequestIds() {
21
- let next = 0;
22
- return () => (next += 1);
23
- }
24
-
25
- /**
26
- * Wrap a port's messages into request/reply calls.
27
- *
28
- * Callers get promises; the plumbing of matching replies to requests, and of
29
- * turning a worker-side failure back into a rejection, lives here. `Error`
30
- * objects do not survive a thread boundary, so failures cross as messages and
31
- * are rebuilt into `Error`s on arrival — a caller sees an ordinary rejection.
32
- *
33
- * @param {import("node:worker_threads").MessagePort | import("node:worker_threads").Worker} port
34
- * @returns {{
35
- * call: (command: string, params?: object) => Promise<unknown>,
36
- * handleReply: (message: object) => boolean,
37
- * rejectAll: (reason: Error) => void
38
- * }}
39
- */
40
- export function createCaller(port) {
41
- const nextId = createRequestIds();
42
- const pending = new Map();
43
-
44
- return {
45
- call(command, params = {}) {
46
- return new Promise((resolve, reject) => {
47
- const id = nextId();
48
- pending.set(id, { resolve, reject });
49
- port.postMessage({ command, id, params });
50
- });
51
- },
52
-
53
- /**
54
- * Feed a message in; returns true when it was a reply this caller owned.
55
- */
56
- handleReply(message) {
57
- const entry = pending.get(message?.id);
58
- if (!entry) {
59
- return false;
60
- }
61
- if (message.type === Event.RESULT) {
62
- pending.delete(message.id);
63
- entry.resolve(message.result);
64
- return true;
65
- }
66
- if (message.type === Event.ERROR) {
67
- pending.delete(message.id);
68
- entry.reject(new Error(message.error ?? "Torrent worker request failed."));
69
- return true;
70
- }
71
- return false;
72
- },
73
-
74
- /**
75
- * Fail everything outstanding — the worker died, or is being shut down.
76
- */
77
- rejectAll(reason) {
78
- for (const [, entry] of pending) {
79
- entry.reject(reason);
80
- }
81
- pending.clear();
82
- }
83
- };
84
- }
85
-
86
- /**
87
- * Receive a chunked body as an ordinary `ReadableStream`.
88
- *
89
- * This is the half that makes the design pay off: chunks arrive as transferred
90
- * buffers (no copying), and are handed on through a standard stream, so callers
91
- * treat it exactly like any other body. Each chunk is acknowledged as it is
92
- * enqueued, which is what lets the worker keep only
93
- * {@link STREAM_HIGH_WATER_CHUNKS} in flight.
94
- *
95
- * Cancelling the stream — a viewer navigating away, a superseded seek — sends
96
- * the cancel command, so the worker stops reading rather than filling a queue
97
- * nobody will drain.
98
- *
99
- * @param {object} params
100
- * @param {import("node:worker_threads").Worker} params.port
101
- * @param {number} params.requestId
102
- * @param {() => void} params.onCancel - Sends CANCEL_READ for this request.
103
- * @returns {{ stream: ReadableStream<Uint8Array>, push: (bytes: Uint8Array) => void, close: () => void, fail: (error: Error) => void }}
104
- */
105
- export function createReceiveStream({ port, requestId, onCancel }) {
106
- let controller = null;
107
- let finished = false;
108
-
109
- const stream = new ReadableStream({
110
- start(streamController) {
111
- controller = streamController;
112
- },
113
- cancel() {
114
- if (!finished) {
115
- finished = true;
116
- onCancel();
117
- }
118
- }
119
- });
120
-
121
- return {
122
- stream,
123
-
124
- push(bytes) {
125
- if (finished || !controller) {
126
- return;
127
- }
128
- controller.enqueue(bytes);
129
- // Acknowledge only once the data is in the stream's own queue, so the
130
- // worker's in-flight count reflects what has actually been taken up.
131
- port.postMessage({ type: Event.CHUNK_ACK, id: requestId });
132
- },
133
-
134
- close() {
135
- if (finished || !controller) {
136
- return;
137
- }
138
- finished = true;
139
- controller.close();
140
- },
141
-
142
- fail(error) {
143
- if (finished || !controller) {
144
- return;
145
- }
146
- finished = true;
147
- controller.error(error);
148
- }
149
- };
150
- }
151
-
152
- /**
153
- * Send a body as chunks, pausing when too many are unacknowledged.
154
- *
155
- * The worker side of the same arrangement. `waitForCapacity` resolves when the
156
- * main thread has taken up enough of what was sent; without it a fast disk
157
- * would outrun the channel and rebuild in the message queue exactly the memory
158
- * the transfers were saving.
159
- *
160
- * @param {object} params
161
- * @param {import("node:worker_threads").MessagePort} params.port
162
- * @param {number} params.requestId
163
- * @returns {{ send: (bytes: Buffer) => Promise<void>, end: () => void, ack: () => void, cancel: () => void, isCancelled: () => boolean }}
164
- */
165
- export function createSendStream({ port, requestId }) {
166
- let inFlight = 0;
167
- let cancelled = false;
168
- let wake = null;
169
-
170
- const waitForCapacity = () => {
171
- if (cancelled || inFlight < STREAM_HIGH_WATER_CHUNKS) {
172
- return Promise.resolve();
173
- }
174
- return new Promise((resolve) => {
175
- wake = resolve;
176
- });
177
- };
178
-
179
- return {
180
- async send(bytes) {
181
- await waitForCapacity();
182
- if (cancelled) {
183
- return;
184
- }
185
- inFlight += 1;
186
- // Transfer the underlying memory rather than copying it the whole point
187
- // of the design, and the difference between 4.8 ms and 37 ms per 10 MB.
188
- port.postMessage(
189
- { type: Event.CHUNK, id: requestId, bytes },
190
- [bytes.buffer]
191
- );
192
- },
193
-
194
- end() {
195
- if (!cancelled) {
196
- port.postMessage({ type: Event.READ_END, id: requestId });
197
- }
198
- },
199
-
200
- ack() {
201
- inFlight = Math.max(0, inFlight - 1);
202
- if (wake) {
203
- const resume = wake;
204
- wake = null;
205
- resume();
206
- }
207
- },
208
-
209
- cancel() {
210
- cancelled = true;
211
- if (wake) {
212
- const resume = wake;
213
- wake = null;
214
- resume();
215
- }
216
- },
217
-
218
- isCancelled() {
219
- return cancelled;
220
- }
221
- };
222
- }
1
+ /**
2
+ * @file The transport both sides of the torrent worker share.
3
+ *
4
+ * One place holds the request/reply bookkeeping and the streaming rules, so the
5
+ * worker and its main-thread client cannot drift apart on the details that
6
+ * matter: which side transfers, which side acknowledges, and when a stream is
7
+ * allowed to keep going.
8
+ *
9
+ * See `protocol.js` for why the design is what it is — every number in it came
10
+ * from a measurement, not a preference.
11
+ */
12
+
13
+ import { Event, STREAM_HIGH_WATER_CHUNKS } from "./protocol.js";
14
+
15
+ /**
16
+ * Issue request ids that stay unique for the life of a thread.
17
+ *
18
+ * @returns {() => number}
19
+ */
20
+ export function createRequestIds() {
21
+ let next = 0;
22
+ return () => (next += 1);
23
+ }
24
+
25
+ /**
26
+ * Wrap a port's messages into request/reply calls.
27
+ *
28
+ * Callers get promises; the plumbing of matching replies to requests, and of
29
+ * turning a worker-side failure back into a rejection, lives here. `Error`
30
+ * objects do not survive a thread boundary, so failures cross as messages and
31
+ * are rebuilt into `Error`s on arrival — a caller sees an ordinary rejection.
32
+ *
33
+ * @param {import("node:worker_threads").MessagePort | import("node:worker_threads").Worker} port
34
+ * @returns {{
35
+ * call: (command: string, params?: object) => Promise<unknown>,
36
+ * handleReply: (message: object) => boolean,
37
+ * rejectAll: (reason: Error) => void
38
+ * }}
39
+ */
40
+ export function createCaller(port) {
41
+ const nextId = createRequestIds();
42
+ const pending = new Map();
43
+
44
+ return {
45
+ call(command, params = {}) {
46
+ return new Promise((resolve, reject) => {
47
+ const id = nextId();
48
+ pending.set(id, { resolve, reject });
49
+ port.postMessage({ command, id, params });
50
+ });
51
+ },
52
+
53
+ /**
54
+ * Feed a message in; returns true when it was a reply this caller owned.
55
+ */
56
+ handleReply(message) {
57
+ const entry = pending.get(message?.id);
58
+ if (!entry) {
59
+ return false;
60
+ }
61
+ if (message.type === Event.RESULT) {
62
+ pending.delete(message.id);
63
+ entry.resolve(message.result);
64
+ return true;
65
+ }
66
+ if (message.type === Event.ERROR) {
67
+ pending.delete(message.id);
68
+ entry.reject(new Error(message.error ?? "Torrent worker request failed."));
69
+ return true;
70
+ }
71
+ return false;
72
+ },
73
+
74
+ /**
75
+ * Fail everything outstanding — the worker died, or is being shut down.
76
+ */
77
+ rejectAll(reason) {
78
+ for (const [, entry] of pending) {
79
+ entry.reject(reason);
80
+ }
81
+ pending.clear();
82
+ }
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Receive a chunked body as an ordinary `ReadableStream`.
88
+ *
89
+ * This is the half that makes the design pay off: chunks arrive as transferred
90
+ * buffers (no copying), and are handed on through a standard stream, so callers
91
+ * treat it exactly like any other body. Each chunk is acknowledged as it is
92
+ * enqueued, which is what lets the worker keep only
93
+ * {@link STREAM_HIGH_WATER_CHUNKS} in flight.
94
+ *
95
+ * Cancelling the stream — a viewer navigating away, a superseded seek — sends
96
+ * the cancel command, so the worker stops reading rather than filling a queue
97
+ * nobody will drain.
98
+ *
99
+ * @param {object} params
100
+ * @param {import("node:worker_threads").Worker} params.port
101
+ * @param {number} params.requestId
102
+ * @param {() => void} params.onCancel - Sends CANCEL_READ for this request.
103
+ * @returns {{ stream: ReadableStream<Uint8Array>, push: (bytes: Uint8Array) => void, close: () => void, fail: (error: Error) => void }}
104
+ */
105
+ export function createReceiveStream({ port, requestId, onCancel }) {
106
+ let controller = null;
107
+ let finished = false;
108
+
109
+ const stream = new ReadableStream({
110
+ start(streamController) {
111
+ controller = streamController;
112
+ },
113
+ cancel() {
114
+ if (!finished) {
115
+ finished = true;
116
+ onCancel();
117
+ }
118
+ }
119
+ });
120
+
121
+ return {
122
+ stream,
123
+
124
+ push(bytes) {
125
+ if (finished || !controller) {
126
+ return;
127
+ }
128
+ controller.enqueue(bytes);
129
+ // Acknowledge only once the data is in the stream's own queue, so the
130
+ // worker's in-flight count reflects what has actually been taken up.
131
+ port.postMessage({ type: Event.CHUNK_ACK, id: requestId });
132
+ },
133
+
134
+ close() {
135
+ if (finished || !controller) {
136
+ return;
137
+ }
138
+ finished = true;
139
+ controller.close();
140
+ },
141
+
142
+ fail(error) {
143
+ if (finished || !controller) {
144
+ return;
145
+ }
146
+ finished = true;
147
+ controller.error(error);
148
+ }
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Send a body as chunks, pausing when too many are unacknowledged.
154
+ *
155
+ * The worker side of the same arrangement. `waitForCapacity` resolves when the
156
+ * main thread has taken up enough of what was sent; without it a fast disk
157
+ * would outrun the channel and rebuild in the message queue exactly the memory
158
+ * the transfers were saving.
159
+ *
160
+ * @param {object} params
161
+ * @param {import("node:worker_threads").MessagePort} params.port
162
+ * @param {number} params.requestId
163
+ * @returns {{ send: (bytes: Buffer) => Promise<void>, end: () => void, ack: () => void, cancel: () => void, isCancelled: () => boolean }}
164
+ */
165
+ export function createSendStream({ port, requestId }) {
166
+ let inFlight = 0;
167
+ let cancelled = false;
168
+ let wake = null;
169
+
170
+ const waitForCapacity = () => {
171
+ if (cancelled || inFlight < STREAM_HIGH_WATER_CHUNKS) {
172
+ return Promise.resolve();
173
+ }
174
+ return new Promise((resolve) => {
175
+ wake = resolve;
176
+ });
177
+ };
178
+
179
+ return {
180
+ async send(bytes) {
181
+ await waitForCapacity();
182
+ if (cancelled) {
183
+ return;
184
+ }
185
+ inFlight += 1;
186
+ // Copy into memory this transport allocated, then transfer THAT.
187
+ //
188
+ // Transferring the caller's buffer is faster and was what shipped, but it
189
+ // is only correct if the caller owns the memory outright — and no test at
190
+ // this boundary can establish that. 2.9.73 tried to decide it by
191
+ // inspection (`byteOffset === 0 && byteLength === buffer.byteLength`),
192
+ // which answers "does this view cover its region", not "did we allocate
193
+ // it". WebTorrent's piece cache returns a buffer covering its whole
194
+ // region and keeps using it, so the check passed and the transfer
195
+ // detached the cache: every later read failed with a detached
196
+ // ArrayBuffer, and because the error never reached the reader it looked
197
+ // like an empty file (`Stream ends prematurely at 0`).
198
+ //
199
+ // The copy costs 3.64 ms per 8 MB on the field host, against 37 ms for a
200
+ // structured clone. It disappears entirely for pieces read through the
201
+ // shared piece store, which the main thread reads by offset without any
202
+ // hand-over at all.
203
+ const payload = new Uint8Array(bytes);
204
+ port.postMessage(
205
+ { type: Event.CHUNK, id: requestId, bytes: payload },
206
+ [payload.buffer]
207
+ );
208
+ },
209
+
210
+ end() {
211
+ if (!cancelled) {
212
+ port.postMessage({ type: Event.READ_END, id: requestId });
213
+ }
214
+ },
215
+
216
+ ack() {
217
+ inFlight = Math.max(0, inFlight - 1);
218
+ if (wake) {
219
+ const resume = wake;
220
+ wake = null;
221
+ resume();
222
+ }
223
+ },
224
+
225
+ cancel() {
226
+ cancelled = true;
227
+ if (wake) {
228
+ const resume = wake;
229
+ wake = null;
230
+ resume();
231
+ }
232
+ },
233
+
234
+ isCancelled() {
235
+ return cancelled;
236
+ }
237
+ };
238
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file Point `webrtc-polyfill` at the JavaScript WebRTC stack, in this thread only.
3
+ *
4
+ * Imported for its side effect, and imported FIRST by `worker.js` — ES module
5
+ * bodies run in import order, so registering the hook here happens before
6
+ * `torrent-pool.js` pulls in WebTorrent, which is what reaches
7
+ * `@thaunknown/simple-peer` and, through it, `webrtc-polyfill`.
8
+ *
9
+ * Scope is deliberately narrow. The hook lives in the worker's isolate, so the
10
+ * main thread keeps using node-datachannel directly for the browser's video
11
+ * channel — see `webrtc-shim.js` for why the two cannot share one process
12
+ * isolate at all.
13
+ */
14
+
15
+ import { registerHooks } from "node:module";
16
+
17
+ const SHIM_URL = new URL("./webrtc-shim.js", import.meta.url).href;
18
+
19
+ registerHooks({
20
+ /**
21
+ * @param {string} specifier
22
+ * @param {object} context
23
+ * @param {(specifier: string, context: object) => { url: string }} nextResolve
24
+ * @returns {{ url: string, shortCircuit?: boolean }}
25
+ */
26
+ resolve(specifier, context, nextResolve) {
27
+ if (specifier === "webrtc-polyfill") {
28
+ return { url: SHIM_URL, shortCircuit: true };
29
+ }
30
+ return nextResolve(specifier, context);
31
+ }
32
+ });