@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
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
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
|
+
}
|