@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.
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StreamResponse = exports.Response = exports.Transfer = exports.MAX_BUFFERED_BODY = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const text_js_1 = require("./text.js");
6
+ const headers_js_1 = require("./headers.js");
7
+ const scheduler_js_1 = require("./scheduler.js");
8
+ exports.MAX_BUFFERED_BODY = 32 * 1024 * 1024;
9
+ // ABI reads are synchronous and main-thread-only. Copy each result before another
10
+ // probe runs, so pending requests can share scratch space without retaining it.
11
+ const READ_BUFFER = Buffer.allocUnsafe(65536);
12
+ /** Owns one native request and all cancellation hooks. Internal to the SDK. */
13
+ class Transfer {
14
+ native;
15
+ handle;
16
+ release;
17
+ external;
18
+ duplex;
19
+ notifier;
20
+ controller = new AbortController();
21
+ closed = false;
22
+ failure;
23
+ timer;
24
+ deadline;
25
+ externalAbort = () => this.close(new errors_js_1.AbortError(undefined, { cause: this.external?.reason }));
26
+ constructor(native, handle, release, external, duplex = false, notifier) {
27
+ this.native = native;
28
+ this.handle = handle;
29
+ this.release = release;
30
+ this.external = external;
31
+ this.duplex = duplex;
32
+ this.notifier = notifier;
33
+ }
34
+ wait(probe, signal) {
35
+ return this.notifier ? this.notifier.probe(this.handle, probe, signal) : scheduler_js_1.scheduler.wait(probe, signal);
36
+ }
37
+ attachSignal(total, started = performance.now()) {
38
+ this.external?.addEventListener("abort", this.externalAbort, { once: true });
39
+ if (this.external?.aborted)
40
+ this.externalAbort();
41
+ if (total && !this.closed) {
42
+ this.deadline = started + total;
43
+ this.timer = setTimeout(() => this.close(new errors_js_1.FastlsTimeoutError("total deadline exceeded", "total")), Math.max(1, this.deadline - performance.now()));
44
+ }
45
+ }
46
+ get signal() { return this.controller.signal; }
47
+ info() {
48
+ this.ensureOpen();
49
+ const info = this.native.call("info", this.handle);
50
+ if (info.state === "error")
51
+ throw (0, errors_js_1.responseError)(info);
52
+ return info;
53
+ }
54
+ write(buffer, end, signal) {
55
+ return this.wait(() => {
56
+ if (!this.canWrite())
57
+ return 2;
58
+ const code = this.native.write(this.handle, buffer, end);
59
+ return code === 1 ? undefined : code;
60
+ }, signal);
61
+ }
62
+ canWrite() {
63
+ const info = this.info();
64
+ return info.state === "pending" || (this.duplex && info.state === "headers" && info.status < 300);
65
+ }
66
+ throwFailure() { if (this.failure)
67
+ throw this.failure; }
68
+ async headers() {
69
+ return this.wait(() => {
70
+ this.ensureOpen();
71
+ const info = this.info();
72
+ return info.state === "pending" ? undefined : info;
73
+ }, this.controller.signal);
74
+ }
75
+ async next() {
76
+ this.ensureOpen();
77
+ const buffer = READ_BUFFER;
78
+ const result = await this.wait(() => {
79
+ this.ensureOpen();
80
+ const { code, length } = this.native.read(this.handle, buffer);
81
+ if (code === 1)
82
+ return undefined;
83
+ if (code === 2)
84
+ return null;
85
+ if (!length)
86
+ throw new errors_js_1.FastlsError("native data result was empty");
87
+ return Buffer.from(buffer.subarray(0, length));
88
+ }, this.controller.signal);
89
+ this.ensureOpen();
90
+ return result;
91
+ }
92
+ ensureOpen() {
93
+ if (!this.closed && this.deadline !== undefined && performance.now() >= this.deadline) {
94
+ this.close(new errors_js_1.FastlsTimeoutError("total deadline exceeded", "total"));
95
+ }
96
+ if (this.closed)
97
+ throw this.failure ?? new errors_js_1.FastlsError("response is closed");
98
+ }
99
+ close(error) {
100
+ if (this.closed)
101
+ return;
102
+ this.closed = true;
103
+ this.failure = error;
104
+ if (this.timer)
105
+ clearTimeout(this.timer);
106
+ this.external?.removeEventListener("abort", this.externalAbort);
107
+ this.controller.abort(error ?? new errors_js_1.FastlsError("response is closed"));
108
+ try {
109
+ this.native.call("close", this.handle);
110
+ }
111
+ finally {
112
+ this.release();
113
+ }
114
+ }
115
+ }
116
+ exports.Transfer = Transfer;
117
+ class Response {
118
+ status;
119
+ headers;
120
+ match;
121
+ content;
122
+ wireHeaders;
123
+ decoded;
124
+ httpVersion;
125
+ url;
126
+ trailers;
127
+ encoding;
128
+ constructor(status, headers, match, content, wireHeaders = headers, decoded = false, httpVersion = "", url = "", trailers = new headers_js_1.Headers()) {
129
+ this.status = status;
130
+ this.headers = headers;
131
+ this.match = match;
132
+ this.content = content;
133
+ this.wireHeaders = wireHeaders;
134
+ this.decoded = decoded;
135
+ this.httpVersion = httpVersion;
136
+ this.url = url;
137
+ this.trailers = trailers;
138
+ this.encoding = (0, text_js_1.responseEncoding)(headers.get("content-type"));
139
+ }
140
+ get ok() { return this.status < 400; }
141
+ raiseForStatus() { if (this.status >= 400 && this.status <= 599)
142
+ throw new errors_js_1.FastlsHTTPError(this); }
143
+ text() { return (0, text_js_1.decodeText)(this.content, this.encoding); }
144
+ json() { return JSON.parse((0, text_js_1.decodeJSON)(this.content)); }
145
+ }
146
+ exports.Response = Response;
147
+ class StreamResponse {
148
+ transfer;
149
+ upload;
150
+ encoding;
151
+ httpVersion;
152
+ url;
153
+ trailers;
154
+ status;
155
+ headers;
156
+ match;
157
+ wireHeaders;
158
+ decoded;
159
+ body;
160
+ consumed = false;
161
+ constructor(transfer, info, upload) {
162
+ this.transfer = transfer;
163
+ this.upload = upload;
164
+ this.status = info.status;
165
+ this.httpVersion = info.protocol ?? "";
166
+ this.url = info.final_url ?? "";
167
+ this.trailers = new headers_js_1.Headers(info.trailer_values);
168
+ this.headers = new headers_js_1.Headers(info.header_values, info.headers);
169
+ this.encoding = (0, text_js_1.responseEncoding)(this.headers.get("content-type"));
170
+ this.wireHeaders = info.wire_header_values ? new headers_js_1.Headers(info.wire_header_values) : this.headers;
171
+ this.decoded = info.decoded ?? false;
172
+ this.match = Object.freeze({ ...info.match });
173
+ this.body = { [Symbol.asyncIterator]: () => this.iterate() };
174
+ }
175
+ get ok() { return this.status < 400; }
176
+ raiseForStatus() { if (this.status >= 400 && this.status <= 599)
177
+ throw new errors_js_1.FastlsHTTPError(this); }
178
+ /** @internal Check cancellation even when a parser still holds buffered data. */
179
+ ensureOpen() { this.transfer.ensureOpen(); }
180
+ async *iterate() {
181
+ if (this.consumed)
182
+ throw new errors_js_1.FastlsError("response body already has a consumer");
183
+ this.consumed = true;
184
+ try {
185
+ while (true) {
186
+ const chunk = await this.transfer.next();
187
+ if (chunk === null) {
188
+ const info = this.transfer.info();
189
+ this.httpVersion = info.protocol ?? this.httpVersion;
190
+ this.url = info.final_url ?? this.url;
191
+ this.trailers = new headers_js_1.Headers(info.trailer_values);
192
+ return;
193
+ }
194
+ yield chunk;
195
+ }
196
+ }
197
+ finally {
198
+ await this.closeAsync();
199
+ }
200
+ }
201
+ async read() {
202
+ let content = Buffer.alloc(0);
203
+ let length = 0;
204
+ for await (const chunk of this.body) {
205
+ const required = length + chunk.length;
206
+ if (required > exports.MAX_BUFFERED_BODY)
207
+ throw new errors_js_1.FastlsError("buffered response exceeds 32MiB; use streaming");
208
+ if (required > content.length) {
209
+ const grown = Buffer.allocUnsafe(Math.min(exports.MAX_BUFFERED_BODY, Math.max(required, content.length * 2)));
210
+ content.copy(grown, 0, 0, length);
211
+ content = grown;
212
+ }
213
+ chunk.copy(content, length);
214
+ length = required;
215
+ }
216
+ return length === content.length ? content : Buffer.from(content.subarray(0, length));
217
+ }
218
+ async text() { return (0, text_js_1.decodeText)(await this.read(), this.encoding); }
219
+ async json() { return JSON.parse((0, text_js_1.decodeJSON)(await this.read())); }
220
+ /** Consume the response concurrently: waiting alone can block on HTTP/2 flow control. */
221
+ async waitUpload() {
222
+ if (!this.upload)
223
+ return;
224
+ await this.upload.done;
225
+ this.upload.check();
226
+ if (!this.upload.complete()) {
227
+ this.transfer.throwFailure();
228
+ throw new errors_js_1.FastlsError("upload ended before EOF was accepted");
229
+ }
230
+ }
231
+ cancel() { this.transfer.close(new errors_js_1.AbortError()); }
232
+ close() { this.transfer.close(); }
233
+ /** Wait for cooperative producer finalization after releasing the native request. */
234
+ async closeAsync() {
235
+ this.close();
236
+ if (this.upload) {
237
+ await this.upload.cleanup;
238
+ this.upload.check();
239
+ }
240
+ }
241
+ }
242
+ exports.StreamResponse = StreamResponse;
@@ -0,0 +1,12 @@
1
+ /** One bounded timer queue for all pending header/read operations in this SDK. */
2
+ declare class Scheduler {
3
+ private pending;
4
+ private timer?;
5
+ private immediateBudget;
6
+ private resetScheduled;
7
+ wait<T>(probe: () => T | undefined, signal: AbortSignal): Promise<T>;
8
+ private schedule;
9
+ private tick;
10
+ }
11
+ export declare const scheduler: Scheduler;
12
+ export {};
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scheduler = void 0;
4
+ /** One bounded timer queue for all pending header/read operations in this SDK. */
5
+ class Scheduler {
6
+ pending = new Set();
7
+ timer;
8
+ immediateBudget = 32;
9
+ resetScheduled = false;
10
+ wait(probe, signal) {
11
+ return new Promise((resolve, reject) => {
12
+ const cleanup = () => {
13
+ this.pending.delete(check);
14
+ signal.removeEventListener("abort", abort);
15
+ if (!this.pending.size && this.timer) {
16
+ clearTimeout(this.timer);
17
+ this.timer = undefined;
18
+ }
19
+ };
20
+ const abort = () => { cleanup(); reject(signal.reason); };
21
+ const check = () => {
22
+ try {
23
+ if (signal.aborted) {
24
+ abort();
25
+ return true;
26
+ }
27
+ const value = probe();
28
+ if (value === undefined)
29
+ return false;
30
+ cleanup();
31
+ resolve(value);
32
+ }
33
+ catch (error) {
34
+ cleanup();
35
+ reject(error);
36
+ }
37
+ return true;
38
+ };
39
+ if (this.immediateBudget > 0) {
40
+ this.immediateBudget--;
41
+ if (!this.resetScheduled) {
42
+ this.resetScheduled = true;
43
+ setImmediate(() => { this.immediateBudget = 32; this.resetScheduled = false; });
44
+ }
45
+ if (check())
46
+ return;
47
+ }
48
+ if (signal.aborted) {
49
+ abort();
50
+ return;
51
+ }
52
+ signal.addEventListener("abort", abort, { once: true });
53
+ this.pending.add(check);
54
+ this.schedule();
55
+ });
56
+ }
57
+ schedule() {
58
+ if (!this.timer && this.pending.size)
59
+ this.timer = setTimeout(() => this.tick(), 10);
60
+ }
61
+ tick() {
62
+ this.timer = undefined;
63
+ const deadline = performance.now() + 4;
64
+ // Rotate pending work so the same slow requests cannot monopolize each tick.
65
+ for (const check of [...this.pending].slice(0, 32)) {
66
+ this.pending.delete(check);
67
+ if (!check())
68
+ this.pending.add(check);
69
+ if (performance.now() >= deadline)
70
+ break;
71
+ }
72
+ this.schedule();
73
+ }
74
+ }
75
+ exports.scheduler = new Scheduler();
@@ -0,0 +1,101 @@
1
+ import { Cookies } from "./cookies.js";
2
+ import { Response, StreamResponse } from "./response.js";
3
+ import { type Timeout } from "./timeout.js";
4
+ import { type UploadBody } from "./upload.js";
5
+ import { DuplexExchange } from "./duplex.js";
6
+ import { SSEStream, type SSEOptions } from "./sse.js";
7
+ import { type QueryParams, type JSONValue, type FormFields, type MultipartFiles } from "./request-options.js";
8
+ import { WebSocket, type WebSocketOptions } from "./websocket.js";
9
+ export interface RequestOptions {
10
+ /** Undefined inherits the session proxy; null or an empty string connects directly. */
11
+ proxy?: string | null;
12
+ /** Undefined inherits proxy trust; null uses system roots. Only HTTPS proxies use this file. */
13
+ proxyCaFile?: string | null;
14
+ profile?: string;
15
+ headers?: Record<string, string>;
16
+ trailers?: Record<string, string | readonly string[]>;
17
+ /** Total deadline including response body consumption, in milliseconds. */
18
+ timeoutMs?: Timeout;
19
+ allowRedirects?: boolean;
20
+ body?: UploadBody;
21
+ params?: QueryParams;
22
+ json?: JSONValue;
23
+ form?: FormFields;
24
+ files?: MultipartFiles;
25
+ decodeContent?: boolean;
26
+ maxDecodedBytes?: number;
27
+ signal?: AbortSignal;
28
+ /** Explicit HTTP/2 simultaneous upload/download. Redirects are disabled. */
29
+ duplex?: boolean;
30
+ }
31
+ export type DuplexOptions = Omit<RequestOptions, "body" | "duplex" | "json" | "form" | "files">;
32
+ export interface SessionOptions {
33
+ /** HTTP/HTTPS CONNECT, socks5 (local DNS), or socks5h (proxy DNS) URL. */
34
+ proxy?: string | null;
35
+ /** Private roots for proxy TLS, independent of the target's caFile. */
36
+ proxyCaFile?: string | null;
37
+ notifications?: "auto" | "required" | "poll";
38
+ profile?: string;
39
+ headers?: Record<string, string>;
40
+ timeoutMs?: Timeout;
41
+ library?: string;
42
+ /** Private CA roots replacing system roots for this session only. */
43
+ caFile?: string;
44
+ cookies?: boolean;
45
+ decodeContent?: boolean;
46
+ maxDecodedBytes?: number;
47
+ }
48
+ export interface Stats {
49
+ notifiers?: number;
50
+ open_requests: number;
51
+ open_sessions: number;
52
+ session_pools: number;
53
+ workers: number;
54
+ buffered_body_bytes: number;
55
+ buffered_upload_bytes: number;
56
+ goroutines: number;
57
+ heap_alloc_bytes: number;
58
+ heap_inuse_bytes: number;
59
+ buffered_ws_write_bytes?: number;
60
+ }
61
+ export declare class Session {
62
+ /** Construct and initialize; failed initialization closes the new session. */
63
+ static open(options?: SessionOptions): Promise<Session>;
64
+ readonly notificationMode: "async-events" | "poll";
65
+ readonly cookies: Cookies;
66
+ private readonly native;
67
+ private readonly handle;
68
+ private readonly profile;
69
+ private readonly proxy;
70
+ private readonly proxyCaPem;
71
+ private readonly headers;
72
+ private readonly timeouts;
73
+ private readonly decodeContent;
74
+ private readonly maxDecodedBytes;
75
+ private readonly caPem?;
76
+ private readonly active;
77
+ private readonly websockets;
78
+ private readonly producers;
79
+ private closed;
80
+ private notifier?;
81
+ private setup?;
82
+ constructor(options?: SessionOptions);
83
+ private ensureOpen;
84
+ /** Initialize the shared wakeup channel before any HTTP request is allocated. */
85
+ ready(signal?: AbortSignal): Promise<void>;
86
+ private begin;
87
+ stream(method: string, url: string, options?: RequestOptions): Promise<StreamResponse>;
88
+ sse(method: string, url: string, options?: SSEOptions): Promise<SSEStream>;
89
+ request(method: string, url: string, options?: RequestOptions): Promise<Response>;
90
+ /** Open a manual exchange; send() and response reads can run concurrently. */
91
+ duplex(method: string, url: string, options?: DuplexOptions): DuplexExchange;
92
+ get(url: string, options?: RequestOptions): Promise<Response>;
93
+ post(url: string, options?: RequestOptions): Promise<Response>;
94
+ websocket(url: string, options?: WebSocketOptions): Promise<WebSocket>;
95
+ listProfiles(): string[];
96
+ stats(): Stats;
97
+ closeIdleConnections(): void;
98
+ close(): void;
99
+ /** Release native resources immediately, then wait for cooperative upload cleanup. */
100
+ closeAsync(): Promise<void>;
101
+ }