@gjsify/fetch 0.0.4 → 0.1.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.
Files changed (88) hide show
  1. package/README.md +27 -2
  2. package/globals.mjs +12 -0
  3. package/lib/body.d.ts +69 -0
  4. package/lib/body.js +375 -0
  5. package/lib/errors/abort-error.d.ts +7 -0
  6. package/lib/errors/abort-error.js +9 -0
  7. package/lib/errors/base.d.ts +6 -0
  8. package/lib/errors/base.js +17 -0
  9. package/lib/errors/fetch-error.d.ts +16 -0
  10. package/lib/errors/fetch-error.js +23 -0
  11. package/lib/esm/body.js +104 -56
  12. package/lib/esm/errors/base.js +3 -1
  13. package/lib/esm/headers.js +116 -131
  14. package/lib/esm/index.js +145 -190
  15. package/lib/esm/request.js +42 -41
  16. package/lib/esm/response.js +19 -4
  17. package/lib/esm/utils/blob-from.js +2 -98
  18. package/lib/esm/utils/data-uri.js +23 -0
  19. package/lib/esm/utils/is.js +7 -3
  20. package/lib/esm/utils/multipart-parser.js +5 -2
  21. package/lib/esm/utils/referrer.js +10 -10
  22. package/lib/esm/utils/soup-helpers.js +22 -0
  23. package/lib/headers.d.ts +33 -0
  24. package/lib/headers.js +195 -0
  25. package/lib/index.d.ts +18 -0
  26. package/lib/index.js +205 -0
  27. package/lib/request.d.ts +101 -0
  28. package/lib/request.js +308 -0
  29. package/lib/response.d.ts +73 -0
  30. package/lib/response.js +158 -0
  31. package/lib/types/index.d.ts +1 -0
  32. package/lib/types/index.js +1 -0
  33. package/lib/types/system-error.d.ts +11 -0
  34. package/lib/types/system-error.js +2 -0
  35. package/lib/utils/blob-from.d.ts +2 -0
  36. package/lib/utils/blob-from.js +4 -0
  37. package/lib/utils/data-uri.d.ts +10 -0
  38. package/lib/utils/data-uri.js +27 -0
  39. package/lib/utils/get-search.d.ts +1 -0
  40. package/lib/utils/get-search.js +8 -0
  41. package/lib/utils/is-redirect.d.ts +7 -0
  42. package/lib/utils/is-redirect.js +10 -0
  43. package/lib/utils/is.d.ts +35 -0
  44. package/lib/utils/is.js +74 -0
  45. package/lib/utils/multipart-parser.d.ts +2 -0
  46. package/lib/utils/multipart-parser.js +396 -0
  47. package/lib/utils/referrer.d.ts +76 -0
  48. package/lib/utils/referrer.js +283 -0
  49. package/lib/utils/soup-helpers.d.ts +12 -0
  50. package/lib/utils/soup-helpers.js +25 -0
  51. package/package.json +23 -27
  52. package/src/body.ts +181 -169
  53. package/src/errors/base.ts +3 -1
  54. package/src/headers.ts +155 -202
  55. package/src/index.spec.ts +268 -3
  56. package/src/index.ts +199 -312
  57. package/src/request.ts +84 -75
  58. package/src/response.ts +48 -18
  59. package/src/test.mts +1 -1
  60. package/src/utils/blob-from.ts +4 -164
  61. package/src/utils/data-uri.ts +29 -0
  62. package/src/utils/is.ts +15 -15
  63. package/src/utils/multipart-parser.ts +3 -3
  64. package/src/utils/referrer.ts +11 -11
  65. package/src/utils/soup-helpers.ts +37 -0
  66. package/tsconfig.json +4 -4
  67. package/tsconfig.tsbuildinfo +1 -0
  68. package/lib/cjs/body.js +0 -255
  69. package/lib/cjs/errors/abort-error.js +0 -9
  70. package/lib/cjs/errors/base.js +0 -17
  71. package/lib/cjs/errors/fetch-error.js +0 -21
  72. package/lib/cjs/headers.js +0 -202
  73. package/lib/cjs/index.js +0 -224
  74. package/lib/cjs/request.js +0 -281
  75. package/lib/cjs/response.js +0 -133
  76. package/lib/cjs/types/index.js +0 -1
  77. package/lib/cjs/types/system-error.js +0 -1
  78. package/lib/cjs/utils/blob-from.js +0 -101
  79. package/lib/cjs/utils/get-search.js +0 -11
  80. package/lib/cjs/utils/is-redirect.js +0 -7
  81. package/lib/cjs/utils/is.js +0 -28
  82. package/lib/cjs/utils/multipart-parser.js +0 -353
  83. package/lib/cjs/utils/referrer.js +0 -153
  84. package/test.gjs.js +0 -34758
  85. package/test.gjs.mjs +0 -53172
  86. package/test.node.js +0 -1226
  87. package/test.node.mjs +0 -6273
  88. package/tsconfig.types.json +0 -8
