@passcod/faith 0.0.2
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/README.md +547 -0
- package/index.d.ts +109 -0
- package/index.js +585 -0
- package/package.json +76 -0
- package/wrapper.d.ts +119 -0
- package/wrapper.js +320 -0
package/wrapper.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Faith Fetch API Wrapper TypeScript Definitions
|
|
3
|
+
*
|
|
4
|
+
* This provides TypeScript definitions for the spec-compliant Fetch API wrapper.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
Agent,
|
|
9
|
+
AgentOptions,
|
|
10
|
+
AgentStats,
|
|
11
|
+
Header,
|
|
12
|
+
FAITH_VERSION,
|
|
13
|
+
REQWEST_VERSION,
|
|
14
|
+
USER_AGENT,
|
|
15
|
+
} from "./index";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Error codes const enum
|
|
19
|
+
*
|
|
20
|
+
* NOTE: This must be kept in sync with FaithErrorKind in src/error.rs
|
|
21
|
+
* Run `npm test` to validate sync (test/error-codes.test.js checks this)
|
|
22
|
+
*/
|
|
23
|
+
export const ERROR_CODES: {
|
|
24
|
+
readonly Aborted: "Aborted";
|
|
25
|
+
readonly BodyStream: "BodyStream";
|
|
26
|
+
readonly InvalidHeader: "InvalidHeader";
|
|
27
|
+
readonly InvalidMethod: "InvalidMethod";
|
|
28
|
+
readonly InvalidUrl: "InvalidUrl";
|
|
29
|
+
readonly JsonParse: "JsonParse";
|
|
30
|
+
readonly Network: "Network";
|
|
31
|
+
readonly ResponseAlreadyDisturbed: "ResponseAlreadyDisturbed";
|
|
32
|
+
readonly ResponseBodyNotAvailable: "ResponseBodyNotAvailable";
|
|
33
|
+
readonly RuntimeThread: "RuntimeThread";
|
|
34
|
+
readonly Timeout: "Timeout";
|
|
35
|
+
readonly Utf8Parse: "Utf8Parse";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface FetchOptions {
|
|
39
|
+
method?: string;
|
|
40
|
+
headers?: Record<string, string> | Headers;
|
|
41
|
+
body?: string | Buffer | Uint8Array | Array<number> | ArrayBuffer;
|
|
42
|
+
timeout?: number; // milliseconds
|
|
43
|
+
credentials?: "omit" | "same-origin" | "include";
|
|
44
|
+
duplex?: "half";
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
agent?: Agent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Response {
|
|
50
|
+
readonly bodyUsed: boolean;
|
|
51
|
+
readonly headers: Headers;
|
|
52
|
+
readonly ok: boolean;
|
|
53
|
+
readonly redirected: boolean;
|
|
54
|
+
readonly status: number;
|
|
55
|
+
readonly statusText: string;
|
|
56
|
+
readonly url: string;
|
|
57
|
+
readonly version: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the response body as a ReadableStream
|
|
61
|
+
* This is a getter to match the Fetch API spec
|
|
62
|
+
*/
|
|
63
|
+
readonly body: ReadableStream<Uint8Array> | null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert response body to text (UTF-8)
|
|
67
|
+
* @returns Promise that resolves with the response body as text
|
|
68
|
+
*/
|
|
69
|
+
text(): Promise<string>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get response body as bytes
|
|
73
|
+
* @returns Promise that resolves with the response body as Uint8Array
|
|
74
|
+
*/
|
|
75
|
+
bytes(): Promise<Uint8Array>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get response body as ArrayBuffer
|
|
79
|
+
* @returns Promise that resolves with the response body as ArrayBuffer
|
|
80
|
+
*/
|
|
81
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse response body as JSON
|
|
85
|
+
* @returns Promise that resolves with the parsed JSON data
|
|
86
|
+
*/
|
|
87
|
+
json(): Promise<any>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get response body as Blob
|
|
91
|
+
* @returns Promise that resolves with the response body as Blob
|
|
92
|
+
*/
|
|
93
|
+
blob(): Promise<Blob>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a clone of the Response object
|
|
97
|
+
* @returns A new Response object with the same properties
|
|
98
|
+
* @throws If response body has already been read
|
|
99
|
+
*/
|
|
100
|
+
clone(): Response;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert to a Web API Response object
|
|
104
|
+
* @returns Web API Response object
|
|
105
|
+
* @throws If response body has been disturbed
|
|
106
|
+
*/
|
|
107
|
+
webResponse(): globalThis.Response;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetch function
|
|
112
|
+
* @param input - The URL to fetch, a Request object, URL object, or any object with a toString() method
|
|
113
|
+
* @param options - Fetch options (overrides Request properties when provided)
|
|
114
|
+
* @returns Promise that resolves with a Response
|
|
115
|
+
*/
|
|
116
|
+
export declare function fetch(
|
|
117
|
+
input: string | Request | URL | { toString(): string },
|
|
118
|
+
options?: FetchOptions,
|
|
119
|
+
): Promise<Response>;
|
package/wrapper.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Faith Fetch API Wrapper
|
|
3
|
+
*
|
|
4
|
+
* This wrapper provides a spec-compliant Fetch API interface on top of
|
|
5
|
+
* the native Rust bindings. The main difference is that `body` is exposed
|
|
6
|
+
* as a property/getter instead of a method, and the class is named `Response`
|
|
7
|
+
* instead of `FetchResponse`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const native = require("./index.js");
|
|
11
|
+
const { faithFetch } = native;
|
|
12
|
+
|
|
13
|
+
// Generate ERROR_CODES const enum from native error codes
|
|
14
|
+
// e.g. { InvalidHeader: "InvalidHeader", InvalidMethod: "InvalidMethod", ... }
|
|
15
|
+
const ERROR_CODES = native.errorCodes().reduce((acc, code) => {
|
|
16
|
+
acc[code] = code;
|
|
17
|
+
return acc;
|
|
18
|
+
}, {});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Response class that provides spec-compliant Fetch API
|
|
22
|
+
*/
|
|
23
|
+
class Response {
|
|
24
|
+
/** @type {import('./index').FaithResponse} */
|
|
25
|
+
#nativeResponse;
|
|
26
|
+
|
|
27
|
+
constructor(nativeResponse) {
|
|
28
|
+
this.#nativeResponse = nativeResponse;
|
|
29
|
+
|
|
30
|
+
// Create a Headers object from the array of header pairs
|
|
31
|
+
const headers = new Headers();
|
|
32
|
+
const headerPairs = this.#nativeResponse.headers;
|
|
33
|
+
if (Array.isArray(headerPairs)) {
|
|
34
|
+
for (const [name, value] of headerPairs) {
|
|
35
|
+
headers.append(name, value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Object.defineProperty(this, "headers", {
|
|
40
|
+
get: () => headers,
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const nativeProto = Object.getPrototypeOf(this.#nativeResponse);
|
|
46
|
+
const descriptors = Object.getOwnPropertyDescriptors(nativeProto);
|
|
47
|
+
|
|
48
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
49
|
+
if (descriptor.get && key !== "headers") {
|
|
50
|
+
Object.defineProperty(this, key, {
|
|
51
|
+
get: () => this.#nativeResponse[key],
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert response body to text (UTF-8)
|
|
61
|
+
* @returns {Promise<string>}
|
|
62
|
+
*/
|
|
63
|
+
async text() {
|
|
64
|
+
return await this.#nativeResponse.text();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get response body as bytes
|
|
69
|
+
* @returns {Promise<Uint8Array>}
|
|
70
|
+
*/
|
|
71
|
+
async bytes() {
|
|
72
|
+
return await this.#nativeResponse.bytes();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Alias for bytes() that returns ArrayBuffer
|
|
77
|
+
* @returns {Promise<ArrayBuffer>}
|
|
78
|
+
*/
|
|
79
|
+
async arrayBuffer() {
|
|
80
|
+
const buffer = await this.#nativeResponse.bytes();
|
|
81
|
+
return buffer.buffer;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse response body as JSON
|
|
86
|
+
* @returns {Promise<any>}
|
|
87
|
+
*/
|
|
88
|
+
async json() {
|
|
89
|
+
return await this.#nativeResponse.json();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get response body as Blob
|
|
94
|
+
* @returns {Promise<Blob>}
|
|
95
|
+
*/
|
|
96
|
+
async blob() {
|
|
97
|
+
const bytes = await this.#nativeResponse.bytes();
|
|
98
|
+
const contentType = this.headers.get("content-type") || "";
|
|
99
|
+
return new Blob([bytes], { type: contentType });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a clone of the Response object
|
|
104
|
+
* @returns {Response} A new Response object with the same properties
|
|
105
|
+
* @throws {Error} If response body has already been read
|
|
106
|
+
*/
|
|
107
|
+
clone() {
|
|
108
|
+
return new Response(this.#nativeResponse.clone());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert to a Web API Response object
|
|
113
|
+
* @returns {Response} Web API Response object
|
|
114
|
+
* @throws {Error} If response body has been disturbed or Response constructor is not available
|
|
115
|
+
*/
|
|
116
|
+
webResponse() {
|
|
117
|
+
// Check if Web API Response constructor is available
|
|
118
|
+
if (typeof globalThis.Response !== "function") {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Web API Response constructor not available in this environment",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Create and return a Web API Response object
|
|
125
|
+
return new globalThis.Response(this.body, {
|
|
126
|
+
status: this.status,
|
|
127
|
+
statusText: this.statusText,
|
|
128
|
+
headers: this.headers,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let defaultAgent;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetch function wrapper
|
|
137
|
+
* @param {string|Request|URL|Object} input - The URL to fetch, a Request object, or an object with toString()
|
|
138
|
+
* @param {Object} [options] - Fetch options (when input is a Request, options override Request properties)
|
|
139
|
+
* @param {string} [options.method] - HTTP method
|
|
140
|
+
* @param {Object|Headers} [options.headers] - HTTP headers (Headers object or plain object with {name: value} pairs).
|
|
141
|
+
* Converted to array of tuples for the native binding.
|
|
142
|
+
* @param {Buffer|Array<number>|string|ArrayBuffer|Uint8Array|ReadableStream} [options.body] - Request body
|
|
143
|
+
* @param {number} [options.timeout] - Timeout in milliseconds
|
|
144
|
+
* @param {string} [options.credentials] - Credentials mode: "omit", "same-origin", or "include" (default)
|
|
145
|
+
* @param {string} [options.duplex] - Duplex mode: "half" (required when body is a ReadableStream)
|
|
146
|
+
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request
|
|
147
|
+
* @returns {Promise<Response>}
|
|
148
|
+
*
|
|
149
|
+
* When a Request object is provided, all its properties (method, headers, body, mode, credentials,
|
|
150
|
+
* cache, redirect, referrer, integrity, etc.) are extracted and passed to the native binding.
|
|
151
|
+
* The options parameter can override any Request property.
|
|
152
|
+
*
|
|
153
|
+
* Objects with a toString() method (like URL objects) will have toString() called to get the URL string.
|
|
154
|
+
*
|
|
155
|
+
* Headers handling:
|
|
156
|
+
* - Headers object: converted to array of [name, value] pairs
|
|
157
|
+
* - Plain object: entries converted to array of [name, value] pairs
|
|
158
|
+
* - null/undefined: treated as no headers
|
|
159
|
+
* - Invalid types: throws TypeError
|
|
160
|
+
*/
|
|
161
|
+
async function fetch(input, options = {}) {
|
|
162
|
+
let url;
|
|
163
|
+
let nativeOptions;
|
|
164
|
+
|
|
165
|
+
// Handle Request object as input
|
|
166
|
+
if (
|
|
167
|
+
typeof input === "object" &&
|
|
168
|
+
input !== null &&
|
|
169
|
+
typeof input.url === "string"
|
|
170
|
+
) {
|
|
171
|
+
// Extract url separately
|
|
172
|
+
url = input.url;
|
|
173
|
+
|
|
174
|
+
// Copy all properties from Request object except url and bodyUsed
|
|
175
|
+
const requestOptions = {};
|
|
176
|
+
for (const key in input) {
|
|
177
|
+
if (key !== "url" && key !== "bodyUsed") {
|
|
178
|
+
const value = input[key];
|
|
179
|
+
if (value !== undefined && value !== null) {
|
|
180
|
+
requestOptions[key] = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle body specially - Request.body is a ReadableStream that needs to be consumed
|
|
186
|
+
if (requestOptions.body !== undefined && requestOptions.body !== null) {
|
|
187
|
+
if (typeof input.arrayBuffer === "function") {
|
|
188
|
+
requestOptions.body = await input.arrayBuffer();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Merge Request properties with options, options take precedence
|
|
193
|
+
nativeOptions = { ...requestOptions, ...options };
|
|
194
|
+
} else if (typeof input === "string") {
|
|
195
|
+
url = input;
|
|
196
|
+
nativeOptions = { ...options };
|
|
197
|
+
} else if (input && typeof input.toString === "function") {
|
|
198
|
+
// Handle objects with toString method (like URL objects)
|
|
199
|
+
url = input.toString();
|
|
200
|
+
nativeOptions = { ...options };
|
|
201
|
+
} else {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
"First argument must be a string URL, Request object, or an object with a toString method",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Convert headers to native format
|
|
208
|
+
// This is the inverse of what Response does: Request headers go from
|
|
209
|
+
// Headers/Object -> Array<[string, string]>, while Response headers go from
|
|
210
|
+
// Array<[string, string]> -> Headers object
|
|
211
|
+
if (nativeOptions.headers !== undefined && nativeOptions.headers !== null) {
|
|
212
|
+
if (nativeOptions.headers instanceof Headers) {
|
|
213
|
+
// Convert Headers object to array of tuples
|
|
214
|
+
const headersArray = [];
|
|
215
|
+
nativeOptions.headers.forEach((value, name) => {
|
|
216
|
+
headersArray.push([name, value]);
|
|
217
|
+
});
|
|
218
|
+
nativeOptions.headers = headersArray;
|
|
219
|
+
} else if (
|
|
220
|
+
typeof nativeOptions.headers === "object" &&
|
|
221
|
+
!Array.isArray(nativeOptions.headers)
|
|
222
|
+
) {
|
|
223
|
+
// Convert plain object to array of tuples
|
|
224
|
+
const headersArray = [];
|
|
225
|
+
for (const [name, value] of Object.entries(nativeOptions.headers)) {
|
|
226
|
+
headersArray.push([name, value]);
|
|
227
|
+
}
|
|
228
|
+
nativeOptions.headers = headersArray;
|
|
229
|
+
} else {
|
|
230
|
+
throw new TypeError("headers must be a Headers object or a plain object");
|
|
231
|
+
}
|
|
232
|
+
} else if (nativeOptions.headers === null) {
|
|
233
|
+
// Convert null to undefined so Rust treats it as None
|
|
234
|
+
delete nativeOptions.headers;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Convert body to Buffer if needed
|
|
238
|
+
// Native binding handles: string, Buffer, Uint8Array
|
|
239
|
+
// We convert: ArrayBuffer, Array<number>, ReadableStream
|
|
240
|
+
// Validate ReadableStream bodies require duplex option
|
|
241
|
+
if (nativeOptions.body !== undefined && nativeOptions.body !== null) {
|
|
242
|
+
// Check if body is a ReadableStream
|
|
243
|
+
if (
|
|
244
|
+
typeof nativeOptions.body === "object" &&
|
|
245
|
+
typeof nativeOptions.body.getReader === "function"
|
|
246
|
+
) {
|
|
247
|
+
// ReadableStream body requires duplex option
|
|
248
|
+
if (!nativeOptions.duplex) {
|
|
249
|
+
throw new TypeError(
|
|
250
|
+
"RequestInit's body is a ReadableStream and duplex option is not set",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Consume the ReadableStream into a Buffer
|
|
255
|
+
const reader = nativeOptions.body.getReader();
|
|
256
|
+
const chunks = [];
|
|
257
|
+
try {
|
|
258
|
+
while (true) {
|
|
259
|
+
const { done, value } = await reader.read();
|
|
260
|
+
if (done) break;
|
|
261
|
+
chunks.push(value);
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
reader.releaseLock();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Concatenate all chunks into a single Buffer
|
|
268
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
269
|
+
const result = new Uint8Array(totalLength);
|
|
270
|
+
let offset = 0;
|
|
271
|
+
for (const chunk of chunks) {
|
|
272
|
+
result.set(chunk, offset);
|
|
273
|
+
offset += chunk.length;
|
|
274
|
+
}
|
|
275
|
+
nativeOptions.body = Buffer.from(result);
|
|
276
|
+
} else if (nativeOptions.body instanceof ArrayBuffer) {
|
|
277
|
+
nativeOptions.body = Buffer.from(nativeOptions.body);
|
|
278
|
+
} else if (Array.isArray(nativeOptions.body)) {
|
|
279
|
+
nativeOptions.body = Buffer.from(nativeOptions.body);
|
|
280
|
+
}
|
|
281
|
+
} else if (nativeOptions.body === null) {
|
|
282
|
+
// Remove null body
|
|
283
|
+
delete nativeOptions.body;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Attach to the default agent if none is provided
|
|
287
|
+
if (!nativeOptions.agent) {
|
|
288
|
+
if (!defaultAgent) {
|
|
289
|
+
defaultAgent = new native.Agent();
|
|
290
|
+
}
|
|
291
|
+
nativeOptions.agent = defaultAgent;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Extract signal to pass as separate parameter
|
|
295
|
+
const signal = nativeOptions.signal;
|
|
296
|
+
delete nativeOptions.signal;
|
|
297
|
+
|
|
298
|
+
// Check if signal is already aborted
|
|
299
|
+
if (signal && signal.aborted) {
|
|
300
|
+
const error = new Error(
|
|
301
|
+
"Aborted: the request was aborted before it could start",
|
|
302
|
+
);
|
|
303
|
+
error.name = "AbortError";
|
|
304
|
+
error.code = ERROR_CODES.Aborted;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const nativeResponse = await faithFetch(url, nativeOptions, signal);
|
|
309
|
+
return new Response(nativeResponse);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
Agent: native.Agent,
|
|
314
|
+
ERROR_CODES,
|
|
315
|
+
FAITH_VERSION: native.FAITH_VERSION,
|
|
316
|
+
fetch,
|
|
317
|
+
REQWEST_VERSION: native.REQWEST_VERSION,
|
|
318
|
+
Response,
|
|
319
|
+
USER_AGENT: native.USER_AGENT,
|
|
320
|
+
};
|