@fastls/fastls 0.0.0-stage → 0.10.1-dev.0
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/LICENSE +203 -0
- package/README.md +385 -2
- package/dist/cookies.d.ts +23 -0
- package/dist/cookies.js +27 -0
- package/dist/duplex.d.ts +21 -0
- package/dist/duplex.js +94 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +48 -0
- package/dist/headers.d.ts +12 -0
- package/dist/headers.js +28 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +2 -0
- package/dist/library-path.d.ts +2 -0
- package/dist/library-path.js +77 -0
- package/dist/native.d.ts +54 -0
- package/dist/native.js +208 -0
- package/dist/notifications.d.ts +40 -0
- package/dist/notifications.js +222 -0
- package/dist/request-options.d.ts +28 -0
- package/dist/request-options.js +172 -0
- package/dist/response.d.ts +84 -0
- package/dist/response.js +242 -0
- package/dist/scheduler.d.ts +12 -0
- package/dist/scheduler.js +75 -0
- package/dist/session.d.ts +101 -0
- package/dist/session.js +390 -0
- package/dist/sse.d.ts +52 -0
- package/dist/sse.js +197 -0
- package/dist/text.d.ts +3 -0
- package/dist/text.js +55 -0
- package/dist/timeout.d.ts +9 -0
- package/dist/timeout.js +19 -0
- package/dist/upload.d.ts +11 -0
- package/dist/upload.js +141 -0
- package/dist/websocket.d.ts +63 -0
- package/dist/websocket.js +248 -0
- package/package.json +41 -4
package/dist/session.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Session = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const cookies_js_1 = require("./cookies.js");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
const native_js_1 = require("./native.js");
|
|
8
|
+
const response_js_1 = require("./response.js");
|
|
9
|
+
const timeout_js_1 = require("./timeout.js");
|
|
10
|
+
const upload_js_1 = require("./upload.js");
|
|
11
|
+
const duplex_js_1 = require("./duplex.js");
|
|
12
|
+
const sse_js_1 = require("./sse.js");
|
|
13
|
+
const notifications_js_1 = require("./notifications.js");
|
|
14
|
+
const request_options_js_1 = require("./request-options.js");
|
|
15
|
+
const websocket_js_1 = require("./websocket.js");
|
|
16
|
+
function decoding(decode, maximum) {
|
|
17
|
+
if (typeof decode !== "boolean")
|
|
18
|
+
throw new TypeError("decodeContent must be a boolean");
|
|
19
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 2 ** 40)
|
|
20
|
+
throw new RangeError("maxDecodedBytes must be 1..1099511627776");
|
|
21
|
+
}
|
|
22
|
+
function proxyValue(value) {
|
|
23
|
+
if (value !== undefined && value !== null && typeof value !== "string")
|
|
24
|
+
throw new TypeError("proxy must be a URL string or null");
|
|
25
|
+
// The native validator owns URL parsing and redacted errors. Do not include
|
|
26
|
+
// credentials in JavaScript URL parser errors or normalize invalid input.
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function proxyTrust(path) {
|
|
30
|
+
if (path === null || path === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof path !== "string")
|
|
33
|
+
throw new TypeError("proxyCaFile must be a path string or null");
|
|
34
|
+
const pem = (0, node_fs_1.readFileSync)(path, "utf8");
|
|
35
|
+
if (!pem.trim())
|
|
36
|
+
throw new TypeError("proxyCaFile is empty");
|
|
37
|
+
return pem;
|
|
38
|
+
}
|
|
39
|
+
function mergeHeaders(...sources) {
|
|
40
|
+
const merged = Object.create(null);
|
|
41
|
+
for (const source of sources) {
|
|
42
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
throw new TypeError("header values must be strings");
|
|
45
|
+
merged[key.toLowerCase()] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
class Session {
|
|
51
|
+
/** Construct and initialize; failed initialization closes the new session. */
|
|
52
|
+
static async open(options = {}) {
|
|
53
|
+
const session = new Session(options);
|
|
54
|
+
try {
|
|
55
|
+
await session.ready();
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await session.closeAsync();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
notificationMode;
|
|
64
|
+
cookies;
|
|
65
|
+
native;
|
|
66
|
+
handle;
|
|
67
|
+
profile;
|
|
68
|
+
proxy;
|
|
69
|
+
proxyCaPem;
|
|
70
|
+
headers;
|
|
71
|
+
timeouts;
|
|
72
|
+
decodeContent;
|
|
73
|
+
maxDecodedBytes;
|
|
74
|
+
caPem;
|
|
75
|
+
active = new Set();
|
|
76
|
+
websockets = new Set();
|
|
77
|
+
producers = new Set();
|
|
78
|
+
closed = false;
|
|
79
|
+
notifier;
|
|
80
|
+
setup;
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
this.proxy = proxyValue(options.proxy);
|
|
83
|
+
this.proxyCaPem = proxyTrust(options.proxyCaFile);
|
|
84
|
+
const notifications = options.notifications === undefined ? "auto" : options.notifications;
|
|
85
|
+
if (!["auto", "required", "poll"].includes(notifications))
|
|
86
|
+
throw new TypeError("notifications must be auto, required or poll");
|
|
87
|
+
this.profile = options.profile ?? "chrome/156/windows";
|
|
88
|
+
this.timeouts = (0, timeout_js_1.timeoutFields)(options.timeoutMs ?? 30000);
|
|
89
|
+
this.decodeContent = options.decodeContent ?? true;
|
|
90
|
+
this.maxDecodedBytes = options.maxDecodedBytes ?? 256 * 1024 * 1024;
|
|
91
|
+
decoding(this.decodeContent, this.maxDecodedBytes);
|
|
92
|
+
this.headers = mergeHeaders(options.headers);
|
|
93
|
+
if (options.cookies !== undefined && typeof options.cookies !== "boolean")
|
|
94
|
+
throw new TypeError("cookies must be a boolean");
|
|
95
|
+
this.caPem = options.caFile === undefined ? undefined : (0, node_fs_1.readFileSync)(options.caFile, "utf8");
|
|
96
|
+
if (this.caPem !== undefined && !this.caPem.trim())
|
|
97
|
+
throw new TypeError("caFile is empty");
|
|
98
|
+
this.native = native_js_1.Native.load(options.library);
|
|
99
|
+
const supported = this.native.notificationsAvailable && this.native.call("features").includes("session_notifications");
|
|
100
|
+
if (notifications === "required" && !supported)
|
|
101
|
+
throw new errors_js_1.FastlsError("notifications require a newer Fastls native library");
|
|
102
|
+
this.notificationMode = notifications !== "poll" && supported ? "async-events" : "poll";
|
|
103
|
+
this.handle = (0, native_js_1.asHandle)(this.native.call("session_create"));
|
|
104
|
+
this.cookies = new cookies_js_1.Cookies(input => {
|
|
105
|
+
this.ensureOpen();
|
|
106
|
+
const raw = (0, native_js_1.encodeNativeJSON)(input, 16384);
|
|
107
|
+
return this.native.call("session_cookies", this.handle, raw, raw.length);
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
if (options.cookies === false) {
|
|
111
|
+
const raw = (0, native_js_1.encodeNativeJSON)({ action: "configure", enabled: false });
|
|
112
|
+
this.native.call("session_cookies", this.handle, raw, raw.length);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
this.close();
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
ensureOpen() {
|
|
121
|
+
if (this.closed)
|
|
122
|
+
throw new errors_js_1.FastlsError("session is closed");
|
|
123
|
+
}
|
|
124
|
+
/** Initialize the shared wakeup channel before any HTTP request is allocated. */
|
|
125
|
+
async ready(signal) {
|
|
126
|
+
this.ensureOpen();
|
|
127
|
+
if (signal !== undefined && !(signal instanceof AbortSignal))
|
|
128
|
+
throw new TypeError("signal must be an AbortSignal");
|
|
129
|
+
if (signal?.aborted)
|
|
130
|
+
throw new errors_js_1.AbortError(undefined, { cause: signal.reason });
|
|
131
|
+
if (this.notificationMode === "poll")
|
|
132
|
+
return;
|
|
133
|
+
if (!this.notifier) {
|
|
134
|
+
const notifier = this.notifier = new notifications_js_1.Notifier(this.native, this.handle);
|
|
135
|
+
const setup = this.setup = notifier.start().catch(async (error) => {
|
|
136
|
+
await notifier.closeAsync();
|
|
137
|
+
if (this.notifier === notifier) {
|
|
138
|
+
this.notifier = undefined;
|
|
139
|
+
this.setup = undefined;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
});
|
|
143
|
+
void setup.catch(() => { });
|
|
144
|
+
}
|
|
145
|
+
const notifier = this.notifier;
|
|
146
|
+
notifier.setupWaiters++;
|
|
147
|
+
try {
|
|
148
|
+
if (signal)
|
|
149
|
+
await (0, upload_js_1.abortable)(this.setup, signal);
|
|
150
|
+
else
|
|
151
|
+
await this.setup;
|
|
152
|
+
this.ensureOpen();
|
|
153
|
+
notifier.check();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
throw new errors_js_1.AbortError(undefined, { cause: signal.reason });
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
if (--notifier.setupWaiters === 0 && !notifier.isReady && this.notifier === notifier) {
|
|
162
|
+
notifier.close();
|
|
163
|
+
this.notifier = undefined;
|
|
164
|
+
this.setup = undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
begin(method, url, options, manual = false) {
|
|
169
|
+
this.ensureOpen();
|
|
170
|
+
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
|
171
|
+
throw new TypeError("signal must be an AbortSignal");
|
|
172
|
+
}
|
|
173
|
+
if (options.signal?.aborted)
|
|
174
|
+
throw new errors_js_1.AbortError(undefined, { cause: options.signal.reason });
|
|
175
|
+
const headers = mergeHeaders(this.headers, options.headers);
|
|
176
|
+
for (const name of ["proxy-authorization", "proxy-connection"]) {
|
|
177
|
+
if (name in headers)
|
|
178
|
+
throw new errors_js_1.FastlsError("proxy headers must not be sent as target headers; configure proxy URL credentials");
|
|
179
|
+
}
|
|
180
|
+
const proxy = proxyValue(options.proxy === undefined ? this.proxy : options.proxy);
|
|
181
|
+
if (proxy && !this.native.call("features").includes("proxy_tunnels")) {
|
|
182
|
+
throw new errors_js_1.FastlsError("proxy support requires a newer Fastls native library");
|
|
183
|
+
}
|
|
184
|
+
const httpsProxy = proxy?.toLowerCase().startsWith("https://") ?? false;
|
|
185
|
+
if (httpsProxy && !this.native.call("features").includes("https_proxy_tls")) {
|
|
186
|
+
throw new errors_js_1.FastlsError("HTTPS proxy support requires a newer Fastls native library");
|
|
187
|
+
}
|
|
188
|
+
const proxyCaPem = !httpsProxy ? undefined : options.proxyCaFile === undefined
|
|
189
|
+
? this.proxyCaPem : proxyTrust(options.proxyCaFile);
|
|
190
|
+
if (options.duplex !== undefined && typeof options.duplex !== "boolean")
|
|
191
|
+
throw new TypeError("duplex must be a boolean");
|
|
192
|
+
const duplex = options.duplex ?? false;
|
|
193
|
+
if (duplex && !this.native.call("features").includes("duplex_h2")) {
|
|
194
|
+
throw new errors_js_1.FastlsError("HTTP/2 duplex requires a newer Fastls native library");
|
|
195
|
+
}
|
|
196
|
+
const prepared = (0, request_options_js_1.prepare)(url, options, headers);
|
|
197
|
+
url = prepared.url;
|
|
198
|
+
const body = prepared.body === undefined && duplex && !manual ? Buffer.alloc(0) : prepared.body;
|
|
199
|
+
const streaming = (0, upload_js_1.isStreamingBody)(body) || duplex;
|
|
200
|
+
const bodyBytes = streaming || body === undefined ? undefined : Buffer.from(body);
|
|
201
|
+
const timeouts = options.timeoutMs === undefined ? this.timeouts : (0, timeout_js_1.timeoutFields)(options.timeoutMs);
|
|
202
|
+
const decodeContent = options.decodeContent ?? this.decodeContent;
|
|
203
|
+
const maxDecodedBytes = options.maxDecodedBytes ?? this.maxDecodedBytes;
|
|
204
|
+
decoding(decodeContent, maxDecodedBytes);
|
|
205
|
+
if (options.allowRedirects !== undefined && typeof options.allowRedirects !== "boolean") {
|
|
206
|
+
throw new TypeError("allowRedirects must be a boolean");
|
|
207
|
+
}
|
|
208
|
+
const started = performance.now();
|
|
209
|
+
let trailers;
|
|
210
|
+
if (options.trailers !== undefined) {
|
|
211
|
+
if (!this.native.call("features").includes("request_trailers"))
|
|
212
|
+
throw new errors_js_1.FastlsError("request trailers require a newer Fastls native library");
|
|
213
|
+
if (!options.trailers || typeof options.trailers !== "object" || Array.isArray(options.trailers))
|
|
214
|
+
throw new TypeError("trailers must be a mapping");
|
|
215
|
+
trailers = {};
|
|
216
|
+
for (const [name, value] of Object.entries(options.trailers)) {
|
|
217
|
+
const values = typeof value === "string" ? [value] : value;
|
|
218
|
+
if (!Array.isArray(values) || values.some(item => typeof item !== "string"))
|
|
219
|
+
throw new TypeError("trailer values must be strings");
|
|
220
|
+
trailers[name] = [...values];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const handle = this.native.start({
|
|
224
|
+
session: this.handle, url, method, headers, trailers, proxy: proxy || undefined, proxy_ca_pem: proxyCaPem,
|
|
225
|
+
profile: options.profile ?? this.profile,
|
|
226
|
+
...timeouts, decode_content: decodeContent, max_decoded_bytes: maxDecodedBytes,
|
|
227
|
+
disable_redirect: duplex || options.allowRedirects === false,
|
|
228
|
+
ca_pem: this.caPem,
|
|
229
|
+
body_base64: bodyBytes?.toString("base64"),
|
|
230
|
+
stream_body: streaming || undefined,
|
|
231
|
+
duplex: duplex || undefined,
|
|
232
|
+
});
|
|
233
|
+
const transfer = new response_js_1.Transfer(this.native, handle, () => { this.active.delete(transfer); this.notifier?.forget(handle); }, options.signal, duplex, this.notifier);
|
|
234
|
+
this.active.add(transfer);
|
|
235
|
+
try {
|
|
236
|
+
this.notifier?.track(handle);
|
|
237
|
+
transfer.attachSignal(timeouts.timeout_ms, started);
|
|
238
|
+
return { transfer, body, streaming };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
transfer.close();
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async stream(method, url, options = {}) {
|
|
246
|
+
await this.ready(options.signal);
|
|
247
|
+
const { transfer, body, streaming } = this.begin(method, url, options);
|
|
248
|
+
try {
|
|
249
|
+
const upload = streaming ? (0, upload_js_1.startUpload)(transfer, body) : undefined;
|
|
250
|
+
if (upload) {
|
|
251
|
+
this.producers.add(upload);
|
|
252
|
+
void upload.cleanup.then(() => this.producers.delete(upload), () => this.producers.delete(upload));
|
|
253
|
+
}
|
|
254
|
+
let info;
|
|
255
|
+
try {
|
|
256
|
+
info = await transfer.headers();
|
|
257
|
+
if (!transfer.duplex && upload && info.status < 300 && !info.upload_discarded && !upload.complete()) {
|
|
258
|
+
throw new errors_js_1.FastlsError("server responded before upload finished; duplex streaming is not supported");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
if (upload && (!transfer.duplex || !info || info.status >= 300)) {
|
|
263
|
+
upload.stop();
|
|
264
|
+
await upload.done;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
transfer.ensureOpen();
|
|
268
|
+
return new response_js_1.StreamResponse(transfer, info, transfer.duplex ? upload : undefined);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
transfer.close();
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async sse(method, url, options = {}) {
|
|
276
|
+
const { maxEventBytes = 1024 * 1024, lastEventId, ...request } = options;
|
|
277
|
+
(0, sse_js_1.validateSSEOptions)(maxEventBytes, lastEventId);
|
|
278
|
+
const headers = mergeHeaders({ accept: "text/event-stream", "cache-control": "no-cache" }, request.headers);
|
|
279
|
+
if (lastEventId !== undefined)
|
|
280
|
+
headers["last-event-id"] = lastEventId;
|
|
281
|
+
const response = await this.stream(method, url, { ...request, headers });
|
|
282
|
+
try {
|
|
283
|
+
return new sse_js_1.SSEStream(response, maxEventBytes, lastEventId);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await response.closeAsync();
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async request(method, url, options = {}) {
|
|
291
|
+
const stream = await this.stream(method, url, options);
|
|
292
|
+
try {
|
|
293
|
+
const content = await stream.read();
|
|
294
|
+
return new response_js_1.Response(stream.status, stream.headers, stream.match, content, stream.wireHeaders, stream.decoded, stream.httpVersion, stream.url, stream.trailers);
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
await stream.closeAsync();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Open a manual exchange; send() and response reads can run concurrently. */
|
|
301
|
+
duplex(method, url, options = {}) {
|
|
302
|
+
if (["body", "duplex", "json", "form", "files"].some(key => key in options))
|
|
303
|
+
throw new TypeError("manual duplex uses send() and finishUpload(), not body, json, form, files or duplex options");
|
|
304
|
+
this.ensureOpen();
|
|
305
|
+
const cancellation = new AbortController();
|
|
306
|
+
const signal = options.signal === undefined ? cancellation.signal : AbortSignal.any([options.signal, cancellation.signal]);
|
|
307
|
+
const transfer = this.ready(signal).then(() => {
|
|
308
|
+
if (cancellation.signal.aborted)
|
|
309
|
+
throw new errors_js_1.FastlsError("exchange is closed");
|
|
310
|
+
return this.begin(method, url, { ...options, signal, duplex: true }, true).transfer;
|
|
311
|
+
}, error => { if (cancellation.signal.aborted)
|
|
312
|
+
throw new errors_js_1.FastlsError("exchange is closed"); throw error; });
|
|
313
|
+
return new duplex_js_1.DuplexExchange(transfer, () => cancellation.abort());
|
|
314
|
+
}
|
|
315
|
+
get(url, options) { return this.request("GET", url, options); }
|
|
316
|
+
post(url, options) { return this.request("POST", url, options); }
|
|
317
|
+
async websocket(url, options = {}) {
|
|
318
|
+
await this.ready(options.signal);
|
|
319
|
+
this.ensureOpen();
|
|
320
|
+
if (options.signal?.aborted)
|
|
321
|
+
throw new errors_js_1.AbortError(undefined, { cause: options.signal.reason });
|
|
322
|
+
if (!this.native.websocketAvailable)
|
|
323
|
+
throw new errors_js_1.FastlsError("WebSocket requires a newer Fastls native library");
|
|
324
|
+
const cookieMode = options.cookieMode === undefined ? "session" : options.cookieMode;
|
|
325
|
+
if (cookieMode !== "session" && cookieMode !== "manual")
|
|
326
|
+
throw new TypeError("cookieMode must be session or manual");
|
|
327
|
+
const features = this.native.call("features");
|
|
328
|
+
const sessionCookies = features.includes("websocket_cookie_control_v1") || features.includes("websocket_cookies");
|
|
329
|
+
if (cookieMode === "manual" && !sessionCookies && features.includes("websocket_cookies_v1"))
|
|
330
|
+
throw new errors_js_1.FastlsError("This native library cannot disable automatic WebSocket cookies; a newer library with websocket_cookie_control_v1 is required");
|
|
331
|
+
if (cookieMode === "session" && !sessionCookies)
|
|
332
|
+
throw new errors_js_1.FastlsError("WebSocket session cookies require a newer Fastls native library; use cookieMode manual for legacy behavior");
|
|
333
|
+
const maximum = (options.maxMessageBytes === undefined ? 1048576 : options.maxMessageBytes);
|
|
334
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 16777216)
|
|
335
|
+
throw new RangeError("maxMessageBytes must be 1..16777216");
|
|
336
|
+
const close = (0, websocket_js_1.wsBudget)((options.closeTimeoutMs === undefined ? 1000 : options.closeTimeoutMs), "closeTimeoutMs", 60000);
|
|
337
|
+
const headers = mergeHeaders(this.headers, options.headers), prepared = (0, request_options_js_1.prepare)(url, { params: options.params }, headers);
|
|
338
|
+
const proxy = proxyValue(options.proxy === undefined ? this.proxy : options.proxy);
|
|
339
|
+
const proxyCA = proxy?.toLowerCase().startsWith("https://") ? (options.proxyCaFile === undefined ? this.proxyCaPem : proxyTrust(options.proxyCaFile)) : undefined;
|
|
340
|
+
if (options.subprotocols !== undefined && (!Array.isArray(options.subprotocols) || options.subprotocols.some(p => typeof p !== "string")))
|
|
341
|
+
throw new TypeError("subprotocols must be an array of strings");
|
|
342
|
+
const handle = this.native.wsStart({ url: prepared.url, session: this.handle, disable_cookies: sessionCookies && cookieMode === "manual" ? true : undefined, headers, profile: options.profile ?? this.profile, proxy: proxy || undefined, ca_pem: this.caPem, proxy_ca_pem: proxyCA, subprotocols: options.subprotocols ?? [], max_message_bytes: maximum,
|
|
343
|
+
timeout_ms: (0, websocket_js_1.wsBudget)((options.handshakeTimeoutMs === undefined ? 30000 : options.handshakeTimeoutMs), "handshakeTimeoutMs"), read_timeout_ms: options.readTimeoutMs === null || options.readTimeoutMs === undefined ? 0 : (0, websocket_js_1.wsBudget)(options.readTimeoutMs, "readTimeoutMs"), write_timeout_ms: (0, websocket_js_1.wsBudget)((options.writeTimeoutMs === undefined ? 30000 : options.writeTimeoutMs), "writeTimeoutMs") });
|
|
344
|
+
const ws = new websocket_js_1.WebSocket(this.native, handle, () => { this.websockets.delete(ws); this.notifier?.forget(handle); }, maximum, close, this.notifier, options.signal);
|
|
345
|
+
this.websockets.add(ws);
|
|
346
|
+
try {
|
|
347
|
+
this.notifier?.track(handle);
|
|
348
|
+
return await ws.opened();
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
ws.abort();
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
listProfiles() { this.ensureOpen(); return this.native.call("profiles"); }
|
|
356
|
+
stats() { this.ensureOpen(); return this.native.call("stats"); }
|
|
357
|
+
closeIdleConnections() { this.ensureOpen(); this.native.call("session_close_idle", this.handle); }
|
|
358
|
+
close() {
|
|
359
|
+
if (this.closed)
|
|
360
|
+
return;
|
|
361
|
+
this.closed = true;
|
|
362
|
+
try {
|
|
363
|
+
for (const transfer of this.active)
|
|
364
|
+
transfer.close(new errors_js_1.FastlsError("session is closed"));
|
|
365
|
+
for (const ws of this.websockets)
|
|
366
|
+
ws.abort(new errors_js_1.FastlsError("session is closed"));
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
try {
|
|
370
|
+
this.notifier?.close();
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
this.native.call("session_close", this.handle);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Release native resources immediately, then wait for cooperative upload cleanup. */
|
|
378
|
+
async closeAsync() {
|
|
379
|
+
const producers = [...this.producers];
|
|
380
|
+
this.close();
|
|
381
|
+
await this.notifier?.closeAsync();
|
|
382
|
+
const results = await Promise.allSettled(producers.map(task => task.cleanup));
|
|
383
|
+
for (const result of results)
|
|
384
|
+
if (result.status === "rejected")
|
|
385
|
+
throw result.reason;
|
|
386
|
+
for (const task of producers)
|
|
387
|
+
task.check();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
exports.Session = Session;
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { StreamResponse } from "./response.js";
|
|
2
|
+
import type { RequestOptions } from "./session.js";
|
|
3
|
+
export interface SSEOptions extends RequestOptions {
|
|
4
|
+
/** Separate limits for a raw line and accumulated UTF-8 data; defaults to 1 MiB. */
|
|
5
|
+
maxEventBytes?: number;
|
|
6
|
+
/** Sets Last-Event-ID and the parser's initial ID. Reconnection is manual. */
|
|
7
|
+
lastEventId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class ServerSentEvent {
|
|
10
|
+
readonly data: string;
|
|
11
|
+
readonly event: string;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly retry?: number | undefined;
|
|
14
|
+
constructor(data: string, event?: string, id?: string, retry?: number | undefined);
|
|
15
|
+
json<T = unknown>(): T;
|
|
16
|
+
}
|
|
17
|
+
export declare function validateSSEOptions(maximum: number, initialId?: string): void;
|
|
18
|
+
/** Internal pull parser. feed() pauses at each event, including within one chunk. */
|
|
19
|
+
export declare class SSEDecoder {
|
|
20
|
+
private readonly maximum;
|
|
21
|
+
lastEventId: string;
|
|
22
|
+
retry?: number;
|
|
23
|
+
private id;
|
|
24
|
+
private blockRetry?;
|
|
25
|
+
private line;
|
|
26
|
+
private length;
|
|
27
|
+
private data;
|
|
28
|
+
private size;
|
|
29
|
+
private event;
|
|
30
|
+
private first;
|
|
31
|
+
private skipLF;
|
|
32
|
+
constructor(maximum: number, initialId?: string);
|
|
33
|
+
private append;
|
|
34
|
+
private processLine;
|
|
35
|
+
feed(chunk: Uint8Array): Generator<ServerSentEvent>;
|
|
36
|
+
finish(): void;
|
|
37
|
+
}
|
|
38
|
+
export declare class SSEStream implements AsyncIterable<ServerSentEvent> {
|
|
39
|
+
readonly response: StreamResponse;
|
|
40
|
+
private readonly decoder;
|
|
41
|
+
private started;
|
|
42
|
+
private closed;
|
|
43
|
+
constructor(response: StreamResponse, maximum?: number, initialId?: string);
|
|
44
|
+
get lastEventId(): string;
|
|
45
|
+
get retry(): number | undefined;
|
|
46
|
+
private checkOpen;
|
|
47
|
+
[Symbol.asyncIterator](): AsyncGenerator<ServerSentEvent>;
|
|
48
|
+
private events;
|
|
49
|
+
cancel(): void;
|
|
50
|
+
close(): void;
|
|
51
|
+
closeAsync(): Promise<void>;
|
|
52
|
+
}
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SSEStream = exports.SSEDecoder = exports.ServerSentEvent = void 0;
|
|
4
|
+
exports.validateSSEOptions = validateSSEOptions;
|
|
5
|
+
const promises_1 = require("node:timers/promises");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
class ServerSentEvent {
|
|
8
|
+
data;
|
|
9
|
+
event;
|
|
10
|
+
id;
|
|
11
|
+
retry;
|
|
12
|
+
constructor(data, event = "message", id = "", retry) {
|
|
13
|
+
this.data = data;
|
|
14
|
+
this.event = event;
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.retry = retry;
|
|
17
|
+
Object.freeze(this);
|
|
18
|
+
}
|
|
19
|
+
json() { return JSON.parse(this.data); }
|
|
20
|
+
}
|
|
21
|
+
exports.ServerSentEvent = ServerSentEvent;
|
|
22
|
+
function validateSSEOptions(maximum, initialId) {
|
|
23
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 64 * 1024 * 1024) {
|
|
24
|
+
throw new RangeError("maxEventBytes must be 1..67108864");
|
|
25
|
+
}
|
|
26
|
+
if (initialId !== undefined) {
|
|
27
|
+
if (typeof initialId !== "string")
|
|
28
|
+
throw new TypeError("lastEventId must be a string");
|
|
29
|
+
if (/[\0\r\n]/.test(initialId))
|
|
30
|
+
throw new TypeError("lastEventId must not contain NUL, CR or LF");
|
|
31
|
+
if (Buffer.byteLength(initialId) > maximum)
|
|
32
|
+
throw new RangeError("lastEventId exceeds maxEventBytes");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Internal pull parser. feed() pauses at each event, including within one chunk. */
|
|
36
|
+
class SSEDecoder {
|
|
37
|
+
maximum;
|
|
38
|
+
lastEventId;
|
|
39
|
+
retry;
|
|
40
|
+
id;
|
|
41
|
+
blockRetry;
|
|
42
|
+
line = Buffer.alloc(0);
|
|
43
|
+
length = 0;
|
|
44
|
+
data = [];
|
|
45
|
+
size = 0;
|
|
46
|
+
event = "";
|
|
47
|
+
first = true;
|
|
48
|
+
skipLF = false;
|
|
49
|
+
constructor(maximum, initialId = "") {
|
|
50
|
+
this.maximum = maximum;
|
|
51
|
+
this.id = this.lastEventId = initialId;
|
|
52
|
+
}
|
|
53
|
+
append(part) {
|
|
54
|
+
const required = this.length + part.length;
|
|
55
|
+
if (required > this.maximum)
|
|
56
|
+
throw new errors_js_1.FastlsError("SSE line exceeds maxEventBytes");
|
|
57
|
+
if (required > this.line.length) {
|
|
58
|
+
const grown = Buffer.allocUnsafe(Math.min(this.maximum, Math.max(required, this.line.length * 2, 256)));
|
|
59
|
+
this.line.copy(grown, 0, 0, this.length);
|
|
60
|
+
this.line = grown;
|
|
61
|
+
}
|
|
62
|
+
this.line.set(part, this.length);
|
|
63
|
+
this.length = required;
|
|
64
|
+
}
|
|
65
|
+
processLine() {
|
|
66
|
+
let line = this.line.toString("utf8", 0, this.length);
|
|
67
|
+
this.length = 0;
|
|
68
|
+
if (this.first) {
|
|
69
|
+
if (line.startsWith("\ufeff"))
|
|
70
|
+
line = line.slice(1);
|
|
71
|
+
this.first = false;
|
|
72
|
+
}
|
|
73
|
+
if (line === "") {
|
|
74
|
+
this.lastEventId = this.id;
|
|
75
|
+
const event = this.data.length ? new ServerSentEvent(this.data.join("\n"), this.event || "message", this.id, this.blockRetry) : undefined;
|
|
76
|
+
this.data = [];
|
|
77
|
+
this.size = 0;
|
|
78
|
+
this.event = "";
|
|
79
|
+
this.blockRetry = undefined;
|
|
80
|
+
return event;
|
|
81
|
+
}
|
|
82
|
+
if (line.startsWith(":"))
|
|
83
|
+
return;
|
|
84
|
+
const colon = line.indexOf(":"), field = colon < 0 ? line : line.slice(0, colon);
|
|
85
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
86
|
+
if (value.startsWith(" "))
|
|
87
|
+
value = value.slice(1);
|
|
88
|
+
if (field === "data") {
|
|
89
|
+
const size = this.size + Buffer.byteLength(value) + (this.data.length ? 1 : 0);
|
|
90
|
+
if (size > this.maximum)
|
|
91
|
+
throw new errors_js_1.FastlsError("SSE event exceeds maxEventBytes");
|
|
92
|
+
this.size = size;
|
|
93
|
+
this.data.push(value);
|
|
94
|
+
}
|
|
95
|
+
else if (field === "event")
|
|
96
|
+
this.event = value;
|
|
97
|
+
else if (field === "id" && !value.includes("\0"))
|
|
98
|
+
this.id = value;
|
|
99
|
+
else if (field === "retry" && /^[0-9]+$/.test(value)) {
|
|
100
|
+
const digits = value.replace(/^0+/, "") || "0", ceiling = "2147483647";
|
|
101
|
+
this.retry = this.blockRetry = digits.length > ceiling.length || (digits.length === ceiling.length && digits > ceiling) ? 2147483647 : Number(digits);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
*feed(chunk) {
|
|
105
|
+
if (!chunk.length)
|
|
106
|
+
return;
|
|
107
|
+
let start = 0;
|
|
108
|
+
if (this.skipLF) {
|
|
109
|
+
if (chunk[0] === 10)
|
|
110
|
+
start = 1;
|
|
111
|
+
this.skipLF = false;
|
|
112
|
+
}
|
|
113
|
+
for (let i = start; i < chunk.length; i++) {
|
|
114
|
+
if (chunk[i] !== 10 && chunk[i] !== 13)
|
|
115
|
+
continue;
|
|
116
|
+
this.append(chunk.subarray(start, i));
|
|
117
|
+
const event = this.processLine();
|
|
118
|
+
if (chunk[i] === 13) {
|
|
119
|
+
if (chunk[i + 1] === 10)
|
|
120
|
+
i++;
|
|
121
|
+
else if (i + 1 === chunk.length)
|
|
122
|
+
this.skipLF = true;
|
|
123
|
+
}
|
|
124
|
+
start = i + 1;
|
|
125
|
+
if (event)
|
|
126
|
+
yield event;
|
|
127
|
+
}
|
|
128
|
+
this.append(chunk.subarray(start));
|
|
129
|
+
}
|
|
130
|
+
finish() {
|
|
131
|
+
this.line = Buffer.alloc(0);
|
|
132
|
+
this.length = 0;
|
|
133
|
+
this.data = [];
|
|
134
|
+
this.size = 0;
|
|
135
|
+
this.event = "";
|
|
136
|
+
this.id = this.lastEventId;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.SSEDecoder = SSEDecoder;
|
|
140
|
+
class SSEStream {
|
|
141
|
+
response;
|
|
142
|
+
decoder;
|
|
143
|
+
started = false;
|
|
144
|
+
closed = false;
|
|
145
|
+
constructor(response, maximum = 1024 * 1024, initialId) {
|
|
146
|
+
this.response = response;
|
|
147
|
+
validateSSEOptions(maximum, initialId);
|
|
148
|
+
this.decoder = new SSEDecoder(maximum, initialId);
|
|
149
|
+
if (response.status === 204)
|
|
150
|
+
return;
|
|
151
|
+
if (response.status !== 200)
|
|
152
|
+
throw new errors_js_1.FastlsError(`SSE requires HTTP 200, received ${response.status}`);
|
|
153
|
+
const types = response.headers.getAll("content-type");
|
|
154
|
+
if (types.length !== 1 || types[0].split(";", 1)[0].replace(/^[ \t]+|[ \t]+$/g, "").toLowerCase() !== "text/event-stream") {
|
|
155
|
+
throw new errors_js_1.FastlsError("SSE requires Content-Type text/event-stream");
|
|
156
|
+
}
|
|
157
|
+
const encodings = response.wireHeaders.getAll("content-encoding").flatMap(value => value.split(",")).map(value => value.trim().toLowerCase());
|
|
158
|
+
if (!response.decoded && encodings.some(value => value !== "" && value !== "identity")) {
|
|
159
|
+
throw new errors_js_1.FastlsError("SSE requires decoded content; enable decodeContent");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
get lastEventId() { return this.decoder.lastEventId; }
|
|
163
|
+
get retry() { return this.decoder.retry; }
|
|
164
|
+
checkOpen() {
|
|
165
|
+
if (this.closed)
|
|
166
|
+
throw new errors_js_1.FastlsError("SSE stream is closed or cancelled");
|
|
167
|
+
this.response.ensureOpen();
|
|
168
|
+
}
|
|
169
|
+
[Symbol.asyncIterator]() {
|
|
170
|
+
this.checkOpen();
|
|
171
|
+
if (this.started)
|
|
172
|
+
throw new errors_js_1.FastlsError("SSE stream permits only one consumer");
|
|
173
|
+
this.started = true;
|
|
174
|
+
return this.events();
|
|
175
|
+
}
|
|
176
|
+
async *events() {
|
|
177
|
+
try {
|
|
178
|
+
if (this.response.status === 204)
|
|
179
|
+
return;
|
|
180
|
+
for await (const chunk of this.response.body) {
|
|
181
|
+
for (const event of this.decoder.feed(chunk)) {
|
|
182
|
+
this.checkOpen();
|
|
183
|
+
yield event;
|
|
184
|
+
// Even many events in one native read must let timers and cancellation run.
|
|
185
|
+
await (0, promises_1.setImmediate)();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await this.closeAsync();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
cancel() { this.closed = true; this.decoder.finish(); this.response.cancel(); }
|
|
194
|
+
close() { this.closed = true; this.decoder.finish(); this.response.close(); }
|
|
195
|
+
async closeAsync() { this.close(); await this.response.closeAsync(); }
|
|
196
|
+
}
|
|
197
|
+
exports.SSEStream = SSEStream;
|
package/dist/text.d.ts
ADDED