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