@gjsify/fetch 0.0.3 → 0.1.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/README.md +27 -2
- package/globals.mjs +12 -0
- package/lib/body.d.ts +69 -0
- package/lib/body.js +375 -0
- package/lib/errors/abort-error.d.ts +7 -0
- package/lib/errors/abort-error.js +9 -0
- package/lib/errors/base.d.ts +6 -0
- package/lib/errors/base.js +17 -0
- package/lib/errors/fetch-error.d.ts +16 -0
- package/lib/errors/fetch-error.js +23 -0
- package/lib/esm/body.js +104 -56
- package/lib/esm/errors/base.js +3 -1
- package/lib/esm/headers.js +116 -131
- package/lib/esm/index.js +145 -190
- package/lib/esm/request.js +42 -41
- package/lib/esm/response.js +19 -4
- package/lib/esm/utils/blob-from.js +2 -98
- package/lib/esm/utils/data-uri.js +23 -0
- package/lib/esm/utils/is.js +7 -3
- package/lib/esm/utils/multipart-parser.js +5 -2
- package/lib/esm/utils/referrer.js +10 -10
- package/lib/esm/utils/soup-helpers.js +22 -0
- package/lib/headers.d.ts +33 -0
- package/lib/headers.js +195 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +205 -0
- package/lib/request.d.ts +101 -0
- package/lib/request.js +308 -0
- package/lib/response.d.ts +73 -0
- package/lib/response.js +158 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/system-error.d.ts +11 -0
- package/lib/types/system-error.js +2 -0
- package/lib/utils/blob-from.d.ts +2 -0
- package/lib/utils/blob-from.js +4 -0
- package/lib/utils/data-uri.d.ts +10 -0
- package/lib/utils/data-uri.js +27 -0
- package/lib/utils/get-search.d.ts +1 -0
- package/lib/utils/get-search.js +8 -0
- package/lib/utils/is-redirect.d.ts +7 -0
- package/lib/utils/is-redirect.js +10 -0
- package/lib/utils/is.d.ts +35 -0
- package/lib/utils/is.js +74 -0
- package/lib/utils/multipart-parser.d.ts +2 -0
- package/lib/utils/multipart-parser.js +396 -0
- package/lib/utils/referrer.d.ts +76 -0
- package/lib/utils/referrer.js +283 -0
- package/lib/utils/soup-helpers.d.ts +12 -0
- package/lib/utils/soup-helpers.js +25 -0
- package/package.json +23 -27
- package/src/body.ts +181 -169
- package/src/errors/base.ts +3 -1
- package/src/headers.ts +155 -202
- package/src/index.spec.ts +268 -3
- package/src/index.ts +199 -312
- package/src/request.ts +84 -75
- package/src/response.ts +48 -18
- package/src/test.mts +1 -1
- package/src/utils/blob-from.ts +4 -164
- package/src/utils/data-uri.ts +29 -0
- package/src/utils/is.ts +15 -15
- package/src/utils/multipart-parser.ts +3 -3
- package/src/utils/referrer.ts +11 -11
- package/src/utils/soup-helpers.ts +37 -0
- package/tsconfig.json +5 -5
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/body.js +0 -255
- package/lib/cjs/errors/abort-error.js +0 -9
- package/lib/cjs/errors/base.js +0 -17
- package/lib/cjs/errors/fetch-error.js +0 -21
- package/lib/cjs/headers.js +0 -202
- package/lib/cjs/index.js +0 -224
- package/lib/cjs/request.js +0 -281
- package/lib/cjs/response.js +0 -133
- package/lib/cjs/types/index.js +0 -1
- package/lib/cjs/types/system-error.js +0 -1
- package/lib/cjs/utils/blob-from.js +0 -101
- package/lib/cjs/utils/get-search.js +0 -11
- package/lib/cjs/utils/is-redirect.js +0 -7
- package/lib/cjs/utils/is.js +0 -28
- package/lib/cjs/utils/multipart-parser.js +0 -353
- package/lib/cjs/utils/referrer.js +0 -153
- package/test.gjs.js +0 -34758
- package/test.gjs.mjs +0 -53177
- package/test.node.js +0 -1226
- package/test.node.mjs +0 -6294
- package/tsconfig.types.json +0 -8
package/src/request.ts
CHANGED
|
@@ -1,38 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
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/
|
|
11
|
-
import { Blob } from '
|
|
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
|
-
|
|
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
|
|
51
|
+
typeof (obj as RequestLike).url === 'string'
|
|
32
52
|
);
|
|
33
53
|
};
|
|
34
54
|
|
|
35
|
-
// @ts-
|
|
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:
|
|
132
|
-
message: Soup.Message;
|
|
133
|
-
inputStream?: 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:
|
|
170
|
+
let requestObj: RequestLike = {};
|
|
148
171
|
|
|
149
172
|
if(isRequest(input)) {
|
|
150
|
-
parsedURL = new URL(
|
|
151
|
-
requestObj =
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
196
|
+
size: initRL.size || 0
|
|
174
197
|
});
|
|
175
198
|
|
|
176
|
-
const headers = new Headers((init
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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 =
|
|
230
|
-
this.compress =
|
|
231
|
-
this.counter =
|
|
232
|
-
this.agent =
|
|
233
|
-
this.highWaterMark =
|
|
234
|
-
this.insecureHTTPParser =
|
|
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
|
|
267
|
+
this.referrerPolicy = init?.referrerPolicy || inputRL.referrerPolicy || '';
|
|
239
268
|
}
|
|
240
269
|
|
|
241
270
|
/**
|
|
242
|
-
*
|
|
243
|
-
* @param options
|
|
244
|
-
* @returns
|
|
271
|
+
* Send the request using Soup.
|
|
245
272
|
*/
|
|
246
273
|
async _send(options: { headers: Headers }) {
|
|
247
|
-
|
|
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
|
|
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<
|
|
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
|
-
*
|
|
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
|
|
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', '
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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/
|
|
15
|
-
import { Blob } from '
|
|
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
|
|
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
|
|
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:
|
|
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, (
|
|
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
package/src/utils/blob-from.ts
CHANGED
|
@@ -1,168 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|