@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Socket } from "node:net";
|
|
2
|
+
import { type Native } from "./native.js";
|
|
3
|
+
type State = {
|
|
4
|
+
generation: number;
|
|
5
|
+
waiters: Set<() => void>;
|
|
6
|
+
};
|
|
7
|
+
/** One authenticated wakeup socket per session; bytes and state stay in the ABI. */
|
|
8
|
+
export declare class Notifier {
|
|
9
|
+
private readonly native;
|
|
10
|
+
private readonly session;
|
|
11
|
+
readonly states: Map<bigint, State>;
|
|
12
|
+
readonly sockets: Set<Socket>;
|
|
13
|
+
readonly token: NonSharedBuffer;
|
|
14
|
+
private server?;
|
|
15
|
+
private writer?;
|
|
16
|
+
private registered;
|
|
17
|
+
private closed;
|
|
18
|
+
private failure?;
|
|
19
|
+
private readonly pendingClose;
|
|
20
|
+
private resolveReady;
|
|
21
|
+
private rejectReady;
|
|
22
|
+
readonly ready: Promise<void>;
|
|
23
|
+
isReady: boolean;
|
|
24
|
+
setupWaiters: number;
|
|
25
|
+
constructor(native: Native, session: bigint);
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
private accept;
|
|
28
|
+
check(): void;
|
|
29
|
+
track(handle: bigint): void;
|
|
30
|
+
generation(handle: bigint): number;
|
|
31
|
+
pulse(handle: bigint): void;
|
|
32
|
+
forget(handle: bigint): void;
|
|
33
|
+
wait(handle: bigint, generation: number, signal: AbortSignal): Promise<void>;
|
|
34
|
+
probe<T>(handle: bigint, probe: () => T | undefined, signal: AbortSignal): Promise<T>;
|
|
35
|
+
private dispose;
|
|
36
|
+
private fail;
|
|
37
|
+
close(): void;
|
|
38
|
+
closeAsync(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Notifier = void 0;
|
|
4
|
+
const node_net_1 = require("node:net");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const promises_1 = require("node:timers/promises");
|
|
7
|
+
const errors_js_1 = require("./errors.js");
|
|
8
|
+
const native_js_1 = require("./native.js");
|
|
9
|
+
let budget = 32, resetScheduled = false;
|
|
10
|
+
function fairTurn() {
|
|
11
|
+
if (!resetScheduled) {
|
|
12
|
+
resetScheduled = true;
|
|
13
|
+
setImmediate(() => { budget = 32; resetScheduled = false; });
|
|
14
|
+
}
|
|
15
|
+
return budget-- > 0 ? undefined : (0, promises_1.setImmediate)();
|
|
16
|
+
}
|
|
17
|
+
/** One authenticated wakeup socket per session; bytes and state stay in the ABI. */
|
|
18
|
+
class Notifier {
|
|
19
|
+
native;
|
|
20
|
+
session;
|
|
21
|
+
states = new Map();
|
|
22
|
+
sockets = new Set();
|
|
23
|
+
token = (0, node_crypto_1.randomBytes)(32);
|
|
24
|
+
server;
|
|
25
|
+
writer;
|
|
26
|
+
registered = false;
|
|
27
|
+
closed = false;
|
|
28
|
+
failure;
|
|
29
|
+
pendingClose = new Set();
|
|
30
|
+
resolveReady;
|
|
31
|
+
rejectReady;
|
|
32
|
+
ready = new Promise((resolve, reject) => { this.resolveReady = resolve; this.rejectReady = reject; });
|
|
33
|
+
isReady = false;
|
|
34
|
+
setupWaiters = 0;
|
|
35
|
+
constructor(native, session) {
|
|
36
|
+
this.native = native;
|
|
37
|
+
this.session = session;
|
|
38
|
+
void this.ready.catch(() => { });
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
const timeout = setTimeout(() => {
|
|
42
|
+
try {
|
|
43
|
+
if (this.registered)
|
|
44
|
+
this.native.call("session_events", this.session);
|
|
45
|
+
this.fail(new errors_js_1.FastlsError("could not start native notifications within 3 seconds"));
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause }));
|
|
49
|
+
}
|
|
50
|
+
}, 3000);
|
|
51
|
+
void this.ready.then(() => clearTimeout(timeout), () => clearTimeout(timeout));
|
|
52
|
+
try {
|
|
53
|
+
const server = this.server = (0, node_net_1.createServer)(socket => this.accept(socket));
|
|
54
|
+
server.on("error", error => this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause: error })));
|
|
55
|
+
server.listen({ host: "127.0.0.1", port: 0, backlog: 2 }, () => {
|
|
56
|
+
if (this.closed || this.failure) {
|
|
57
|
+
server.close();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const address = server.address();
|
|
62
|
+
if (!address || typeof address === "string")
|
|
63
|
+
throw new errors_js_1.FastlsError("invalid notification endpoint");
|
|
64
|
+
this.native.call("session_watch", this.session, address.port, this.token, this.token.length);
|
|
65
|
+
this.registered = true;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause: error }));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause: error }));
|
|
74
|
+
}
|
|
75
|
+
return this.ready;
|
|
76
|
+
}
|
|
77
|
+
accept(socket) {
|
|
78
|
+
socket.on("error", error => { if (socket === this.writer)
|
|
79
|
+
this.fail(new errors_js_1.FastlsError("native notification connection failed", { cause: error })); });
|
|
80
|
+
if (this.closed || this.failure || this.writer || this.sockets.size >= 2) {
|
|
81
|
+
socket.destroy();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.sockets.add(socket);
|
|
85
|
+
const closing = new Promise(resolve => socket.once("close", () => { this.sockets.delete(socket); resolve(); if (socket === this.writer && !this.closed && !this.failure)
|
|
86
|
+
this.fail(new errors_js_1.FastlsError("native notification connection closed")); }));
|
|
87
|
+
this.pendingClose.add(closing);
|
|
88
|
+
void closing.then(() => this.pendingClose.delete(closing));
|
|
89
|
+
const handshake = Buffer.alloc(32);
|
|
90
|
+
let length = 0;
|
|
91
|
+
socket.setTimeout(1000, () => socket.destroy());
|
|
92
|
+
socket.on("data", chunk => {
|
|
93
|
+
try {
|
|
94
|
+
if (this.closed || this.failure)
|
|
95
|
+
return;
|
|
96
|
+
if (socket !== this.writer) {
|
|
97
|
+
const take = Math.min(32 - length, chunk.length);
|
|
98
|
+
chunk.copy(handshake, length, 0, take);
|
|
99
|
+
length += take;
|
|
100
|
+
chunk = chunk.subarray(take);
|
|
101
|
+
if (length !== 32)
|
|
102
|
+
return;
|
|
103
|
+
if (!(0, node_crypto_1.timingSafeEqual)(handshake, this.token) || this.writer) {
|
|
104
|
+
socket.destroy();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.writer = socket;
|
|
108
|
+
socket.setTimeout(0);
|
|
109
|
+
this.server?.close();
|
|
110
|
+
for (const candidate of this.sockets)
|
|
111
|
+
if (candidate !== socket)
|
|
112
|
+
candidate.destroy();
|
|
113
|
+
this.isReady = true;
|
|
114
|
+
this.resolveReady();
|
|
115
|
+
}
|
|
116
|
+
if (!chunk.length)
|
|
117
|
+
return;
|
|
118
|
+
if (chunk.some(byte => byte !== 1))
|
|
119
|
+
throw new errors_js_1.FastlsError("invalid native notification packet");
|
|
120
|
+
const batch = this.native.call("session_events", this.session);
|
|
121
|
+
if (!Array.isArray(batch.handles) || batch.handles.length > 256 || typeof batch.overflow !== "boolean")
|
|
122
|
+
throw new errors_js_1.FastlsError("invalid native notification batch");
|
|
123
|
+
for (const handle of batch.overflow ? [...this.states.keys()] : batch.handles.map(native_js_1.asHandle))
|
|
124
|
+
this.pulse(handle);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.fail(new errors_js_1.FastlsError("native notifications failed", { cause: error }));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
check() {
|
|
132
|
+
if (this.failure)
|
|
133
|
+
throw this.failure;
|
|
134
|
+
if (this.closed)
|
|
135
|
+
throw new errors_js_1.FastlsError("notification session is closed");
|
|
136
|
+
}
|
|
137
|
+
track(handle) { this.check(); this.states.set(handle, { generation: 0, waiters: new Set() }); }
|
|
138
|
+
generation(handle) { this.check(); return this.states.get(handle)?.generation ?? 0; }
|
|
139
|
+
pulse(handle) {
|
|
140
|
+
const state = this.states.get(handle);
|
|
141
|
+
if (!state)
|
|
142
|
+
return;
|
|
143
|
+
state.generation++;
|
|
144
|
+
for (const wake of [...state.waiters])
|
|
145
|
+
wake();
|
|
146
|
+
}
|
|
147
|
+
forget(handle) { const state = this.states.get(handle); this.states.delete(handle); if (state)
|
|
148
|
+
for (const wake of [...state.waiters])
|
|
149
|
+
wake(); }
|
|
150
|
+
wait(handle, generation, signal) {
|
|
151
|
+
this.check();
|
|
152
|
+
if (signal.aborted)
|
|
153
|
+
return Promise.reject(signal.reason);
|
|
154
|
+
const state = this.states.get(handle);
|
|
155
|
+
if (!state || state.generation !== generation)
|
|
156
|
+
return Promise.resolve();
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const clean = () => { state.waiters.delete(wake); signal.removeEventListener("abort", abort); };
|
|
159
|
+
const wake = () => { clean(); try {
|
|
160
|
+
this.check();
|
|
161
|
+
resolve();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
reject(error);
|
|
165
|
+
} };
|
|
166
|
+
const abort = () => { clean(); reject(signal.reason); };
|
|
167
|
+
state.waiters.add(wake);
|
|
168
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async probe(handle, probe, signal) {
|
|
172
|
+
while (true) {
|
|
173
|
+
const pause = fairTurn();
|
|
174
|
+
if (pause)
|
|
175
|
+
await pause;
|
|
176
|
+
this.check();
|
|
177
|
+
if (signal.aborted)
|
|
178
|
+
throw signal.reason;
|
|
179
|
+
const generation = this.generation(handle), value = probe();
|
|
180
|
+
if (value !== undefined)
|
|
181
|
+
return value;
|
|
182
|
+
await this.wait(handle, generation, signal);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
dispose() {
|
|
186
|
+
if (this.registered) {
|
|
187
|
+
this.registered = false;
|
|
188
|
+
this.native.call("session_unwatch", this.session);
|
|
189
|
+
}
|
|
190
|
+
if (this.server?.listening)
|
|
191
|
+
this.server.close();
|
|
192
|
+
for (const socket of this.sockets)
|
|
193
|
+
socket.destroy();
|
|
194
|
+
}
|
|
195
|
+
fail(error) {
|
|
196
|
+
if (this.closed || this.failure)
|
|
197
|
+
return;
|
|
198
|
+
this.failure = error;
|
|
199
|
+
this.rejectReady(error);
|
|
200
|
+
for (const handle of this.states.keys())
|
|
201
|
+
this.pulse(handle);
|
|
202
|
+
// A failing ABI cleanup must not throw out of a socket event callback.
|
|
203
|
+
try {
|
|
204
|
+
this.dispose();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
for (const socket of this.sockets)
|
|
208
|
+
socket.destroy();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
close() {
|
|
212
|
+
if (this.closed)
|
|
213
|
+
return;
|
|
214
|
+
this.closed = true;
|
|
215
|
+
this.rejectReady(new errors_js_1.FastlsError("notification session is closed"));
|
|
216
|
+
for (const handle of [...this.states.keys()])
|
|
217
|
+
this.forget(handle);
|
|
218
|
+
this.dispose();
|
|
219
|
+
}
|
|
220
|
+
async closeAsync() { this.close(); await Promise.all([...this.pendingClose]); }
|
|
221
|
+
}
|
|
222
|
+
exports.Notifier = Notifier;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { UploadBody } from "./upload.js";
|
|
2
|
+
export type QueryScalar = string | Uint8Array | number | boolean | null | undefined;
|
|
3
|
+
export type FormFields = Record<string, QueryScalar | readonly QueryScalar[]> | ReadonlyMap<string, QueryScalar | readonly QueryScalar[]> | readonly (readonly [string | Uint8Array, QueryScalar | readonly QueryScalar[]])[];
|
|
4
|
+
export type QueryParams = FormFields | string;
|
|
5
|
+
export type JSONValue = null | boolean | number | string | JSONValue[] | {
|
|
6
|
+
[key: string]: JSONValue;
|
|
7
|
+
};
|
|
8
|
+
/** Caller-owned binary reader. Return an empty array at EOF; never more than maxBytes. */
|
|
9
|
+
export interface MultipartReader {
|
|
10
|
+
name?: string;
|
|
11
|
+
read(maxBytes: number, signal: AbortSignal): Uint8Array | Promise<Uint8Array>;
|
|
12
|
+
}
|
|
13
|
+
export type MultipartBody = Uint8Array | MultipartReader;
|
|
14
|
+
export type MultipartFile = MultipartBody | readonly [string | null, MultipartBody, string?];
|
|
15
|
+
export type MultipartFiles = Record<string, MultipartFile> | ReadonlyMap<string, MultipartFile> | readonly (readonly [string, MultipartFile])[];
|
|
16
|
+
export interface EncodingOptions {
|
|
17
|
+
params?: QueryParams;
|
|
18
|
+
json?: JSONValue;
|
|
19
|
+
form?: FormFields;
|
|
20
|
+
files?: MultipartFiles;
|
|
21
|
+
body?: UploadBody;
|
|
22
|
+
duplex?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Called before native handle allocation, with headers already merged. No file reads. */
|
|
25
|
+
export declare function prepare(url: string, options: EncodingOptions, headers: Record<string, string>): {
|
|
26
|
+
url: string;
|
|
27
|
+
body: UploadBody | undefined;
|
|
28
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepare = prepare;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const upload_js_1 = require("./upload.js");
|
|
7
|
+
function pairs(value, name) {
|
|
8
|
+
if (value instanceof Map)
|
|
9
|
+
return [...value];
|
|
10
|
+
if (Array.isArray(value) && value.every(p => Array.isArray(p) && p.length === 2))
|
|
11
|
+
return value;
|
|
12
|
+
if (value && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null))
|
|
13
|
+
return Object.entries(value);
|
|
14
|
+
throw new TypeError(`${name} must be an object, Map or array of pairs`);
|
|
15
|
+
}
|
|
16
|
+
function scalar(value) {
|
|
17
|
+
if (typeof value === "string" || value instanceof Uint8Array)
|
|
18
|
+
return value;
|
|
19
|
+
if (typeof value === "boolean")
|
|
20
|
+
return value ? "True" : "False";
|
|
21
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
22
|
+
return String(value);
|
|
23
|
+
throw new TypeError("form/query values must be strings, bytes, finite numbers or booleans");
|
|
24
|
+
}
|
|
25
|
+
function values(value, name) {
|
|
26
|
+
const result = [];
|
|
27
|
+
for (const [key, item] of pairs(value, name)) {
|
|
28
|
+
if (typeof key !== "string" && !(key instanceof Uint8Array))
|
|
29
|
+
throw new TypeError(`${name} names must be strings or bytes`);
|
|
30
|
+
for (const part of Array.isArray(item) ? item : [item])
|
|
31
|
+
if (part !== null && part !== undefined)
|
|
32
|
+
result.push([key, scalar(part)]);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function quote(value) {
|
|
37
|
+
const bytes = typeof value === "string" ? Buffer.from(value) : value;
|
|
38
|
+
let encoded = "";
|
|
39
|
+
for (const b of bytes)
|
|
40
|
+
encoded += b === 32 ? "+" : (b >= 65 && b <= 90) || (b >= 97 && b <= 122) || (b >= 48 && b <= 57) || [45, 46, 95, 126].includes(b) ? String.fromCharCode(b) : `%${b.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
41
|
+
return encoded;
|
|
42
|
+
}
|
|
43
|
+
function form(value) { return values(value, "form").map(([k, v]) => `${quote(k)}=${quote(v)}`).join("&"); }
|
|
44
|
+
function metadata(value) {
|
|
45
|
+
if (typeof value !== "string" || /[\x00-\x1f\x7f]/.test(value))
|
|
46
|
+
throw new TypeError("multipart metadata must be strings without control characters");
|
|
47
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', "%22");
|
|
48
|
+
}
|
|
49
|
+
function validateJSON(value, stack = new Set()) {
|
|
50
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)))
|
|
51
|
+
return;
|
|
52
|
+
if (!value || typeof value !== "object" || (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null))
|
|
53
|
+
throw new TypeError("json must contain only JSON values and finite numbers");
|
|
54
|
+
if (stack.has(value))
|
|
55
|
+
throw new TypeError("json must not contain cycles");
|
|
56
|
+
if ("toJSON" in value)
|
|
57
|
+
throw new TypeError("json does not support toJSON hooks");
|
|
58
|
+
stack.add(value);
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
for (let i = 0; i < value.length; i++)
|
|
61
|
+
validateJSON(value[i], stack);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
for (const entry of Object.values(value))
|
|
65
|
+
validateJSON(entry, stack);
|
|
66
|
+
}
|
|
67
|
+
stack.delete(value);
|
|
68
|
+
}
|
|
69
|
+
class Multipart {
|
|
70
|
+
boundary = "fastls-" + (0, node_crypto_1.randomBytes)(24).toString("hex");
|
|
71
|
+
parts = [];
|
|
72
|
+
used = false;
|
|
73
|
+
constructor(fields, files) {
|
|
74
|
+
for (const [key, value] of fields === undefined ? [] : values(fields, "form")) {
|
|
75
|
+
const name = typeof key === "string" ? key : new TextDecoder("utf-8", { fatal: true }).decode(key);
|
|
76
|
+
this.add(`Content-Disposition: form-data; name="${metadata(name)}"\r\n`, typeof value === "string" ? Buffer.from(value) : value);
|
|
77
|
+
}
|
|
78
|
+
for (const [key, spec] of pairs(files, "files")) {
|
|
79
|
+
let filename, body, kind;
|
|
80
|
+
if (Array.isArray(spec)) {
|
|
81
|
+
if (spec.length !== 2 && spec.length !== 3)
|
|
82
|
+
throw new TypeError("files tuples must be [filename, body, contentType?]");
|
|
83
|
+
[filename, body, kind] = spec;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
body = spec;
|
|
87
|
+
filename = (0, node_path_1.basename)(String(body?.name ?? key).replaceAll("\\", "/"));
|
|
88
|
+
}
|
|
89
|
+
if (!(body instanceof Uint8Array) && (!body || typeof body.read !== "function"))
|
|
90
|
+
throw new TypeError("files bodies must be bytes or binary readers");
|
|
91
|
+
let header = `Content-Disposition: form-data; name="${metadata(key)}"`;
|
|
92
|
+
if (filename !== null)
|
|
93
|
+
header += `; filename="${metadata(filename)}"`;
|
|
94
|
+
// Stable cross-platform subset; callers supply other media types explicitly.
|
|
95
|
+
const extension = typeof filename === "string" ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
|
|
96
|
+
kind ??= { ".txt": "text/plain", ".json": "application/json", ".html": "text/html", ".csv": "text/csv", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".pdf": "application/pdf" }[extension] ?? "application/octet-stream";
|
|
97
|
+
metadata(kind);
|
|
98
|
+
this.add(header + `\r\nContent-Type: ${kind}\r\n`, body);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
add(header, body) { this.parts.push({ header: Buffer.from(`--${this.boundary}\r\n${header}\r\n`), body }); }
|
|
102
|
+
async *chunks(signal) {
|
|
103
|
+
if (this.used)
|
|
104
|
+
throw new TypeError("multipart bodies cannot be replayed");
|
|
105
|
+
this.used = true;
|
|
106
|
+
for (const { header, body } of this.parts) {
|
|
107
|
+
signal.throwIfAborted();
|
|
108
|
+
yield header;
|
|
109
|
+
if (body instanceof Uint8Array) {
|
|
110
|
+
for (let offset = 0; offset < body.byteLength; offset += 65536)
|
|
111
|
+
yield body.subarray(offset, offset + 65536);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
while (true) {
|
|
115
|
+
signal.throwIfAborted();
|
|
116
|
+
const block = await body.read(65536, signal);
|
|
117
|
+
if (!(block instanceof Uint8Array) || block.byteLength > 65536)
|
|
118
|
+
throw new TypeError("multipart readers must return at most 64KiB of bytes");
|
|
119
|
+
if (!block.byteLength)
|
|
120
|
+
break;
|
|
121
|
+
yield block;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
yield Buffer.from("\r\n");
|
|
125
|
+
}
|
|
126
|
+
const terminal = Buffer.from(`--${this.boundary}--\r\n`);
|
|
127
|
+
upload_js_1.terminalUploadChunks.add(terminal);
|
|
128
|
+
yield terminal;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Called before native handle allocation, with headers already merged. No file reads. */
|
|
132
|
+
function prepare(url, options, headers) {
|
|
133
|
+
const hasJSON = Object.hasOwn(options, "json");
|
|
134
|
+
if (hasJSON && (options.body !== undefined || options.form !== undefined || options.files !== undefined))
|
|
135
|
+
throw new TypeError("json cannot be combined with body, form or files");
|
|
136
|
+
if (options.body !== undefined && (options.form !== undefined || options.files !== undefined))
|
|
137
|
+
throw new TypeError("body cannot be combined with form or files");
|
|
138
|
+
if (options.params !== undefined) {
|
|
139
|
+
const query = typeof options.params === "string" ? options.params : form(options.params);
|
|
140
|
+
if (/[^\x00-\x7f]|#/.test(query))
|
|
141
|
+
throw new TypeError("raw params must be an ASCII query without a fragment");
|
|
142
|
+
if (query) {
|
|
143
|
+
const fragment = url.indexOf("#"), base = fragment < 0 ? url : url.slice(0, fragment), hash = fragment < 0 ? "" : url.slice(fragment);
|
|
144
|
+
url = base + (base.includes("?") ? base.endsWith("?") ? "" : "&" : "?") + query + hash;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let body = options.body, kind;
|
|
148
|
+
if (options.files !== undefined) {
|
|
149
|
+
if (options.duplex)
|
|
150
|
+
throw new TypeError("multipart cannot be combined with duplex");
|
|
151
|
+
if ("content-type" in headers)
|
|
152
|
+
throw new TypeError("multipart manages Content-Type and its boundary");
|
|
153
|
+
const multipart = new Multipart(options.form, options.files);
|
|
154
|
+
body = signal => multipart.chunks(signal);
|
|
155
|
+
kind = `multipart/form-data; boundary=${multipart.boundary}`;
|
|
156
|
+
}
|
|
157
|
+
else if (hasJSON) {
|
|
158
|
+
validateJSON(options.json);
|
|
159
|
+
body = JSON.stringify(options.json);
|
|
160
|
+
kind = "application/json";
|
|
161
|
+
}
|
|
162
|
+
else if (options.form !== undefined) {
|
|
163
|
+
body = form(options.form);
|
|
164
|
+
kind = "application/x-www-form-urlencoded";
|
|
165
|
+
}
|
|
166
|
+
if (kind) {
|
|
167
|
+
if ("content-length" in headers || "transfer-encoding" in headers)
|
|
168
|
+
throw new TypeError("encoded bodies manage Content-Length and Transfer-Encoding");
|
|
169
|
+
headers["content-type"] ??= kind;
|
|
170
|
+
}
|
|
171
|
+
return { url, body };
|
|
172
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Headers } from "./headers.js";
|
|
2
|
+
import { Native, type NativeInfo } from "./native.js";
|
|
3
|
+
import type { Notifier } from "./notifications.js";
|
|
4
|
+
export declare const MAX_BUFFERED_BODY: number;
|
|
5
|
+
export interface UploadLifecycle {
|
|
6
|
+
done: Promise<void>;
|
|
7
|
+
cleanup: Promise<void>;
|
|
8
|
+
complete(): boolean;
|
|
9
|
+
check(): void;
|
|
10
|
+
}
|
|
11
|
+
/** Owns one native request and all cancellation hooks. Internal to the SDK. */
|
|
12
|
+
export declare class Transfer {
|
|
13
|
+
private readonly native;
|
|
14
|
+
private readonly handle;
|
|
15
|
+
private readonly release;
|
|
16
|
+
private readonly external?;
|
|
17
|
+
readonly duplex: boolean;
|
|
18
|
+
private readonly notifier?;
|
|
19
|
+
private readonly controller;
|
|
20
|
+
private closed;
|
|
21
|
+
private failure?;
|
|
22
|
+
private timer?;
|
|
23
|
+
private deadline?;
|
|
24
|
+
private readonly externalAbort;
|
|
25
|
+
constructor(native: Native, handle: bigint, release: () => void, external?: AbortSignal | undefined, duplex?: boolean, notifier?: Notifier | undefined);
|
|
26
|
+
private wait;
|
|
27
|
+
attachSignal(total?: number, started?: number): void;
|
|
28
|
+
get signal(): AbortSignal;
|
|
29
|
+
info(): NativeInfo;
|
|
30
|
+
write(buffer: Uint8Array, end: boolean, signal: AbortSignal): Promise<number>;
|
|
31
|
+
canWrite(): boolean;
|
|
32
|
+
throwFailure(): void;
|
|
33
|
+
headers(): Promise<NativeInfo>;
|
|
34
|
+
next(): Promise<Buffer | null>;
|
|
35
|
+
ensureOpen(): void;
|
|
36
|
+
close(error?: Error): void;
|
|
37
|
+
}
|
|
38
|
+
export declare class Response {
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly headers: Headers;
|
|
41
|
+
readonly match: Readonly<Record<string, unknown>>;
|
|
42
|
+
readonly content: Buffer;
|
|
43
|
+
readonly wireHeaders: Headers;
|
|
44
|
+
readonly decoded: boolean;
|
|
45
|
+
readonly httpVersion: string;
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly trailers: Headers;
|
|
48
|
+
encoding: string;
|
|
49
|
+
constructor(status: number, headers: Headers, match: Readonly<Record<string, unknown>>, content: Buffer, wireHeaders?: Headers, decoded?: boolean, httpVersion?: string, url?: string, trailers?: Headers);
|
|
50
|
+
get ok(): boolean;
|
|
51
|
+
raiseForStatus(): void;
|
|
52
|
+
text(): string;
|
|
53
|
+
json<T = unknown>(): T;
|
|
54
|
+
}
|
|
55
|
+
export declare class StreamResponse {
|
|
56
|
+
private readonly transfer;
|
|
57
|
+
private readonly upload?;
|
|
58
|
+
encoding: string;
|
|
59
|
+
httpVersion: string;
|
|
60
|
+
url: string;
|
|
61
|
+
trailers: Headers;
|
|
62
|
+
readonly status: number;
|
|
63
|
+
readonly headers: Headers;
|
|
64
|
+
readonly match: Readonly<Record<string, unknown>>;
|
|
65
|
+
readonly wireHeaders: Headers;
|
|
66
|
+
readonly decoded: boolean;
|
|
67
|
+
readonly body: AsyncIterable<Buffer>;
|
|
68
|
+
private consumed;
|
|
69
|
+
constructor(transfer: Transfer, info: NativeInfo, upload?: UploadLifecycle | undefined);
|
|
70
|
+
get ok(): boolean;
|
|
71
|
+
raiseForStatus(): void;
|
|
72
|
+
/** @internal Check cancellation even when a parser still holds buffered data. */
|
|
73
|
+
ensureOpen(): void;
|
|
74
|
+
private iterate;
|
|
75
|
+
read(): Promise<Buffer>;
|
|
76
|
+
text(): Promise<string>;
|
|
77
|
+
json<T = unknown>(): Promise<T>;
|
|
78
|
+
/** Consume the response concurrently: waiting alone can block on HTTP/2 flow control. */
|
|
79
|
+
waitUpload(): Promise<void>;
|
|
80
|
+
cancel(): void;
|
|
81
|
+
close(): void;
|
|
82
|
+
/** Wait for cooperative producer finalization after releasing the native request. */
|
|
83
|
+
closeAsync(): Promise<void>;
|
|
84
|
+
}
|