package/src/index.ts CHANGED
@@ -1,43 +1,32 @@
1
- import Soup from '@girs/soup-3.0';
2
- import Gio from '@girs/gio-2.0';
3
- import Request from './request.js';
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from node-fetch (https://github.com/node-fetch/node-fetch) and the Fetch API spec (https://fetch.spec.whatwg.org/)
3
+ // Copyright (c) node-fetch contributors. MIT license.
4
+ // Modifications: Rewritten for GJS using libsoup 3.0 (Soup.Session)
4
5
 
5
- /**
6
- * Index.js
7
- *
8
- * a request API compatible with window.fetch
9
- *
10
- * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
11
- */
12
-
13
- import zlib from 'zlib';
14
- import Stream, { PassThrough, pipeline as pump } from 'stream';
6
+ import type Gio from '@girs/gio-2.0';
7
+ import Stream from 'node:stream';
15
8
 
16
- import dataUriToBuffer from 'data-uri-to-buffer';
9
+ import { parseDataUri } from './utils/data-uri.js';
17
10
 
18
11
  import { writeToStream, clone } from './body.js';
19
12
  import Response from './response.js';
20
13
  import Headers from './headers.js';
21
- import { getSoupRequestOptions } from './request.js';
14
+ import Request, { getSoupRequestOptions } from './request.js';
22
15
  import { FetchError } from './errors/fetch-error.js';
23
16
  import { AbortError } from './errors/abort-error.js';
24
17
  import { isRedirect } from './utils/is-redirect.js';
25
- import { FormData } from 'formdata-polyfill/esm.min.js';
18
+ import { FormData } from '@gjsify/formdata';
26
19
  import { isDomainOrSubdomain, isSameProtocol } from './utils/is.js';
27
20
  import { parseReferrerPolicyFromHeader } from './utils/referrer.js';
28
21
  import {
29
22
  Blob,
30
23
  File,
31
- fileFromSync,
32
- fileFrom,
33
- blobFromSync,
34
- blobFrom
35
24
  } from './utils/blob-from.js';
36
25
 
37
- import { URL } from '@gjsify/deno-runtime/ext/url/00_url';
26
+ import { URL } from '@gjsify/url';
38
27
 
39
28
  export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
40
- export { Blob, File, fileFromSync, fileFrom, blobFromSync, blobFrom };
29
+ export { Blob, File };
41
30
 
42
31
  import type { SystemError } from './types/index.js';
43
32
 
@@ -49,319 +38,217 @@ const supportedSchemas = new Set(['data:', 'http:', 'https:']);
49
38
  * @param url Absolute url or Request instance
50
39
  * @param init Fetch options
51
40
  */
52
- export default async function fetch(url: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
53
- return new Promise(async (resolve, reject) => {
54
- // Build request object
55
- const request = new Request(url, init);
56
- const { parsedURL, options } = getSoupRequestOptions(request);
57
- if (!supportedSchemas.has(parsedURL.protocol)) {
58
- throw new TypeError(`@gjsify/fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, '')}" is not supported.`);
59
- }
60
-
61
- if (parsedURL.protocol === 'data:') {
62
- const data = dataUriToBuffer(request.url);
63
- const response = new Response(data, { headers: { 'Content-Type': data.typeFull } });
64
- resolve(response);
65
- return;
41
+ export default async function fetch(url: RequestInfo | URL | Request, init: RequestInit = {}): Promise<Response> {
42
+ // Build request object
43
+ const request = new Request(url, init);
44
+ const { parsedURL, options } = getSoupRequestOptions(request);
45
+ if (!supportedSchemas.has(parsedURL.protocol)) {
46
+ throw new TypeError(`@gjsify/fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, '')}" is not supported.`);
47
+ }
48
+
49
+ // Handle data: URIs
50
+ if (parsedURL.protocol === 'data:') {
51
+ const { buffer, typeFull } = parseDataUri(request.url);
52
+ const response = new Response(Buffer.from(buffer), { headers: { 'Content-Type': typeFull } });
53
+ return response;
54
+ }
55
+
56
+ const { signal } = request;
57
+
58
+ // Check if already aborted
59
+ if (signal && signal.aborted) {
60
+ throw new AbortError('The operation was aborted.');
61
+ }
62
+
63
+ // Send HTTP request via Soup
64
+ let readable: Stream.Readable;
65
+ let cancellable: Gio.Cancellable;
66
+
67
+ try {
68
+ const sendRes = await request._send(options);
69
+ readable = sendRes.readable;
70
+ cancellable = sendRes.cancellable;
71
+ } catch (error: unknown) {
72
+ const err = error instanceof Error ? error : new Error(String(error));
73
+ throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err as unknown as SystemError);
74
+ }
75
+
76
+ // Wire up abort signal to cancellable
77
+ const abortHandler = () => {
78
+ cancellable.cancel();
79
+ };
80
+
81
+ if (signal) {
82
+ signal.addEventListener('abort', abortHandler, { once: true });
83
+ }
84
+
85
+ const finalize = () => {
86
+ if (signal) {
87
+ signal.removeEventListener('abort', abortHandler);
66
88
  }
89
+ };
67
90
 
68
- const { signal } = request;
69
- let response = null;
70
-
71
- const abort = () => {
72
- const error = new AbortError('The operation was aborted.');
73
- reject(error);
74
- if (request.body && request.body instanceof Stream.Readable) {
75
- request.body.destroy(error);
76
- }
77
-
78
- if (!response || !response.body) {
79
- return;
80
- }
81
-
82
- response.body.emit('error', error);
83
- };
91
+ // Listen for cancellation
92
+ cancellable.connect('cancelled', () => {
93
+ readable.destroy(new AbortError('The operation was aborted.'));
94
+ });
84
95
 
85
- if (signal && signal.aborted) {
86
- abort();
87
- return;
88
- }
96
+ // Handle stream errors
97
+ readable.on('error', (error: SystemError) => {
98
+ finalize();
99
+ // Error is consumed by the body when read
100
+ });
89
101
 
90
- const abortAndFinalize = () => {
91
- abort();
92
- finalize();
93
- };
102
+ const message = request._message;
103
+ const headers = Headers._newFromSoupMessage(message);
104
+ const statusCode = message.status_code;
105
+ const statusMessage = message.get_reason_phrase();
94
106
 
95
- let readable: Stream.Readable;
96
- let cancellable: Gio.Cancellable;
107
+ // HTTP fetch step 5 — handle redirects
108
+ if (isRedirect(statusCode)) {
109
+ const location = headers.get('Location');
97
110
 
98
- // Send request
111
+ let locationURL: URL | null = null;
99
112
  try {
100
- const sendRes = await request._send(options);
101
- readable = sendRes.readable;
102
- cancellable = sendRes.cancellable;
103
- } catch (error) {
104
- reject(error);
113
+ locationURL = location === null ? null : new URL(location, request.url);
114
+ } catch {
115
+ if (request.redirect !== 'manual') {
116
+ finalize();
117
+ throw new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect');
118
+ }
105
119
  }
106
120
 
107
- if (signal) {
108
- signal.addEventListener('abort', abortAndFinalize);
109
- }
121
+ switch (request.redirect) {
122
+ case 'error':
123
+ finalize();
124
+ throw new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect');
110
125
 
111
- const cancelledSignalId = cancellable.connect('cancelled', () => {
112
- abortAndFinalize();
113
- });
126
+ case 'manual':
127
+ // Nothing to do — return opaque redirect response
128
+ break;
114
129
 
115
- const finalize = () => {
116
- cancellable.cancel()
117
- if (signal) {
118
- signal.removeEventListener('abort', abortAndFinalize);
119
- }
120
- cancellable.disconnect(cancelledSignalId);
121
- };
122
-
123
- const message = request._message;
124
-
125
- readable.on('error', (error: SystemError) => {
126
- reject(new FetchError(`request to ${request.url} failed, reason: ${error.message}`, 'system', error));
127
- finalize();
128
- });
129
-
130
- message.connect('finished', (message) => {
131
-
132
- const headers = Headers._newFromSoupMessage( request._message, Soup.MessageHeadersType.RESPONSE);
133
- const statusCode = message.status_code;
134
- const statusMessage = message.get_reason_phrase() ;
135
-
136
- // HTTP fetch step 5
137
- if (isRedirect(statusCode)) {
138
- // HTTP fetch step 5.2
139
- const location = headers.get('Location');
140
-
141
- // HTTP fetch step 5.3
142
- let locationURL = null;
143
- try {
144
- locationURL = location === null ? null : new URL(location, request.url);
145
- } catch {
146
- // error here can only be invalid URL in Location: header
147
- // do not throw when options.redirect == manual
148
- // let the user extract the errorneous redirect URL
149
- if (request.redirect !== 'manual') {
150
- reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect'));
151
- finalize();
152
- return;
153
- }
130
+ case 'follow': {
131
+ if (locationURL === null) {
132
+ break;
133
+ }
134
+
135
+ if (request.counter >= request.follow) {
136
+ finalize();
137
+ throw new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect');
154
138
  }
155
139
 
156
- // HTTP fetch step 5.5
157
- switch (request.redirect) {
158
- case 'error':
159
- reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect'));
160
- finalize();
161
- return;
162
- case 'manual':
163
- // Nothing to do
164
- break;
165
- case 'follow': {
166
- // HTTP-redirect fetch step 2
167
- if (locationURL === null) {
168
- break;
169
- }
170
-
171
- // HTTP-redirect fetch step 5
172
- if (request.counter >= request.follow) {
173
- reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
174
- finalize();
175
- return;
176
- }
177
-
178
- // HTTP-redirect fetch step 6 (counter increment)
179
- // Create a new Request object.
180
- const requestOptions = {
181
- headers: new Headers(request.headers),
182
- follow: request.follow,
183
- counter: request.counter + 1,
184
- agent: request.agent,
185
- compress: request.compress,
186
- method: request.method,
187
- body: clone(request),
188
- signal: request.signal,
189
- size: request.size,
190
- referrer: request.referrer,
191
- referrerPolicy: request.referrerPolicy
192
- };
193
-
194
- // when forwarding sensitive headers like "Authorization",
195
- // "WWW-Authenticate", and "Cookie" to untrusted targets,
196
- // headers will be ignored when following a redirect to a domain
197
- // that is not a subdomain match or exact match of the initial domain.
198
- // For example, a redirect from "foo.com" to either "foo.com" or "sub.foo.com"
199
- // will forward the sensitive headers, but a redirect to "bar.com" will not.
200
- // headers will also be ignored when following a redirect to a domain using
201
- // a different protocol. For example, a redirect from "https://foo.com" to "http://foo.com"
202
- // will not forward the sensitive headers
203
- if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
204
- for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
205
- requestOptions.headers.delete(name);
206
- }
207
- }
208
-
209
- // HTTP-redirect fetch step 9
210
- if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
211
- reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
212
- finalize();
213
- return;
214
- }
215
-
216
- // HTTP-redirect fetch step 11
217
- if (statusCode === 303 || ((statusCode === 301 || statusCode === 302) && request.method === 'POST')) {
218
- requestOptions.method = 'GET';
219
- requestOptions.body = undefined;
220
- requestOptions.headers.delete('content-length');
221
- }
222
-
223
- // HTTP-redirect fetch step 14
224
- const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
225
- if (responseReferrerPolicy) {
226
- requestOptions.referrerPolicy = responseReferrerPolicy;
227
- }
228
-
229
- // HTTP-redirect fetch step 15
230
- resolve(fetch(new Request(locationURL, requestOptions)));
231
- finalize();
232
- return;
140
+ const requestOptions: Omit<RequestInit, 'headers'> & {
141
+ headers: Headers;
142
+ follow: number;
143
+ counter: number;
144
+ agent: string | ((url: URL) => string);
145
+ compress: boolean;
146
+ size: number;
147
+ } = {
148
+ headers: new Headers(request.headers),
149
+ follow: request.follow,
150
+ counter: request.counter + 1,
151
+ agent: request.agent,
152
+ compress: request.compress,
153
+ method: request.method,
154
+ body: clone(request) as unknown as BodyInit | null,
155
+ signal: request.signal,
156
+ size: request.size,
157
+ referrer: request.referrer,
158
+ referrerPolicy: request.referrerPolicy
159
+ };
160
+
161
+ // Don't forward sensitive headers to different domains/protocols
162
+ if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
163
+ for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
164
+ requestOptions.headers.delete(name);
233
165
  }
166
+ }
234
167
 
235
- default:
236
- return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`));
168
+ // Cannot follow redirect with body being a readable stream
169
+ if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
170
+ finalize();
171
+ throw new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect');
237
172
  }
238
- }
239
173
 
240
- // Prepare response
241
- // if (signal) {
242
- // response_.once('end', () => {
243
- // signal.removeEventListener('abort', abortAndFinalize);
244
- // });
245
- // }
174
+ // 303 or POST→GET conversion
175
+ if (statusCode === 303 || ((statusCode === 301 || statusCode === 302) && request.method === 'POST')) {
176
+ requestOptions.method = 'GET';
177
+ requestOptions.body = undefined;
178
+ requestOptions.headers.delete('content-length');
179
+ }
246
180
 
247
- let body = pump(response_, new PassThrough(), error => {
248
- if (error) {
249
- reject(error);
181
+ // Update referrer policy from response
182
+ const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
183
+ if (responseReferrerPolicy) {
184
+ requestOptions.referrerPolicy = responseReferrerPolicy;
250
185
  }
251
- });
252
- // see https://github.com/nodejs/node/pull/29376
253
- /* c8 ignore next 3 */
254
- // if (process.version < 'v12.10') {
255
- // response_.on('aborted', abortAndFinalize);
256
- // }
257
-
258
- const responseOptions = {
259
- url: request.url,
260
- status: statusCode,
261
- statusText: statusMessage,
262
- headers,
263
- size: request.size,
264
- counter: request.counter,
265
- highWaterMark: request.highWaterMark
266
- };
267
-
268
- // HTTP-network fetch step 12.1.1.3
269
- const codings = headers.get('Content-Encoding');
270
-
271
- // HTTP-network fetch step 12.1.1.4: handle content codings
272
-
273
- // in following scenarios we ignore compression support
274
- // 1. compression support is disabled
275
- // 2. HEAD request
276
- // 3. no Content-Encoding header
277
- // 4. no content response (204)
278
- // 5. content not modified response (304)
279
- if (!request.compress || request.method === 'HEAD' || codings === null || statusCode === 204 || statusCode === 304) {
280
- response = new Response(body, responseOptions);
281
- resolve(response);
282
- return;
283
- }
284
186
 
285
- // For Node v6+
286
- // Be less strict when decoding compressed responses, since sometimes
287
- // servers send slightly invalid responses that are still accepted
288
- // by common browsers.
289
- // Always using Z_SYNC_FLUSH is what cURL does.
290
- const zlibOptions = {
291
- flush: zlib.Z_SYNC_FLUSH,
292
- finishFlush: zlib.Z_SYNC_FLUSH
293
- };
294
-
295
- // For gzip
296
- if (codings === 'gzip' || codings === 'x-gzip') {
297
- body = pump(body, zlib.createGunzip(zlibOptions), error => {
298
- if (error) {
299
- reject(error);
300
- }
301
- });
302
- response = new Response(body, responseOptions);
303
- resolve(response);
304
- return;
187
+ finalize();
188
+ return fetch(new Request(locationURL, requestOptions as unknown as RequestInit));
305
189
  }
306
190
 
307
- // For deflate
308
- // if (codings === 'deflate' || codings === 'x-deflate') {
309
- // // Handle the infamous raw deflate response from old servers
310
- // // a hack for old IIS and Apache servers
311
- // const raw = pump(response_, new PassThrough(), error => {
312
- // if (error) {
313
- // reject(error);
314
- // }
315
- // });
316
-
317
- // raw.once('data', chunk => {
318
- // // See http://stackoverflow.com/questions/37519828
319
- // if ((chunk[0] & 0x0F) === 0x08) {
320
- // body = pump(body, zlib.createInflate(), error => {
321
- // if (error) {
322
- // reject(error);
323
- // }
324
- // });
325
- // } else {
326
- // body = pump(body, zlib.createInflateRaw(), error => {
327
- // if (error) {
328
- // reject(error);
329
- // }
330
- // });
331
- // }
332
-
333
- // response = new Response(body, responseOptions);
334
- // resolve(response);
335
- // });
336
-
337
- // raw.once('end', () => {
338
- // // Some old IIS servers return zero-length OK deflate responses, so
339
- // // 'data' is never emitted. See https://github.com/node-fetch/node-fetch/pull/903
340
- // if (!response) {
341
- // response = new Response(body, responseOptions);
342
- // resolve(response);
343
- // }
344
- // });
345
- // return;
346
- // }
347
-
348
- // For br
349
- if (codings === 'br') {
350
- body = pump(body, zlib.createBrotliDecompress(), error => {
351
- if (error) {
352
- reject(error);
353
- }
354
- });
355
- response = new Response(body, responseOptions);
356
- resolve(response);
357
- return;
191
+ default:
192
+ throw new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`);
193
+ }
194
+ }
195
+
196
+ // Build response
197
+ const responseOptions = {
198
+ url: request.url,
199
+ status: statusCode,
200
+ statusText: statusMessage,
201
+ headers,
202
+ size: request.size,
203
+ counter: request.counter,
204
+ highWaterMark: request.highWaterMark
205
+ };
206
+
207
+ // Handle content encoding (decompression)
208
+ const codings = headers.get('Content-Encoding');
209
+
210
+ // Skip decompression when:
211
+ // 1. compression support is disabled
212
+ // 2. HEAD request
213
+ // 3. no Content-Encoding header
214
+ // 4. no content response (204)
215
+ // 5. content not modified response (304)
216
+ if (!request.compress || request.method === 'HEAD' || codings === null || statusCode === 204 || statusCode === 304) {
217
+ finalize();
218
+ return new Response(readable, responseOptions);
219
+ }
220
+
221
+ // Try to use DecompressionStream Web API (available in modern SpiderMonkey)
222
+ if (typeof DecompressionStream !== 'undefined') {
223
+ let format: CompressionFormat | null = null;
224
+
225
+ if (codings === 'gzip' || codings === 'x-gzip') {
226
+ format = 'gzip';
227
+ } else if (codings === 'deflate' || codings === 'x-deflate') {
228
+ format = 'deflate';
229
+ }
230
+
231
+ if (format) {
232
+ const webBody = new Response(readable, responseOptions).body;
233
+ if (webBody) {
234
+ const decompressed = webBody.pipeThrough(new DecompressionStream(format) as ReadableWritablePair<Uint8Array, Uint8Array>);
235
+ finalize();
236
+ return new Response(decompressed as unknown as ReadableStream, responseOptions);
358
237
  }
238
+ }
239
+ }
359
240
 
360
- // Otherwise, use response as-is
361
- response = new Response(body, responseOptions);
362
- resolve(response);
363
- });
241
+ // Fallback: return the body as-is (no streaming decompression available)
242
+ finalize();
243
+ return new Response(readable, responseOptions);
244
+ }
364
245
 
365
- writeToStream(inputStream, request).catch(reject);
366
- });
246
+ // Register Fetch API globals on GJS (pattern: @gjsify/abort-controller, @gjsify/eventsource)
247
+ // On Node.js, native globals are already fully functional — only overwrite on GJS.
248
+ const _isGJS = typeof (globalThis as any).imports !== 'undefined';
249
+ if (_isGJS) {
250
+ (globalThis as any).fetch = fetch;
251
+ (globalThis as any).Headers = Headers;
252
+ (globalThis as any).Request = Request;
253
+ (globalThis as any).Response = Response;
367
254
  }