@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/request.ts CHANGED
@@ -1,38 +1,58 @@
1
- import '@girs/gjs';
2
- import '@girs/gio-2.0';
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from node-fetch (https://github.com/node-fetch/node-fetch/blob/main/src/request.js)
3
+ // Copyright (c) node-fetch contributors. MIT license.
4
+ // Modifications: Rewritten for GJS using Soup.Message and Gio
3
5
 
4
6
  import GLib from '@girs/glib-2.0';
5
7
  import Soup from '@girs/soup-3.0';
6
8
  import Gio from '@girs/gio-2.0';
7
- import * as GioExt from '@gjsify/gio-2.0';
8
- import * as SoupExt from '@gjsify/soup-3.0';
9
+ import { soupSendAsync, inputStreamToReadable } from './utils/soup-helpers.js';
9
10
 
10
- import { URL } from '@gjsify/deno-runtime/ext/url/00_url';
11
- import { Blob } from '@gjsify/deno-runtime/ext/web/09_file';
11
+ import { URL } from '@gjsify/url';
12
+ import { Blob } from './utils/blob-from.js';
12
13
 
13
- import { Readable } from 'stream';
14
+ import { Readable } from 'node:stream';
14
15
 
15
16
  import Headers from './headers.js';
16
17
  import Body, {clone, extractContentType, getTotalBytes} from './body.js';
17
18
  import {isAbortSignal} from './utils/is.js';
18
- // import { getSearch } from './utils/get-search.js';
19
+ import type { FormData } from '@gjsify/formdata';
19
20
  import {
20
21
  validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY
21
22
  } from './utils/referrer.js';
22
23
 
23
24
  const INTERNALS = Symbol('Request internals');
24
25
 
26
+ /** Properties that may exist on a Request-like object (used for safe casting). */
27
+ interface RequestLike {
28
+ url?: string;
29
+ method?: string;
30
+ headers?: Headers | HeadersInit;
31
+ redirect?: RequestRedirect;
32
+ signal?: AbortSignal | null;
33
+ referrer?: string;
34
+ referrerPolicy?: ReferrerPolicy;
35
+ body?: BodyInit | null;
36
+ follow?: number;
37
+ compress?: boolean;
38
+ counter?: number;
39
+ agent?: string | ((url: URL) => string);
40
+ highWaterMark?: number;
41
+ insecureHTTPParser?: boolean;
42
+ size?: number;
43
+ }
44
+
25
45
  /**
26
46
  * Check if `obj` is an instance of Request.
27
47
  */
28
- const isRequest = (obj: RequestInfo | URL) => {
48
+ const isRequest = (obj: RequestInfo | URL | Request): boolean => {
29
49
  return (
30
50
  typeof obj === 'object' &&
31
- typeof (obj as Request).url === 'string'
51
+ typeof (obj as RequestLike).url === 'string'
32
52
  );
33
53
  };
34
54
 
35
- // @ts-ignore
55
+ // @ts-expect-error — declaration merging with globalThis.Request for Fetch API compatibility
36
56
  export interface Request extends globalThis.Request {}
37
57
 
38
58
  /** This Fetch API interface represents a resource request. */
@@ -128,9 +148,9 @@ export class Request extends Body {
128
148
  referrer: string | URL;
129
149
  referrerPolicy: ReferrerPolicy;
130
150
  // Gjsify
131
- session: SoupExt.ExtSession & Soup.Session;
132
- message: Soup.Message;
133
- inputStream?: Gio.InputStream & GioExt.ExtInputStream<Gio.InputStream>;
151
+ session: Soup.Session | null;
152
+ message: Soup.Message | null;
153
+ inputStream?: Gio.InputStream;
134
154
  readable?: Readable
135
155
  };
136
156
 
@@ -142,13 +162,16 @@ export class Request extends Body {
142
162
  highWaterMark = 16384;
143
163
  insecureHTTPParser = false;
144
164
 
145
- constructor(input: RequestInfo | URL, init?: RequestInit) {
165
+ constructor(input: RequestInfo | URL | Request, init?: RequestInit) {
166
+ const inputRL = input as unknown as RequestLike;
167
+ const initRL = (init || {}) as unknown as RequestLike;
168
+
146
169
  let parsedURL: URL;
147
- let requestObj: Partial<Request> = {};
170
+ let requestObj: RequestLike = {};
148
171
 
149
172
  if(isRequest(input)) {
150
- parsedURL = new URL((input as Request).url);
151
- requestObj = input as Request;
173
+ parsedURL = new URL(inputRL.url);
174
+ requestObj = inputRL;
152
175
  } else {
153
176
  parsedURL = new URL(input as string | URL);
154
177
  }
@@ -157,23 +180,23 @@ export class Request extends Body {
157
180
  throw new TypeError(`${parsedURL} is an url with embedded credentials.`);
158
181
  }
159
182
 
160
- let method = init.method || requestObj.method || 'GET';
183
+ let method = initRL.method || requestObj.method || 'GET';
161
184
  if (/^(delete|get|head|options|post|put)$/i.test(method)) {
162
185
  method = method.toUpperCase();
163
186
  }
164
187
 
165
- if ((init.body != null || (isRequest(input) && (input as Request).body !== null)) &&
188
+ if ((init?.body != null || (isRequest(input) && inputRL.body !== null)) &&
166
189
  (method === 'GET' || method === 'HEAD')) {
167
190
  throw new TypeError('Request with GET/HEAD method cannot have body');
168
191
  }
169
192
 
170
- const inputBody = init.body ? init.body : (isRequest(input) && (input as Request).body !== null ? clone(input as Request & Body) : null);
193
+ const inputBody = init?.body ? init.body : (isRequest(input) && inputRL.body !== null ? clone(input as unknown as Request & Body) : null);
171
194
 
172
195
  super(inputBody, {
173
- size: (init as Request).size || (init as any).size || 0
196
+ size: initRL.size || 0
174
197
  });
175
198
 
176
- const headers = new Headers((init.headers || (input as Request).headers || {}) as HeadersInit);
199
+ const headers = new Headers((init?.headers || inputRL.headers || {}) as HeadersInit);
177
200
 
178
201
  if (inputBody !== null && !headers.has('Content-Type')) {
179
202
  const contentType = extractContentType(inputBody, this);
@@ -183,9 +206,9 @@ export class Request extends Body {
183
206
  }
184
207
 
185
208
  let signal = isRequest(input) ?
186
- (input as Request).signal :
209
+ inputRL.signal :
187
210
  null;
188
- if ('signal' in init) {
211
+ if (init && 'signal' in init) {
189
212
  signal = init.signal;
190
213
  }
191
214
 
@@ -194,7 +217,7 @@ export class Request extends Body {
194
217
  }
195
218
 
196
219
  // §5.4, Request constructor steps, step 15.1
197
- let referrer: string | URL = init.referrer == null ? (input as Request).referrer : init.referrer;
220
+ let referrer: string | URL = init?.referrer == null ? inputRL.referrer : init.referrer;
198
221
  if (referrer === '') {
199
222
  // §5.4, Request constructor steps, step 15.2
200
223
  referrer = 'no-referrer';
@@ -207,15 +230,21 @@ export class Request extends Body {
207
230
  referrer = undefined;
208
231
  }
209
232
 
210
- const session = SoupExt.ExtSession.new();
211
- const message = new Soup.Message({
212
- method,
213
- uri: this._uri,
214
- });
233
+ // Only create Soup objects for HTTP/HTTPS — data: URIs etc. don't go through Soup
234
+ const scheme = parsedURL.protocol;
235
+ let session: Soup.Session | null = null;
236
+ let message: Soup.Message | null = null;
237
+ if (scheme === 'http:' || scheme === 'https:') {
238
+ session = new Soup.Session();
239
+ message = new Soup.Message({
240
+ method,
241
+ uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.NONE),
242
+ });
243
+ }
215
244
 
216
245
  this[INTERNALS] = {
217
246
  method,
218
- redirect: init.redirect || (input as Request).redirect || 'follow',
247
+ redirect: init?.redirect || inputRL.redirect || 'follow',
219
248
  headers,
220
249
  parsedURL,
221
250
  signal,
@@ -226,32 +255,35 @@ export class Request extends Body {
226
255
  };
227
256
 
228
257
  // Node-fetch-only options
229
- this.follow = (init as Request).follow === undefined ? ((input as Request).follow === undefined ? 20 : (input as Request).follow) : (init as Request).follow;
230
- this.compress = (init as Request).compress === undefined ? ((input as Request).compress === undefined ? true : (input as Request).compress) : (init as Request).compress;
231
- this.counter = (init as Request).counter || (input as Request).counter || 0;
232
- this.agent = (init as Request).agent || (input as Request).agent;
233
- this.highWaterMark = (init as Request).highWaterMark || (input as Request).highWaterMark || 16384;
234
- this.insecureHTTPParser = (init as Request).insecureHTTPParser || (input as Request).insecureHTTPParser || false;
258
+ this.follow = initRL.follow === undefined ? (inputRL.follow === undefined ? 20 : inputRL.follow) : initRL.follow;
259
+ this.compress = initRL.compress === undefined ? (inputRL.compress === undefined ? true : inputRL.compress) : initRL.compress;
260
+ this.counter = initRL.counter || inputRL.counter || 0;
261
+ this.agent = initRL.agent || inputRL.agent;
262
+ this.highWaterMark = initRL.highWaterMark || inputRL.highWaterMark || 16384;
263
+ this.insecureHTTPParser = initRL.insecureHTTPParser || inputRL.insecureHTTPParser || false;
235
264
 
236
265
  // §5.4, Request constructor steps, step 16.
237
266
  // Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy
238
- this.referrerPolicy = init.referrerPolicy || (input as Request).referrerPolicy || '';
267
+ this.referrerPolicy = init?.referrerPolicy || inputRL.referrerPolicy || '';
239
268
  }
240
269
 
241
270
  /**
242
- * Custom send method using Soup, used in fetch to send the request
243
- * @param options
244
- * @returns
271
+ * Send the request using Soup.
245
272
  */
246
273
  async _send(options: { headers: Headers }) {
247
- options.headers._appendToSoupMessage(this._message);
274
+ const { session, message } = this[INTERNALS];
275
+
276
+ if (!session || !message) {
277
+ throw new Error('Cannot send request: no Soup session (non-HTTP URL?)');
278
+ }
279
+
280
+ options.headers._appendToSoupMessage(message);
248
281
 
249
282
  const cancellable = new Gio.Cancellable();
250
283
 
251
- this[INTERNALS].inputStream = await this._session.sendAsync(this._message, GLib.PRIORITY_DEFAULT, cancellable);
284
+ this[INTERNALS].inputStream = await soupSendAsync(session, message, GLib.PRIORITY_DEFAULT, cancellable);
285
+ this[INTERNALS].readable = inputStreamToReadable(this[INTERNALS].inputStream);
252
286
 
253
- this[INTERNALS].readable = this[INTERNALS].inputStream.toReadable({});
254
-
255
287
  return {
256
288
  inputStream: this[INTERNALS].inputStream,
257
289
  readable: this[INTERNALS].readable,
@@ -263,7 +295,6 @@ export class Request extends Body {
263
295
  * Clone this request
264
296
  */
265
297
  clone(): Request {
266
- // @ts-ignores
267
298
  return new Request(this);
268
299
  }
269
300
 
@@ -276,7 +307,7 @@ export class Request extends Body {
276
307
  async formData(): Promise<FormData> {
277
308
  return super.formData();
278
309
  }
279
- async json(): Promise<any> {
310
+ async json(): Promise<unknown> {
280
311
  return super.json();
281
312
  }
282
313
  async text(): Promise<string> {
@@ -298,10 +329,7 @@ Object.defineProperties(Request.prototype, {
298
329
  export default Request;
299
330
 
300
331
  /**
301
- * Convert a Request to Soup request options.
302
- *
303
- * @param request - A Request instance
304
- * @return The options object to be passed to http.request
332
+ * @param request
305
333
  */
306
334
  export const getSoupRequestOptions = (request: Request) => {
307
335
  const { parsedURL } = request[INTERNALS];
@@ -313,14 +341,14 @@ export const getSoupRequestOptions = (request: Request) => {
313
341
  }
314
342
 
315
343
  // HTTP-network-or-cache fetch steps 2.4-2.7
316
- let contentLengthValue = null;
344
+ let contentLengthValue: string | null = null;
317
345
  if (request.body === null && /^(post|put)$/i.test(request.method)) {
318
346
  contentLengthValue = '0';
319
347
  }
320
348
 
321
349
  if (request.body !== null) {
322
350
  const totalBytes = getTotalBytes(request);
323
- // Set Content-Length if totalBytes is a number (that is not NaN)
351
+ // Set Content-Length if totalBytes is a Number (that is not NaN)
324
352
  if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
325
353
  contentLengthValue = String(totalBytes);
326
354
  }
@@ -331,15 +359,11 @@ export const getSoupRequestOptions = (request: Request) => {
331
359
  }
332
360
 
333
361
  // 4.1. Main fetch, step 2.6
334
- // > If request's referrer policy is the empty string, then set request's referrer policy to the
335
- // > default referrer policy.
336
362
  if (request.referrerPolicy === '') {
337
363
  request.referrerPolicy = DEFAULT_REFERRER_POLICY;
338
364
  }
339
365
 
340
366
  // 4.1. Main fetch, step 2.7
341
- // > If request's referrer is not "no-referrer", set request's referrer to the result of invoking
342
- // > determine request's referrer.
343
367
  if (request.referrer && request.referrer !== 'no-referrer') {
344
368
  request[INTERNALS].referrer = determineRequestsReferrer(request);
345
369
  } else {
@@ -347,15 +371,13 @@ export const getSoupRequestOptions = (request: Request) => {
347
371
  }
348
372
 
349
373
  // 4.5. HTTP-network-or-cache fetch, step 6.9
350
- // > If httpRequest's referrer is a URL, then append `Referer`/httpRequest's referrer, serialized
351
- // > and isomorphic encoded, to httpRequest's header list.
352
374
  if (request[INTERNALS].referrer instanceof URL) {
353
375
  headers.set('Referer', request.referrer);
354
376
  }
355
377
 
356
378
  // HTTP-network-or-cache fetch step 2.11
357
379
  if (!headers.has('User-Agent')) {
358
- headers.set('User-Agent', 'node-fetch');
380
+ headers.set('User-Agent', 'gjsify-fetch');
359
381
  }
360
382
 
361
383
  // HTTP-network-or-cache fetch step 2.15
@@ -372,25 +394,12 @@ export const getSoupRequestOptions = (request: Request) => {
372
394
  headers.set('Connection', 'close');
373
395
  }
374
396
 
375
- // HTTP-network fetch step 4.2
376
- // chunked encoding is handled by Node.js
377
-
378
- // const search = getSearch(parsedURL);
379
-
380
- // Pass the full URL directly to request(), but overwrite the following
381
- // options:
382
397
  const options = {
383
- // Overwrite search to retain trailing ? (issue #776)
384
- // path: parsedURL.pathname + search,
385
- // The following options are not expressed in the URL
386
- // method: request.method,
387
398
  headers,
388
- // insecureHTTPParser: request.insecureHTTPParser,
389
- // agent
390
399
  };
391
400
 
392
401
  return {
393
402
  parsedURL,
394
403
  options
395
404
  };
396
- };
405
+ };
package/src/response.ts CHANGED
@@ -1,8 +1,7 @@
1
- /**
2
- * Response.js
3
- *
4
- * Response class provides content decoding
5
- */
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from node-fetch (https://github.com/node-fetch/node-fetch/blob/main/src/response.js)
3
+ // Copyright (c) node-fetch contributors. MIT license.
4
+ // Modifications: Rewritten for GJS using Gio streams and GLib
6
5
 
7
6
  import GLib from '@girs/glib-2.0';
8
7
  import Gio from '@girs/gio-2.0';
@@ -11,14 +10,30 @@ import Headers from './headers.js';
11
10
  import Body, { clone, extractContentType } from './body.js';
12
11
  import { isRedirect } from './utils/is-redirect.js';
13
12
 
14
- import { URL } from '@gjsify/deno-runtime/ext/url/00_url';
15
- import { Blob } from '@gjsify/deno-runtime/ext/web/09_file';
13
+ import { URL } from '@gjsify/url';
14
+ import { Blob } from './utils/blob-from.js';
16
15
 
17
- import type { Readable } from 'stream';
16
+ import type { Readable } from 'node:stream';
18
17
 
19
18
  const INTERNALS = Symbol('Response internals');
20
19
 
21
- interface ResponseInit extends globalThis.ResponseInit {
20
+ interface ResponseOptions {
21
+ status?: number;
22
+ statusText?: string;
23
+ headers?: HeadersInit | Headers;
24
+ url?: string;
25
+ type?: ResponseType;
26
+ ok?: boolean;
27
+ redirected?: boolean;
28
+ size?: number;
29
+ counter?: number;
30
+ highWaterMark?: number;
31
+ }
32
+
33
+ interface ResponseInit {
34
+ status?: number;
35
+ statusText?: string;
36
+ headers?: HeadersInit | Headers;
22
37
  type?: ResponseType;
23
38
  url?: string;
24
39
  counter?: number;
@@ -36,7 +51,7 @@ interface ResponseInit extends globalThis.ResponseInit {
36
51
  * @param body Readable stream
37
52
  * @param opts Response options
38
53
  */
39
- export class Response extends Body implements globalThis.Response {
54
+ export class Response extends Body {
40
55
 
41
56
  [INTERNALS]: {
42
57
  type: ResponseType;
@@ -50,7 +65,7 @@ export class Response extends Body implements globalThis.Response {
50
65
 
51
66
  _inputStream: Gio.InputStream | null = null;
52
67
 
53
- constructor(body: BodyInit | Readable | Blob | Buffer | null = null, options: ResponseInit = {}) {
68
+ constructor(body: BodyInit | Readable | Blob | Buffer | null = null, options: ResponseOptions = {}) {
54
69
  super(body, options);
55
70
 
56
71
  // eslint-disable-next-line no-eq-null, eqeqeq, no-negated-condition
@@ -116,7 +131,6 @@ export class Response extends Body implements globalThis.Response {
116
131
  *
117
132
  * @return Response
118
133
  */
119
- // @ts-ignore
120
134
  clone() {
121
135
  return new Response(clone(this, this.highWaterMark), {
122
136
  type: this.type,
@@ -155,6 +169,23 @@ export class Response extends Body implements globalThis.Response {
155
169
  return response;
156
170
  }
157
171
 
172
+ /**
173
+ * Create a Response with a JSON body.
174
+ * @param data The data to serialize as JSON.
175
+ * @param init Optional response init options.
176
+ * @returns A Response with the JSON body and appropriate content-type header.
177
+ */
178
+ static json(data: unknown, init?: ResponseOptions) {
179
+ const body = JSON.stringify(data);
180
+ const options: ResponseOptions = { ...init };
181
+ const headers = new Headers(options.headers);
182
+ if (!headers.has('content-type')) {
183
+ headers.set('content-type', 'application/json');
184
+ }
185
+ options.headers = headers;
186
+ return new Response(body, options);
187
+ }
188
+
158
189
  get [Symbol.toStringTag]() {
159
190
  return 'Response';
160
191
  }
@@ -163,11 +194,11 @@ export class Response extends Body implements globalThis.Response {
163
194
  if (!this._inputStream) {
164
195
  return super.text();
165
196
  }
166
-
197
+
167
198
  const outputStream = Gio.MemoryOutputStream.new_resizable();
168
-
199
+
169
200
  await new Promise<number>((resolve, reject) => {
170
- outputStream.splice_async(this._inputStream, Gio.OutputStreamSpliceFlags.CLOSE_TARGET | Gio.OutputStreamSpliceFlags.CLOSE_SOURCE, GLib.PRIORITY_DEFAULT, null, (self, res) => {
201
+ outputStream.splice_async(this._inputStream, Gio.OutputStreamSpliceFlags.CLOSE_TARGET | Gio.OutputStreamSpliceFlags.CLOSE_SOURCE, GLib.PRIORITY_DEFAULT, null, (_self: Gio.OutputStream, res: Gio.AsyncResult) => {
171
202
  try {
172
203
  resolve(outputStream.splice_finish(res));
173
204
  } catch (error) {
@@ -175,10 +206,9 @@ export class Response extends Body implements globalThis.Response {
175
206
  }
176
207
  });
177
208
  });
178
-
179
-
209
+
180
210
  const bytes = outputStream.steal_as_bytes();
181
-
211
+
182
212
  return new TextDecoder().decode(bytes.toArray());
183
213
  }
184
214
  }
package/src/test.mts CHANGED
@@ -1,4 +1,4 @@
1
-
1
+ import '@gjsify/node-globals';
2
2
  import { run } from '@gjsify/unit';
3
3
 
4
4
  import testSuite from './index.spec.js';
@@ -1,168 +1,8 @@
1
- import { DOMException } from "@gjsify/deno-runtime/ext/web/01_dom_exception";
2
- import { Blob, File } from "@gjsify/deno-runtime/ext/web/09_file";
1
+ // Re-export Blob/File from buffer (which provides the polyfill on GJS)
2
+ // Reference: Node.js buffer.Blob (available since v18)
3
+ import { Blob, File } from 'node:buffer';
3
4
 
4
- import {
5
- realpathSync,
6
- statSync,
7
- rmdirSync,
8
- createReadStream,
9
- promises as fs
10
- } from 'node:fs'
11
- import { basename, sep, join } from 'node:path'
12
- import { tmpdir } from 'node:os'
13
- import process from 'node:process'
14
-
15
- // import Blob from './index.js'
16
-
17
- const { stat, mkdtemp } = fs
18
- let i = 0, tempDir, registry
19
-
20
- /**
21
- * @param {string} path filepath on the disk
22
- * @param {string} [type] mimetype to use
23
- */
24
- const blobFromSync = (path: string, type: string) => fromBlob(statSync(path), path, type)
25
-
26
- /**
27
- * @param {string} path filepath on the disk
28
- * @param {string} [type] mimetype to use
29
- * @returns {Promise<Blob>}
30
- */
31
- const blobFrom = (path: string, type: string): Promise<Blob> => stat(path).then(stat => fromBlob(stat, path, type))
32
-
33
- /**
34
- * @param {string} path filepath on the disk
35
- * @param {string} [type] mimetype to use
36
- * @returns {Promise<File>}
37
- */
38
- const fileFrom = (path: string, type: string): Promise<File> => stat(path).then(stat => fromFile(stat, path, type))
39
-
40
- /**
41
- * @param {string} path filepath on the disk
42
- * @param {string} [type] mimetype to use
43
- */
44
- const fileFromSync = (path: string, type: string) => fromFile(statSync(path), path, type)
45
-
46
- // @ts-ignore
47
- const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
48
- path,
49
- size: stat.size,
50
- lastModified: stat.mtimeMs,
51
- start: 0
52
- })], { type })
53
-
54
- // @ts-ignore
55
- const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
56
- path,
57
- size: stat.size,
58
- lastModified: stat.mtimeMs,
59
- start: 0
60
- })], basename(path), { type, lastModified: stat.mtimeMs })
61
-
62
- /**
63
- * Creates a temporary blob backed by the filesystem.
64
- * NOTE: requires node.js v14 or higher to use FinalizationRegistry
65
- *
66
- * @param {*} data Same as fs.writeFile data
67
- * @param {BlobPropertyBag & {signal?: AbortSignal}} options
68
- * @param {AbortSignal} [signal] in case you wish to cancel the write operation
69
- * @returns {Promise<Blob>}
70
- */
71
- const createTemporaryBlob = async (data: any, { signal, type }: BlobPropertyBag & { signal?: AbortSignal; } = {}): Promise<Blob> => {
72
- registry = registry || new FinalizationRegistry(fs.unlink)
73
- tempDir = tempDir || await mkdtemp(realpathSync(tmpdir()) + sep)
74
- const id = `${i++}`
75
- const destination = join(tempDir, id)
76
- if (data instanceof ArrayBuffer) data = new Uint8Array(data)
77
- await fs.writeFile(destination, data, { signal })
78
- const blob = await blobFrom(destination, type)
79
- registry.register(blob, destination)
80
- return blob
81
- }
82
-
83
- /**
84
- * Creates a temporary File backed by the filesystem.
85
- * Pretty much the same as constructing a new File(data, name, options)
86
- *
87
- * NOTE: requires node.js v14 or higher to use FinalizationRegistry
88
- * @param {*} data
89
- * @param {string} name
90
- * @param {FilePropertyBag & {signal?: AbortSignal}} opts
91
- * @returns {Promise<File>}
92
- */
93
- const createTemporaryFile = async (data: any, name: string, opts: FilePropertyBag & { signal?: AbortSignal; }): Promise<File> => {
94
- const blob = await createTemporaryBlob(data)
95
- return new File([blob], name, opts)
96
- }
97
-
98
- /**
99
- * This is a blob backed up by a file on the disk
100
- * with minium requirement. Its wrapped around a Blob as a blobPart
101
- * so you have no direct access to this.
102
- *
103
- * @private
104
- */
105
- class BlobDataItem {
106
- #path: string;
107
- #start: number;
108
- size
109
- lastModified
110
- originalSize
111
-
112
- constructor(options) {
113
- this.#path = options.path
114
- this.#start = options.start
115
- this.size = options.size
116
- this.lastModified = options.lastModified
117
- this.originalSize = options.originalSize === undefined
118
- ? options.size
119
- : options.originalSize
120
- }
121
-
122
- /**
123
- * Slicing arguments is first validated and formatted
124
- * to not be out of range by Blob.prototype.slice
125
- */
126
- slice(start: number, end: number) {
127
- return new BlobDataItem({
128
- path: this.#path,
129
- lastModified: this.lastModified,
130
- originalSize: this.originalSize,
131
- size: end - start,
132
- start: this.#start + start
133
- })
134
- }
135
-
136
- async * stream() {
137
- const { mtimeMs, size } = await stat(this.#path)
138
-
139
- if (mtimeMs > this.lastModified || this.originalSize !== size) {
140
- throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError')
141
- }
142
-
143
- yield* createReadStream(this.#path, {
144
- start: this.#start,
145
- end: this.#start + this.size - 1
146
- })
147
- }
148
-
149
- get [Symbol.toStringTag]() {
150
- return 'Blob'
151
- }
152
- }
153
-
154
- process.once('exit', () => {
155
- tempDir && rmdirSync(tempDir, { recursive: true })
156
- })
157
-
158
- export default blobFromSync
159
5
  export {
160
6
  Blob,
161
- blobFrom,
162
- blobFromSync,
163
- createTemporaryBlob,
164
7
  File,
165
- fileFrom,
166
- fileFromSync,
167
- createTemporaryFile
168
- }
8
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Parse a data: URI into its components.
3
+ * Replaces the `data-uri-to-buffer` npm package.
4
+ *
5
+ * Format: data:[<mediatype>][;base64],<data>
6
+ */
7
+ export function parseDataUri(uri: string): { buffer: Uint8Array; typeFull: string } {
8
+ const match = uri.match(/^data:([^,]*?)(;base64)?,(.*)$/s);
9
+ if (!match) {
10
+ throw new TypeError(`Invalid data URI: ${uri.slice(0, 50)}...`);
11
+ }
12
+
13
+ const typeFull = match[1] || 'text/plain;charset=US-ASCII';
14
+ const isBase64 = !!match[2];
15
+ const data = match[3];
16
+
17
+ let buffer: Uint8Array;
18
+ if (isBase64) {
19
+ const binaryString = atob(data);
20
+ buffer = new Uint8Array(binaryString.length);
21
+ for (let i = 0; i < binaryString.length; i++) {
22
+ buffer[i] = binaryString.charCodeAt(i);
23
+ }
24
+ } else {
25
+ buffer = new TextEncoder().encode(decodeURIComponent(data));
26
+ }
27
+
28
+ return { buffer, typeFull };
29
+ }