@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/text.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.responseEncoding = responseEncoding;
|
|
4
|
+
exports.decodeText = decodeText;
|
|
5
|
+
exports.decodeJSON = decodeJSON;
|
|
6
|
+
function responseEncoding(contentType) {
|
|
7
|
+
return /(?:^|;)\s*charset\s*=\s*(?:"([^"]+)"|'([^']+)'|([^;\s]+))/i.exec(contentType ?? "")?.slice(1).find(Boolean)?.toLowerCase() ?? "utf-8";
|
|
8
|
+
}
|
|
9
|
+
function decodeText(bytes, encoding) {
|
|
10
|
+
// WHATWG aliases ISO-8859-1 to Windows-1252. Preserve the declared Latin-1
|
|
11
|
+
// byte mapping, matching Python for the full 0..255 range.
|
|
12
|
+
if (/^(?:iso-8859-1|latin-?1)$/i.test(encoding))
|
|
13
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1");
|
|
14
|
+
return new TextDecoder(encoding, { ignoreBOM: true }).decode(bytes);
|
|
15
|
+
}
|
|
16
|
+
function decodeJSON(bytes) {
|
|
17
|
+
let encoding = "utf-8", start = 0;
|
|
18
|
+
if (bytes.length >= 4 && ((bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 254 && bytes[3] === 255) || (bytes[0] === 255 && bytes[1] === 254 && bytes[2] === 0 && bytes[3] === 0))) {
|
|
19
|
+
encoding = bytes[0] === 0 ? "utf-32be" : "utf-32le";
|
|
20
|
+
start = 4;
|
|
21
|
+
}
|
|
22
|
+
else if (bytes[0] === 254 && bytes[1] === 255) {
|
|
23
|
+
encoding = "utf-16be";
|
|
24
|
+
start = 2;
|
|
25
|
+
}
|
|
26
|
+
else if (bytes[0] === 255 && bytes[1] === 254) {
|
|
27
|
+
encoding = "utf-16le";
|
|
28
|
+
start = 2;
|
|
29
|
+
}
|
|
30
|
+
else if (bytes.length >= 4) {
|
|
31
|
+
if (bytes[0] === 0)
|
|
32
|
+
encoding = bytes[1] === 0 ? "utf-32be" : "utf-16be";
|
|
33
|
+
else if (bytes[1] === 0)
|
|
34
|
+
encoding = bytes[2] === 0 && bytes[3] === 0 ? "utf-32le" : "utf-16le";
|
|
35
|
+
}
|
|
36
|
+
else if (bytes.length === 2) {
|
|
37
|
+
if (bytes[0] === 0)
|
|
38
|
+
encoding = "utf-16be";
|
|
39
|
+
else if (bytes[1] === 0)
|
|
40
|
+
encoding = "utf-16le";
|
|
41
|
+
}
|
|
42
|
+
if (encoding.startsWith("utf-32")) {
|
|
43
|
+
if ((bytes.length - start) % 4)
|
|
44
|
+
throw new TypeError("invalid UTF-32 JSON");
|
|
45
|
+
const out = [];
|
|
46
|
+
for (let offset = start; offset < bytes.length; offset += 4) {
|
|
47
|
+
const cp = encoding === "utf-32le" ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
|
48
|
+
if (cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))
|
|
49
|
+
throw new TypeError("invalid UTF-32 JSON");
|
|
50
|
+
out.push(String.fromCodePoint(cp));
|
|
51
|
+
}
|
|
52
|
+
return out.join("");
|
|
53
|
+
}
|
|
54
|
+
return new TextDecoder(encoding, { fatal: true }).decode(bytes.subarray(start));
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface TimeoutPolicy {
|
|
2
|
+
total: number;
|
|
3
|
+
connect?: number;
|
|
4
|
+
write?: number;
|
|
5
|
+
headers?: number;
|
|
6
|
+
read?: number;
|
|
7
|
+
}
|
|
8
|
+
export type Timeout = number | Readonly<TimeoutPolicy>;
|
|
9
|
+
export declare function timeoutFields(value: Timeout): Record<string, number>;
|
package/dist/timeout.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.timeoutFields = timeoutFields;
|
|
4
|
+
function timeoutFields(value) {
|
|
5
|
+
const policy = typeof value === "number" ? { total: value } : value;
|
|
6
|
+
if (!policy || typeof policy !== "object")
|
|
7
|
+
throw new TypeError("timeoutMs must be milliseconds or a timeout policy");
|
|
8
|
+
const fields = {};
|
|
9
|
+
for (const phase of ["total", "connect", "write", "headers", "read"]) {
|
|
10
|
+
const number = policy[phase];
|
|
11
|
+
if (number === undefined && phase !== "total")
|
|
12
|
+
continue;
|
|
13
|
+
if (!Number.isSafeInteger(number) || number < 1 || number > 86400000) {
|
|
14
|
+
throw new RangeError(`${phase} timeoutMs must be an integer in 1..86400000`);
|
|
15
|
+
}
|
|
16
|
+
fields[phase === "total" ? "timeout_ms" : `${phase}_timeout_ms`] = number;
|
|
17
|
+
}
|
|
18
|
+
return fields;
|
|
19
|
+
}
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Transfer, type UploadLifecycle } from "./response.js";
|
|
2
|
+
export type ByteSource = Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
|
|
3
|
+
/** A factory can use the supplied signal to interrupt its own pending I/O. */
|
|
4
|
+
export type UploadBody = string | Uint8Array | ByteSource | ((signal: AbortSignal) => ByteSource);
|
|
5
|
+
/** Internal marker for a known last block; avoid an EOF race with early multipart responses. */
|
|
6
|
+
export declare const terminalUploadChunks: WeakSet<Uint8Array<ArrayBufferLike>>;
|
|
7
|
+
export declare function abortable<T>(promise: PromiseLike<T> | T, signal: AbortSignal): Promise<T>;
|
|
8
|
+
export declare function isStreamingBody(value: UploadBody | undefined): boolean;
|
|
9
|
+
export declare function startUpload(transfer: Transfer, body: UploadBody): UploadLifecycle & {
|
|
10
|
+
stop: () => void;
|
|
11
|
+
};
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.terminalUploadChunks = void 0;
|
|
4
|
+
exports.abortable = abortable;
|
|
5
|
+
exports.isStreamingBody = isStreamingBody;
|
|
6
|
+
exports.startUpload = startUpload;
|
|
7
|
+
const promises_1 = require("node:timers/promises");
|
|
8
|
+
/** Internal marker for a known last block; avoid an EOF race with early multipart responses. */
|
|
9
|
+
exports.terminalUploadChunks = new WeakSet();
|
|
10
|
+
function abortable(promise, signal) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
13
|
+
Promise.resolve(promise).then(value => {
|
|
14
|
+
signal.removeEventListener("abort", abort);
|
|
15
|
+
resolve(value);
|
|
16
|
+
}, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
17
|
+
if (signal.aborted)
|
|
18
|
+
abort();
|
|
19
|
+
else
|
|
20
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function* scalarChunks(value) {
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
for (let offset = 0; offset < value.length;) {
|
|
26
|
+
let end = Math.min(value.length, offset + 16384);
|
|
27
|
+
if (end < value.length && value.charCodeAt(end - 1) >= 0xd800 && value.charCodeAt(end - 1) <= 0xdbff)
|
|
28
|
+
end--;
|
|
29
|
+
yield Buffer.from(value.slice(offset, end), "utf8");
|
|
30
|
+
offset = end;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (let offset = 0; offset < value.byteLength; offset += 65536)
|
|
35
|
+
yield value.subarray(offset, offset + 65536);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isStreamingBody(value) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
return value.length > 512 * 1024 || Buffer.byteLength(value) > 512 * 1024;
|
|
43
|
+
if (value instanceof Uint8Array)
|
|
44
|
+
return value.byteLength > 512 * 1024;
|
|
45
|
+
if (typeof value === "function" || (value && (Symbol.iterator in value || Symbol.asyncIterator in value)))
|
|
46
|
+
return true;
|
|
47
|
+
throw new TypeError("body must be string, Uint8Array, an iterable of bytes or an upload factory");
|
|
48
|
+
}
|
|
49
|
+
function startUpload(transfer, body) {
|
|
50
|
+
let completed = false;
|
|
51
|
+
let failure;
|
|
52
|
+
let clean;
|
|
53
|
+
let failCleanup;
|
|
54
|
+
const cleanup = new Promise((resolve, reject) => { clean = resolve; failCleanup = reject; });
|
|
55
|
+
// Cleanup is also awaited explicitly by closeAsync; keep synchronous close safe.
|
|
56
|
+
void cleanup.catch(() => { });
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const forwardAbort = () => controller.abort(transfer.signal.reason);
|
|
59
|
+
transfer.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
60
|
+
if (transfer.signal.aborted)
|
|
61
|
+
forwardAbort();
|
|
62
|
+
const stop = () => controller.abort(new Error("upload finished"));
|
|
63
|
+
const done = (async () => {
|
|
64
|
+
let iterator;
|
|
65
|
+
let nextPending = false;
|
|
66
|
+
let pendingNext;
|
|
67
|
+
let source;
|
|
68
|
+
try {
|
|
69
|
+
source = typeof body === "function" ? body(controller.signal)
|
|
70
|
+
: typeof body === "string" || body instanceof Uint8Array ? scalarChunks(body) : body;
|
|
71
|
+
iterator = Symbol.asyncIterator in source ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
|
|
72
|
+
let turns = 0;
|
|
73
|
+
while (!controller.signal.aborted && transfer.canWrite()) {
|
|
74
|
+
nextPending = true;
|
|
75
|
+
pendingNext = Promise.resolve(iterator.next());
|
|
76
|
+
const result = await abortable(pendingNext, controller.signal);
|
|
77
|
+
nextPending = false;
|
|
78
|
+
pendingNext = undefined;
|
|
79
|
+
if (result.done) {
|
|
80
|
+
completed = await transfer.write(Buffer.alloc(0), true, controller.signal) === 0;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!(result.value instanceof Uint8Array))
|
|
84
|
+
throw new TypeError("upload chunks must be Uint8Array or Buffer");
|
|
85
|
+
const terminal = exports.terminalUploadChunks.has(result.value);
|
|
86
|
+
for (let offset = 0; offset < result.value.byteLength; offset += 65536) {
|
|
87
|
+
const end = terminal && offset + 65536 >= result.value.byteLength;
|
|
88
|
+
if (await transfer.write(result.value.subarray(offset, offset + 65536), end, controller.signal) === 2)
|
|
89
|
+
return;
|
|
90
|
+
if (end) {
|
|
91
|
+
completed = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (++turns % 16 === 0)
|
|
96
|
+
await (0, promises_1.setImmediate)();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!controller.signal.aborted) {
|
|
101
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
102
|
+
transfer.close(failure);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
controller.abort(new Error("upload stopped"));
|
|
107
|
+
transfer.signal.removeEventListener("abort", forwardAbort);
|
|
108
|
+
const finishing = (async () => {
|
|
109
|
+
// Readable.destroy interrupts pending next(); arbitrary promises require cooperation.
|
|
110
|
+
if (source && "destroy" in source && typeof source.destroy === "function")
|
|
111
|
+
source.destroy();
|
|
112
|
+
// Calling return() queues cleanup behind an outstanding async next().
|
|
113
|
+
// Observe both outcomes so an error thrown by finally is not swallowed
|
|
114
|
+
// when cancellation already won the abortable() race.
|
|
115
|
+
const results = await Promise.allSettled([pendingNext, iterator?.return?.()]);
|
|
116
|
+
for (const result of results) {
|
|
117
|
+
if (result.status !== "rejected")
|
|
118
|
+
continue;
|
|
119
|
+
const error = result.reason;
|
|
120
|
+
if (controller.signal.aborted && (error === controller.signal.reason || (error instanceof Error && error.name === "AbortError")))
|
|
121
|
+
continue;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
void finishing.then(clean, error => {
|
|
126
|
+
failure ??= error instanceof Error ? error : new Error(String(error));
|
|
127
|
+
transfer.close(failure);
|
|
128
|
+
failCleanup(failure);
|
|
129
|
+
});
|
|
130
|
+
// A pending uncooperative next() must not delay native request cancellation.
|
|
131
|
+
if (!nextPending) {
|
|
132
|
+
try {
|
|
133
|
+
await abortable(finishing, transfer.signal);
|
|
134
|
+
}
|
|
135
|
+
catch { /* failure retained above */ }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
return { done, cleanup, stop, complete: () => completed, check: () => { if (failure)
|
|
140
|
+
throw failure; } };
|
|
141
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { FastlsError } from "./errors.js";
|
|
2
|
+
import { Headers } from "./headers.js";
|
|
3
|
+
import type { Native, NativeInfo } from "./native.js";
|
|
4
|
+
import type { Notifier } from "./notifications.js";
|
|
5
|
+
import type { QueryParams } from "./request-options.js";
|
|
6
|
+
export interface WebSocketOptions {
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
profile?: string;
|
|
9
|
+
params?: QueryParams;
|
|
10
|
+
proxy?: string | null;
|
|
11
|
+
proxyCaFile?: string | null;
|
|
12
|
+
subprotocols?: readonly string[];
|
|
13
|
+
handshakeTimeoutMs?: number;
|
|
14
|
+
readTimeoutMs?: number | null;
|
|
15
|
+
writeTimeoutMs?: number;
|
|
16
|
+
cookieMode?: "session" | "manual";
|
|
17
|
+
closeTimeoutMs?: number;
|
|
18
|
+
maxMessageBytes?: number;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
export declare class WebSocketClosed extends FastlsError {
|
|
22
|
+
readonly code: number;
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
name: string;
|
|
25
|
+
constructor(code?: number, reason?: string);
|
|
26
|
+
}
|
|
27
|
+
export declare function wsBudget(value: number, name: string, maximum?: number): number;
|
|
28
|
+
export declare class WebSocket {
|
|
29
|
+
private readonly native;
|
|
30
|
+
private readonly handle;
|
|
31
|
+
private readonly release;
|
|
32
|
+
readonly maxMessageBytes: number;
|
|
33
|
+
private readonly closeTimeoutMs;
|
|
34
|
+
private readonly notifier?;
|
|
35
|
+
private readonly external?;
|
|
36
|
+
private readonly controller;
|
|
37
|
+
private closed;
|
|
38
|
+
private reading;
|
|
39
|
+
private writing;
|
|
40
|
+
private failure?;
|
|
41
|
+
private last?;
|
|
42
|
+
private closing?;
|
|
43
|
+
private readonly externalAbort;
|
|
44
|
+
headers: Headers;
|
|
45
|
+
subprotocol: string;
|
|
46
|
+
constructor(native: Native, handle: bigint, release: () => void, maxMessageBytes: number, closeTimeoutMs: number, notifier?: Notifier | undefined, external?: AbortSignal | undefined);
|
|
47
|
+
private ensure;
|
|
48
|
+
get info(): NativeInfo;
|
|
49
|
+
get closeCode(): number;
|
|
50
|
+
get closeReason(): string;
|
|
51
|
+
private check;
|
|
52
|
+
private wait;
|
|
53
|
+
/** @internal */
|
|
54
|
+
opened(): Promise<this>;
|
|
55
|
+
private write;
|
|
56
|
+
send(message: string | Uint8Array): Promise<void>;
|
|
57
|
+
private control;
|
|
58
|
+
ping(data?: Uint8Array): Promise<void>;
|
|
59
|
+
pong(data?: Uint8Array): Promise<void>;
|
|
60
|
+
recv(): Promise<string | Buffer>;
|
|
61
|
+
closeAsync(code?: number, reason?: string): Promise<void>;
|
|
62
|
+
abort(error?: Error): void;
|
|
63
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebSocket = exports.WebSocketClosed = void 0;
|
|
4
|
+
exports.wsBudget = wsBudget;
|
|
5
|
+
const promises_1 = require("node:timers/promises");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
const headers_js_1 = require("./headers.js");
|
|
8
|
+
const scheduler_js_1 = require("./scheduler.js");
|
|
9
|
+
class WebSocketClosed extends errors_js_1.FastlsError {
|
|
10
|
+
code;
|
|
11
|
+
reason;
|
|
12
|
+
name = "WebSocketClosed";
|
|
13
|
+
constructor(code = 1006, reason = "") {
|
|
14
|
+
super(`WebSocket closed (${code})`);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.reason = reason;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.WebSocketClosed = WebSocketClosed;
|
|
20
|
+
function wsBudget(value, name, maximum = 86400000) {
|
|
21
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
22
|
+
throw new RangeError(`${name} must be 1..${maximum} milliseconds`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function closePayload(code, reason) {
|
|
26
|
+
if (!Number.isInteger(code) || !([1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014].includes(code) || (code >= 3000 && code <= 4999)))
|
|
27
|
+
throw new RangeError("invalid WebSocket close code");
|
|
28
|
+
if (typeof reason !== "string" || /[\uD800-\uDFFF]/u.test(reason))
|
|
29
|
+
throw new TypeError("close reason must be well-formed Unicode");
|
|
30
|
+
const text = Buffer.from(reason);
|
|
31
|
+
if (text.length > 123)
|
|
32
|
+
throw new RangeError("close reason exceeds 123 UTF-8 bytes");
|
|
33
|
+
const out = Buffer.allocUnsafe(text.length + 2);
|
|
34
|
+
out.writeUInt16BE(code);
|
|
35
|
+
text.copy(out, 2);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const scratch = Buffer.allocUnsafe(65536);
|
|
39
|
+
class WebSocket {
|
|
40
|
+
native;
|
|
41
|
+
handle;
|
|
42
|
+
release;
|
|
43
|
+
maxMessageBytes;
|
|
44
|
+
closeTimeoutMs;
|
|
45
|
+
notifier;
|
|
46
|
+
external;
|
|
47
|
+
controller = new AbortController();
|
|
48
|
+
closed = false;
|
|
49
|
+
reading = false;
|
|
50
|
+
writing = false;
|
|
51
|
+
failure;
|
|
52
|
+
last;
|
|
53
|
+
closing;
|
|
54
|
+
externalAbort = () => this.abort(new errors_js_1.AbortError(undefined, { cause: this.external?.reason }));
|
|
55
|
+
headers = new headers_js_1.Headers();
|
|
56
|
+
subprotocol = "";
|
|
57
|
+
constructor(native, handle, release, maxMessageBytes, closeTimeoutMs, notifier, external) {
|
|
58
|
+
this.native = native;
|
|
59
|
+
this.handle = handle;
|
|
60
|
+
this.release = release;
|
|
61
|
+
this.maxMessageBytes = maxMessageBytes;
|
|
62
|
+
this.closeTimeoutMs = closeTimeoutMs;
|
|
63
|
+
this.notifier = notifier;
|
|
64
|
+
this.external = external;
|
|
65
|
+
external?.addEventListener("abort", this.externalAbort, { once: true });
|
|
66
|
+
if (external?.aborted)
|
|
67
|
+
this.externalAbort();
|
|
68
|
+
}
|
|
69
|
+
ensure() { if (this.closed)
|
|
70
|
+
throw this.failure ?? new errors_js_1.FastlsError("WebSocket is closed"); }
|
|
71
|
+
get info() { if (this.closed && this.last)
|
|
72
|
+
return this.last; this.ensure(); return this.last = this.native.call("info", this.handle); }
|
|
73
|
+
get closeCode() { return (this.closed ? this.last : this.info)?.websocket?.close_code ?? 1006; }
|
|
74
|
+
get closeReason() { return (this.closed ? this.last : this.info)?.websocket?.close_reason ?? ""; }
|
|
75
|
+
check(info) {
|
|
76
|
+
if (info.state === "error")
|
|
77
|
+
throw (0, errors_js_1.responseError)(info);
|
|
78
|
+
if (info.state === "eof")
|
|
79
|
+
throw new WebSocketClosed(info.websocket?.close_code, info.websocket?.close_reason);
|
|
80
|
+
}
|
|
81
|
+
wait(probe, signal = this.controller.signal) {
|
|
82
|
+
return this.notifier ? this.notifier.probe(this.handle, probe, signal) : scheduler_js_1.scheduler.wait(probe, signal);
|
|
83
|
+
}
|
|
84
|
+
/** @internal */
|
|
85
|
+
async opened() {
|
|
86
|
+
try {
|
|
87
|
+
const info = await this.wait(() => { const i = this.info; if (i.status === 101 && (i.state === "headers" || i.state === "eof"))
|
|
88
|
+
return i; this.check(i); return i.state === "pending" ? undefined : i; });
|
|
89
|
+
this.headers = new headers_js_1.Headers(info.header_values, info.headers);
|
|
90
|
+
this.subprotocol = info.websocket?.subprotocol ?? "";
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.abort(error instanceof Error ? error : undefined);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async write(opcode, data, signal = this.controller.signal) {
|
|
99
|
+
this.ensure();
|
|
100
|
+
if (this.writing)
|
|
101
|
+
throw new errors_js_1.FastlsError("another WebSocket send is active");
|
|
102
|
+
this.writing = true;
|
|
103
|
+
try {
|
|
104
|
+
let accepted = false;
|
|
105
|
+
await this.wait(() => {
|
|
106
|
+
const info = this.info;
|
|
107
|
+
this.check(info);
|
|
108
|
+
if (accepted)
|
|
109
|
+
return !info.websocket.write_pending ? true : undefined;
|
|
110
|
+
const code = this.native.wsSend(this.handle, opcode, data);
|
|
111
|
+
if (code < 0) {
|
|
112
|
+
this.check(this.info);
|
|
113
|
+
throw new errors_js_1.FastlsError("invalid WebSocket send or connection is closing");
|
|
114
|
+
}
|
|
115
|
+
if (code !== 0 && code !== 1)
|
|
116
|
+
throw new errors_js_1.FastlsError("invalid WebSocket send status");
|
|
117
|
+
if (code === 0) {
|
|
118
|
+
accepted = true;
|
|
119
|
+
const next = this.info;
|
|
120
|
+
this.check(next);
|
|
121
|
+
if (!next.websocket.write_pending)
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}, signal);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.abort(error instanceof Error ? error : undefined);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
this.writing = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
send(message) {
|
|
136
|
+
if (typeof message !== "string" && !(message instanceof Uint8Array))
|
|
137
|
+
return Promise.reject(new TypeError("WebSocket messages must be strings or bytes"));
|
|
138
|
+
if (typeof message === "string" && /[\uD800-\uDFFF]/u.test(message))
|
|
139
|
+
return Promise.reject(new TypeError("WebSocket text must be well-formed Unicode"));
|
|
140
|
+
if ((typeof message === "string" ? Buffer.byteLength(message) : message.byteLength) > this.maxMessageBytes)
|
|
141
|
+
return Promise.reject(new RangeError("WebSocket message exceeds maxMessageBytes"));
|
|
142
|
+
// Copy before any await: accepted data must not observe caller mutation.
|
|
143
|
+
return this.write(typeof message === "string" ? 1 : 2, Buffer.from(message));
|
|
144
|
+
}
|
|
145
|
+
control(opcode, data) {
|
|
146
|
+
if (!(data instanceof Uint8Array))
|
|
147
|
+
return Promise.reject(new TypeError("control payload must be bytes"));
|
|
148
|
+
if (data.byteLength > 125)
|
|
149
|
+
return Promise.reject(new RangeError("control payload exceeds 125 bytes"));
|
|
150
|
+
return this.write(opcode, Buffer.from(data));
|
|
151
|
+
}
|
|
152
|
+
ping(data = Buffer.alloc(0)) { return this.control(9, data); }
|
|
153
|
+
pong(data = Buffer.alloc(0)) { return this.control(10, data); }
|
|
154
|
+
async recv() {
|
|
155
|
+
this.ensure();
|
|
156
|
+
if (this.reading)
|
|
157
|
+
throw new errors_js_1.FastlsError("another WebSocket recv is active");
|
|
158
|
+
this.reading = true;
|
|
159
|
+
try {
|
|
160
|
+
let content = Buffer.alloc(0), length = 0, kind, turns = 0;
|
|
161
|
+
while (true) {
|
|
162
|
+
const block = await this.wait(() => {
|
|
163
|
+
this.ensure();
|
|
164
|
+
const r = this.native.wsRead(this.handle, scratch);
|
|
165
|
+
if (r.code === 1)
|
|
166
|
+
return undefined;
|
|
167
|
+
if (r.code !== 0) {
|
|
168
|
+
this.check(this.info);
|
|
169
|
+
throw new WebSocketClosed();
|
|
170
|
+
}
|
|
171
|
+
if (![1, 2].includes(r.opcode))
|
|
172
|
+
throw new errors_js_1.FastlsError("invalid WebSocket message opcode");
|
|
173
|
+
return { ...r, data: Buffer.from(scratch.subarray(0, r.length)) };
|
|
174
|
+
});
|
|
175
|
+
kind ??= block.opcode;
|
|
176
|
+
if (block.opcode !== kind)
|
|
177
|
+
throw new errors_js_1.FastlsError("WebSocket message opcode changed mid-message");
|
|
178
|
+
const size = length + block.length;
|
|
179
|
+
if (size > this.maxMessageBytes)
|
|
180
|
+
throw new errors_js_1.FastlsError("WebSocket message exceeds maxMessageBytes");
|
|
181
|
+
if (size > content.length) {
|
|
182
|
+
const grown = Buffer.allocUnsafe(Math.min(this.maxMessageBytes, Math.max(size, content.length * 2)));
|
|
183
|
+
content.copy(grown, 0, 0, length);
|
|
184
|
+
content = grown;
|
|
185
|
+
}
|
|
186
|
+
block.data.copy(content, length);
|
|
187
|
+
length = size;
|
|
188
|
+
if (block.end) {
|
|
189
|
+
const bytes = content.subarray(0, length);
|
|
190
|
+
return kind === 1 ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes) : Buffer.from(bytes);
|
|
191
|
+
}
|
|
192
|
+
if (++turns % 16 === 0)
|
|
193
|
+
await (0, promises_1.setImmediate)();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.abort(error instanceof Error ? error : undefined);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
this.reading = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
closeAsync(code = 1000, reason = "") {
|
|
205
|
+
const data = closePayload(code, reason);
|
|
206
|
+
if (this.closing)
|
|
207
|
+
return this.closing;
|
|
208
|
+
if (this.closed)
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
this.closing = (async () => {
|
|
211
|
+
const budget = new AbortController(), timer = setTimeout(() => budget.abort(new errors_js_1.FastlsError("WebSocket close timed out")), this.closeTimeoutMs);
|
|
212
|
+
const signal = AbortSignal.any([this.controller.signal, budget.signal]);
|
|
213
|
+
try {
|
|
214
|
+
if (this.info.state === "headers") {
|
|
215
|
+
// A close may interrupt a pending reader/writer after its total budget.
|
|
216
|
+
if (!this.writing)
|
|
217
|
+
await this.write(8, data, signal);
|
|
218
|
+
await this.wait(() => { const info = this.info; return info.state !== "headers" ? true : undefined; }, signal);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch { /* Closing always releases the handle, including a stalled peer. */ }
|
|
222
|
+
finally {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
this.abort();
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
return this.closing;
|
|
228
|
+
}
|
|
229
|
+
abort(error) {
|
|
230
|
+
if (this.closed)
|
|
231
|
+
return;
|
|
232
|
+
try {
|
|
233
|
+
this.last = this.native.call("info", this.handle);
|
|
234
|
+
}
|
|
235
|
+
catch { /* Retain last metadata on teardown. */ }
|
|
236
|
+
this.closed = true;
|
|
237
|
+
this.failure = error;
|
|
238
|
+
this.external?.removeEventListener("abort", this.externalAbort);
|
|
239
|
+
this.controller.abort(error ?? new errors_js_1.FastlsError("WebSocket is closed"));
|
|
240
|
+
try {
|
|
241
|
+
this.native.call("close", this.handle);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
this.release();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.WebSocket = WebSocket;
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastls/fastls",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.10.1-dev.0",
|
|
4
|
+
"description": "Node.js bindings for the Fastls C ABI",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"koffi": "3.3.3",
|
|
24
|
+
"lossless-json": "4.3.3"
|
|
25
|
+
},
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/FastTLS/fastls-ffi.git",
|
|
30
|
+
"directory": "node"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"@fastls/fastls-win32-x64": "0.10.1-dev.0",
|
|
38
|
+
"@fastls/fastls-linux-x64-glibc": "0.10.1-dev.0",
|
|
39
|
+
"@fastls/fastls-linux-arm64-glibc": "0.10.1-dev.0",
|
|
40
|
+
"@fastls/fastls-darwin-x64": "0.10.1-dev.0",
|
|
41
|
+
"@fastls/fastls-darwin-arm64": "0.10.1-dev.0"
|
|
42
|
+
}
|
|
43
|
+
}
|