@fastls/fastls 0.0.0-stage → 0.10.1
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 +371 -2
- package/dist/cookies.d.ts +30 -0
- package/dist/cookies.js +70 -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/fetch.d.ts +20 -0
- package/dist/fetch.js +113 -0
- package/dist/headers.d.ts +12 -0
- package/dist/headers.js +28 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +28 -0
- package/dist/index.mjs +3 -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 +211 -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 +88 -0
- package/dist/response.js +253 -0
- package/dist/scheduler.d.ts +12 -0
- package/dist/scheduler.js +75 -0
- package/dist/session.d.ts +110 -0
- package/dist/session.js +455 -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 +249 -0
- package/package.json +42 -4
|
@@ -0,0 +1,249 @@
|
|
|
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(); const info = this.last = this.native.call("info", this.handle); if (info.status)
|
|
73
|
+
this.headers = new headers_js_1.Headers(info.header_values); return info; }
|
|
74
|
+
get closeCode() { return (this.closed ? this.last : this.info)?.websocket?.close_code ?? 1006; }
|
|
75
|
+
get closeReason() { return (this.closed ? this.last : this.info)?.websocket?.close_reason ?? ""; }
|
|
76
|
+
check(info) {
|
|
77
|
+
if (info.state === "error")
|
|
78
|
+
throw (0, errors_js_1.responseError)(info);
|
|
79
|
+
if (info.state === "eof")
|
|
80
|
+
throw new WebSocketClosed(info.websocket?.close_code, info.websocket?.close_reason);
|
|
81
|
+
}
|
|
82
|
+
wait(probe, signal = this.controller.signal) {
|
|
83
|
+
return this.notifier ? this.notifier.probe(this.handle, probe, signal) : scheduler_js_1.scheduler.wait(probe, signal);
|
|
84
|
+
}
|
|
85
|
+
/** @internal */
|
|
86
|
+
async opened() {
|
|
87
|
+
try {
|
|
88
|
+
const info = await this.wait(() => { const i = this.info; if (i.status === 101 && (i.state === "headers" || i.state === "eof"))
|
|
89
|
+
return i; this.check(i); return i.state === "pending" ? undefined : i; });
|
|
90
|
+
this.headers = new headers_js_1.Headers(info.header_values, info.headers);
|
|
91
|
+
this.subprotocol = info.websocket?.subprotocol ?? "";
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.abort(error instanceof Error ? error : undefined);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async write(opcode, data, signal = this.controller.signal) {
|
|
100
|
+
this.ensure();
|
|
101
|
+
if (this.writing)
|
|
102
|
+
throw new errors_js_1.FastlsError("another WebSocket send is active");
|
|
103
|
+
this.writing = true;
|
|
104
|
+
try {
|
|
105
|
+
let accepted = false;
|
|
106
|
+
await this.wait(() => {
|
|
107
|
+
const info = this.info;
|
|
108
|
+
this.check(info);
|
|
109
|
+
if (accepted)
|
|
110
|
+
return !info.websocket.write_pending ? true : undefined;
|
|
111
|
+
const code = this.native.wsSend(this.handle, opcode, data);
|
|
112
|
+
if (code < 0) {
|
|
113
|
+
this.check(this.info);
|
|
114
|
+
throw new errors_js_1.FastlsError("invalid WebSocket send or connection is closing");
|
|
115
|
+
}
|
|
116
|
+
if (code !== 0 && code !== 1)
|
|
117
|
+
throw new errors_js_1.FastlsError("invalid WebSocket send status");
|
|
118
|
+
if (code === 0) {
|
|
119
|
+
accepted = true;
|
|
120
|
+
const next = this.info;
|
|
121
|
+
this.check(next);
|
|
122
|
+
if (!next.websocket.write_pending)
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}, signal);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.abort(error instanceof Error ? error : undefined);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.writing = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
send(message) {
|
|
137
|
+
if (typeof message !== "string" && !(message instanceof Uint8Array))
|
|
138
|
+
return Promise.reject(new TypeError("WebSocket messages must be strings or bytes"));
|
|
139
|
+
if (typeof message === "string" && /[\uD800-\uDFFF]/u.test(message))
|
|
140
|
+
return Promise.reject(new TypeError("WebSocket text must be well-formed Unicode"));
|
|
141
|
+
if ((typeof message === "string" ? Buffer.byteLength(message) : message.byteLength) > this.maxMessageBytes)
|
|
142
|
+
return Promise.reject(new RangeError("WebSocket message exceeds maxMessageBytes"));
|
|
143
|
+
// Copy before any await: accepted data must not observe caller mutation.
|
|
144
|
+
return this.write(typeof message === "string" ? 1 : 2, Buffer.from(message));
|
|
145
|
+
}
|
|
146
|
+
control(opcode, data) {
|
|
147
|
+
if (!(data instanceof Uint8Array))
|
|
148
|
+
return Promise.reject(new TypeError("control payload must be bytes"));
|
|
149
|
+
if (data.byteLength > 125)
|
|
150
|
+
return Promise.reject(new RangeError("control payload exceeds 125 bytes"));
|
|
151
|
+
return this.write(opcode, Buffer.from(data));
|
|
152
|
+
}
|
|
153
|
+
ping(data = Buffer.alloc(0)) { return this.control(9, data); }
|
|
154
|
+
pong(data = Buffer.alloc(0)) { return this.control(10, data); }
|
|
155
|
+
async recv() {
|
|
156
|
+
this.ensure();
|
|
157
|
+
if (this.reading)
|
|
158
|
+
throw new errors_js_1.FastlsError("another WebSocket recv is active");
|
|
159
|
+
this.reading = true;
|
|
160
|
+
try {
|
|
161
|
+
let content = Buffer.alloc(0), length = 0, kind, turns = 0;
|
|
162
|
+
while (true) {
|
|
163
|
+
const block = await this.wait(() => {
|
|
164
|
+
this.ensure();
|
|
165
|
+
const r = this.native.wsRead(this.handle, scratch);
|
|
166
|
+
if (r.code === 1)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (r.code !== 0) {
|
|
169
|
+
this.check(this.info);
|
|
170
|
+
throw new WebSocketClosed();
|
|
171
|
+
}
|
|
172
|
+
if (![1, 2].includes(r.opcode))
|
|
173
|
+
throw new errors_js_1.FastlsError("invalid WebSocket message opcode");
|
|
174
|
+
return { ...r, data: Buffer.from(scratch.subarray(0, r.length)) };
|
|
175
|
+
});
|
|
176
|
+
kind ??= block.opcode;
|
|
177
|
+
if (block.opcode !== kind)
|
|
178
|
+
throw new errors_js_1.FastlsError("WebSocket message opcode changed mid-message");
|
|
179
|
+
const size = length + block.length;
|
|
180
|
+
if (size > this.maxMessageBytes)
|
|
181
|
+
throw new errors_js_1.FastlsError("WebSocket message exceeds maxMessageBytes");
|
|
182
|
+
if (size > content.length) {
|
|
183
|
+
const grown = Buffer.allocUnsafe(Math.min(this.maxMessageBytes, Math.max(size, content.length * 2)));
|
|
184
|
+
content.copy(grown, 0, 0, length);
|
|
185
|
+
content = grown;
|
|
186
|
+
}
|
|
187
|
+
block.data.copy(content, length);
|
|
188
|
+
length = size;
|
|
189
|
+
if (block.end) {
|
|
190
|
+
const bytes = content.subarray(0, length);
|
|
191
|
+
return kind === 1 ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes) : Buffer.from(bytes);
|
|
192
|
+
}
|
|
193
|
+
if (++turns % 16 === 0)
|
|
194
|
+
await (0, promises_1.setImmediate)();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.abort(error instanceof Error ? error : undefined);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
this.reading = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
closeAsync(code = 1000, reason = "") {
|
|
206
|
+
const data = closePayload(code, reason);
|
|
207
|
+
if (this.closing)
|
|
208
|
+
return this.closing;
|
|
209
|
+
if (this.closed)
|
|
210
|
+
return Promise.resolve();
|
|
211
|
+
this.closing = (async () => {
|
|
212
|
+
const budget = new AbortController(), timer = setTimeout(() => budget.abort(new errors_js_1.FastlsError("WebSocket close timed out")), this.closeTimeoutMs);
|
|
213
|
+
const signal = AbortSignal.any([this.controller.signal, budget.signal]);
|
|
214
|
+
try {
|
|
215
|
+
if (this.info.state === "headers") {
|
|
216
|
+
// A close may interrupt a pending reader/writer after its total budget.
|
|
217
|
+
if (!this.writing)
|
|
218
|
+
await this.write(8, data, signal);
|
|
219
|
+
await this.wait(() => { const info = this.info; return info.state !== "headers" ? true : undefined; }, signal);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { /* Closing always releases the handle, including a stalled peer. */ }
|
|
223
|
+
finally {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
this.abort();
|
|
226
|
+
}
|
|
227
|
+
})();
|
|
228
|
+
return this.closing;
|
|
229
|
+
}
|
|
230
|
+
abort(error) {
|
|
231
|
+
if (this.closed)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
this.last = this.native.call("info", this.handle);
|
|
235
|
+
}
|
|
236
|
+
catch { /* Retain last metadata on teardown. */ }
|
|
237
|
+
this.closed = true;
|
|
238
|
+
this.failure = error;
|
|
239
|
+
this.external?.removeEventListener("abort", this.externalAbort);
|
|
240
|
+
this.controller.abort(error ?? new errors_js_1.FastlsError("WebSocket is closed"));
|
|
241
|
+
try {
|
|
242
|
+
this.native.call("close", this.handle);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
this.release();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
exports.WebSocket = WebSocket;
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastls/fastls",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.10.1",
|
|
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
|
+
"tough-cookie": "6.0.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/FastTLS/fastls-ffi.git",
|
|
31
|
+
"directory": "node"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
|
36
|
+
},
|
|
37
|
+
"optionalDependencies": {
|
|
38
|
+
"@fastls/fastls-win32-x64": "0.10.1",
|
|
39
|
+
"@fastls/fastls-linux-x64-glibc": "0.10.1",
|
|
40
|
+
"@fastls/fastls-linux-arm64-glibc": "0.10.1",
|
|
41
|
+
"@fastls/fastls-darwin-x64": "0.10.1",
|
|
42
|
+
"@fastls/fastls-darwin-arm64": "0.10.1"
|
|
43
|
+
}
|
|
44
|
+
}
|