@gjsify/fetch 0.4.0 → 0.4.4

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/package.json CHANGED
@@ -1,63 +1,66 @@
1
1
  {
2
- "name": "@gjsify/fetch",
3
- "version": "0.4.0",
4
- "description": "Web and Node.js fetch module for Gjs",
5
- "module": "lib/esm/index.js",
6
- "types": "lib/index.d.ts",
7
- "type": "module",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/esm/index.js"
2
+ "name": "@gjsify/fetch",
3
+ "version": "0.4.4",
4
+ "description": "Web and Node.js fetch module for Gjs",
5
+ "module": "lib/esm/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "files": [
8
+ "lib"
9
+ ],
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "default": "./lib/esm/index.js"
15
+ },
16
+ "./register": {
17
+ "types": "./lib/register.d.ts",
18
+ "default": "./lib/esm/register.js"
19
+ },
20
+ "./register/fetch": {
21
+ "default": "./lib/esm/register/fetch.js"
22
+ },
23
+ "./register/xhr": {
24
+ "default": "./lib/esm/register/xhr.js"
25
+ }
12
26
  },
13
- "./register": {
14
- "types": "./lib/register.d.ts",
15
- "default": "./lib/esm/register.js"
27
+ "sideEffects": [
28
+ "./lib/esm/register.js",
29
+ "./lib/esm/register/*.js"
30
+ ],
31
+ "scripts": {
32
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
33
+ "check": "tsc --noEmit",
34
+ "build": "gjsify run build:gjsify && gjsify run build:types",
35
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
36
+ "build:types": "tsc",
37
+ "build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
38
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
39
+ "build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
40
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
41
+ "test": "gjsify run build:gjsify && gjsify run build:test:node && gjsify run test:node",
42
+ "test:gjs": "gjsify run test.gjs.mjs",
43
+ "test:node": "node test.node.mjs"
16
44
  },
17
- "./register/fetch": {
18
- "default": "./lib/esm/register/fetch.js"
45
+ "keywords": [
46
+ "gjs",
47
+ "node",
48
+ "fetch"
49
+ ],
50
+ "devDependencies": {
51
+ "@gjsify/cli": "^0.4.4",
52
+ "@gjsify/unit": "^0.4.4",
53
+ "@types/node": "^25.6.2",
54
+ "typescript": "^6.0.3"
19
55
  },
20
- "./register/xhr": {
21
- "default": "./lib/esm/register/xhr.js"
56
+ "dependencies": {
57
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
58
+ "@girs/gjs": "4.0.0-rc.15",
59
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
60
+ "@girs/soup-3.0": "3.6.6-4.0.0-rc.15",
61
+ "@gjsify/formdata": "^0.4.4",
62
+ "@gjsify/http": "^0.4.4",
63
+ "@gjsify/url": "^0.4.4",
64
+ "@gjsify/utils": "^0.4.4"
22
65
  }
23
- },
24
- "sideEffects": [
25
- "./lib/esm/register.js",
26
- "./lib/esm/register/*.js"
27
- ],
28
- "scripts": {
29
- "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
30
- "check": "tsc --noEmit",
31
- "build": "yarn build:gjsify && yarn build:types",
32
- "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
33
- "build:types": "tsc",
34
- "build:test": "yarn build:test:gjs && yarn build:test:node",
35
- "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
36
- "build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
37
- "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
38
- "test": "yarn build:gjsify && yarn build:test:node && yarn test:node",
39
- "test:gjs": "gjsify run test.gjs.mjs",
40
- "test:node": "node test.node.mjs"
41
- },
42
- "keywords": [
43
- "gjs",
44
- "node",
45
- "fetch"
46
- ],
47
- "devDependencies": {
48
- "@gjsify/cli": "^0.4.0",
49
- "@gjsify/unit": "^0.4.0",
50
- "@types/node": "^25.6.2",
51
- "typescript": "^6.0.3"
52
- },
53
- "dependencies": {
54
- "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
55
- "@girs/gjs": "4.0.0-rc.15",
56
- "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
57
- "@girs/soup-3.0": "3.6.6-4.0.0-rc.15",
58
- "@gjsify/formdata": "^0.4.0",
59
- "@gjsify/http": "^0.4.0",
60
- "@gjsify/url": "^0.4.0",
61
- "@gjsify/utils": "^0.4.0"
62
- }
63
- }
66
+ }
package/src/body.ts DELETED
@@ -1,448 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Adapted from node-fetch (https://github.com/node-fetch/node-fetch/blob/main/src/body.js)
3
- // Copyright (c) node-fetch contributors. MIT license.
4
- // Modifications: Rewritten for GJS using libsoup 3.0 and @gjsify/url
5
-
6
- import { URLSearchParams } from '@gjsify/url';
7
- import { Blob } from './utils/blob-from.js';
8
-
9
- import { PassThrough, pipeline as pipelineCb, Readable, Stream, Writable } from 'node:stream';
10
- import { Buffer } from 'node:buffer';
11
-
12
- import { FormData, formDataToBlob } from '@gjsify/formdata';
13
-
14
- import { FetchError } from './errors/fetch-error.js';
15
- import { FetchBaseError } from './errors/base.js';
16
- import { isBlob, isURLSearchParameters } from './utils/is.js';
17
-
18
- import type { Request } from './request.js';
19
- import type { Response } from './response.js';
20
- import type { SystemError } from './types/index.js';
21
-
22
- const pipeline = (source: Readable, dest: Writable): Promise<void> =>
23
- new Promise((resolve, reject) => {
24
- pipelineCb(source, dest, (err) => {
25
- if (err) reject(err);
26
- else resolve();
27
- });
28
- });
29
-
30
- const INTERNALS = Symbol('Body internals');
31
-
32
- function isAnyArrayBuffer(val: unknown): val is ArrayBuffer {
33
- return val instanceof ArrayBuffer ||
34
- (typeof SharedArrayBuffer !== 'undefined' && val instanceof SharedArrayBuffer);
35
- }
36
-
37
- function isBoxedPrimitive(val: unknown): boolean {
38
- return (
39
- val instanceof String ||
40
- val instanceof Number ||
41
- val instanceof Boolean ||
42
- (typeof Symbol !== 'undefined' && val instanceof Symbol) ||
43
- (typeof BigInt !== 'undefined' && val instanceof (BigInt as unknown as typeof Number))
44
- );
45
- }
46
-
47
- /**
48
- * Body mixin
49
- *
50
- * Ref: https://fetch.spec.whatwg.org/#body
51
- *
52
- * @param body Readable stream
53
- * @param opts Response options
54
- */
55
- export default class Body {
56
-
57
- [INTERNALS]: {
58
- body: null | Buffer | Readable | Blob;
59
- stream: Readable | null;
60
- boundary: string;
61
- disturbed: boolean,
62
- error: null | FetchBaseError;
63
- } = {
64
- body: null,
65
- stream: null,
66
- boundary: '',
67
- disturbed: false,
68
- error: null,
69
- }
70
-
71
- size = 0;
72
-
73
- constructor(body: BodyInit | Readable | Blob | Buffer | null, options: { size?: number; headers?: unknown } = { size: 0 }) {
74
- this.size = options.size || 0;
75
- if (body === null || body === undefined) {
76
- // Body is undefined or null
77
- this[INTERNALS].body = null;
78
- } else if (isURLSearchParameters(body)) {
79
- // Body is a URLSearchParams
80
- this[INTERNALS].body = Buffer.from(body.toString())
81
- } else if (isBlob(body)) {
82
- // Body is blob
83
- this[INTERNALS].body = body as Blob;
84
- } else if (Buffer.isBuffer(body)) {
85
- // Body is Buffer
86
- this[INTERNALS].body = body;
87
- } else if (isAnyArrayBuffer(body)) {
88
- // Body is ArrayBuffer
89
- this[INTERNALS].body = Buffer.from(body);
90
- } else if (ArrayBuffer.isView(body)) {
91
- // Body is ArrayBufferView
92
- this[INTERNALS].body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
93
- } else if (body instanceof Readable) {
94
- // Body is Node.js stream
95
- this[INTERNALS].body = body;
96
- } else if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {
97
- // Body is Web ReadableStream — convert to Node.js Readable
98
- this[INTERNALS].body = readableStreamToReadable(body);
99
- } else if (body instanceof FormData) {
100
- // Body is FormData
101
- const blob = formDataToBlob(body) as Blob & globalThis.Blob;
102
- this[INTERNALS].body = blob;
103
- this[INTERNALS].boundary = blob.type?.split('boundary=')?.[1] ?? '';
104
- } else if (typeof body === 'string'){
105
- // String body
106
- this[INTERNALS].body = Buffer.from(body);
107
- } else if (body instanceof URLSearchParams){
108
- this[INTERNALS].body = Buffer.from(body.toString());
109
- } else {
110
- console.warn(`Unknown body type "${typeof body}", try to parse the body to string!`);
111
- this[INTERNALS].body = Buffer.from(String(body));
112
- }
113
-
114
- // Set up the internal stream
115
- const b = this[INTERNALS].body;
116
- if (Buffer.isBuffer(b)) {
117
- this[INTERNALS].stream = Readable.from(b);
118
- } else if (isBlob(b)) {
119
- this[INTERNALS].stream = Readable.from(blobToAsyncIterable(b as Blob));
120
- } else if (b instanceof Readable) {
121
- this[INTERNALS].stream = b;
122
- }
123
-
124
- if (b instanceof Stream) {
125
- b.on('error', (error_: Error) => {
126
- const error = error_ instanceof FetchBaseError
127
- ? error_
128
- : new FetchError(`Invalid response body while trying to fetch ${(this as unknown as Request).url}: ${error_.message}`, 'system', error_ as unknown as SystemError);
129
- this[INTERNALS].error = error;
130
- });
131
- }
132
- }
133
-
134
- get body(): ReadableStream<Uint8Array> | null {
135
- const stream = this[INTERNALS].stream;
136
- if (!stream) return null;
137
-
138
- // If ReadableStream is available, wrap the Readable into one
139
- if (typeof ReadableStream !== 'undefined') {
140
- let closed = false;
141
- return new ReadableStream<Uint8Array>({
142
- start(controller) {
143
- stream.on('data', (chunk: Buffer | Uint8Array) => {
144
- if (closed) return;
145
- try {
146
- controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
147
- } catch { /* consumer cancelled — drop */ }
148
- });
149
- stream.on('end', () => {
150
- if (closed) return;
151
- closed = true;
152
- // Defensive: consumer may have cancelled the stream already,
153
- // in which case .close() throws TypeError. Don't surface that
154
- // as an unhandled error in the nextTick queue.
155
- try { controller.close(); } catch { /* already closed/cancelled */ }
156
- });
157
- stream.on('error', (err: Error) => {
158
- if (closed) return;
159
- closed = true;
160
- try { controller.error(err); } catch { /* already closed/cancelled */ }
161
- });
162
- },
163
- cancel() {
164
- closed = true;
165
- stream.destroy();
166
- }
167
- });
168
- }
169
-
170
- return null;
171
- }
172
-
173
- get _stream() {
174
- return this[INTERNALS].stream;
175
- }
176
-
177
- /** Return the raw body buffer without consuming the stream (used by Request._send). */
178
- get _rawBodyBuffer(): Buffer | null {
179
- const b = this[INTERNALS].body;
180
- if (b === null) return null;
181
- if (Buffer.isBuffer(b)) return b;
182
- if (b instanceof Uint8Array) return Buffer.from(b.buffer, b.byteOffset, b.byteLength);
183
- return null;
184
- }
185
-
186
- get bodyUsed() {
187
- return this[INTERNALS].disturbed;
188
- }
189
-
190
- /**
191
- * Decode response as ArrayBuffer
192
- */
193
- async arrayBuffer(): Promise<ArrayBuffer> {
194
- const {buffer, byteOffset, byteLength} = await consumeBody(this);
195
- return buffer.slice(byteOffset, byteOffset + byteLength) as ArrayBuffer;
196
- }
197
-
198
- async formData(): Promise<FormData> {
199
- const ct = (this as unknown as Request).headers?.get('content-type');
200
-
201
- if (ct?.startsWith('application/x-www-form-urlencoded')) {
202
- const formData = new FormData();
203
- const parameters = new URLSearchParams(await this.text());
204
-
205
- for (const [name, value] of parameters) {
206
- formData.append(name, value);
207
- }
208
-
209
- return formData;
210
- }
211
-
212
- const {toFormData} = await import('./utils/multipart-parser.js');
213
- return toFormData(this.body, ct);
214
- }
215
-
216
- /**
217
- * Return raw response as Blob
218
- */
219
- async blob(): Promise<Blob> {
220
- const ct = ((this as unknown as Request).headers?.get('content-type')) || (this[INTERNALS].body && (this[INTERNALS].body as Blob).type) || '';
221
- const buf = await this.arrayBuffer();
222
-
223
- return new Blob([buf], {
224
- type: ct
225
- });
226
- }
227
-
228
- /**
229
- * Decode response as json
230
- */
231
- async json(): Promise<unknown> {
232
- const text = await this.text();
233
- return JSON.parse(text);
234
- }
235
-
236
- /**
237
- * Decode response as text
238
- */
239
- async text(): Promise<string> {
240
- const buffer = await consumeBody(this);
241
- return new TextDecoder().decode(buffer);
242
- }
243
- }
244
-
245
- // In browsers, all properties are enumerable.
246
- Object.defineProperties(Body.prototype, {
247
- body: {enumerable: true},
248
- bodyUsed: {enumerable: true},
249
- arrayBuffer: {enumerable: true},
250
- blob: {enumerable: true},
251
- json: {enumerable: true},
252
- text: {enumerable: true},
253
- });
254
-
255
- /**
256
- * Consume and convert an entire Body to a Buffer.
257
- */
258
- async function consumeBody(data: Body & { url?: string }): Promise<Buffer> {
259
- if (data[INTERNALS].disturbed) {
260
- throw new TypeError(`body used already for: ${data.url}`);
261
- }
262
-
263
- data[INTERNALS].disturbed = true;
264
-
265
- if (data[INTERNALS].error) {
266
- throw data[INTERNALS].error;
267
- }
268
-
269
- const { _stream: body } = data;
270
-
271
- // Body is null
272
- if (body === null) {
273
- return Buffer.alloc(0);
274
- }
275
-
276
- if (!(body instanceof Stream)) {
277
- return Buffer.alloc(0);
278
- }
279
-
280
- // Body is stream — consume it
281
- const accum: (Buffer | Uint8Array)[] = [];
282
- let accumBytes = 0;
283
-
284
- try {
285
- for await (const chunk of body) {
286
- if (data.size > 0 && accumBytes + chunk.length > data.size) {
287
- const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
288
- body.destroy(error);
289
- throw error;
290
- }
291
-
292
- accumBytes += chunk.length;
293
- accum.push(chunk);
294
- }
295
- } catch (error: unknown) {
296
- const err = error instanceof Error ? error : new Error(String(error));
297
- const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err as unknown as SystemError);
298
- throw error_;
299
- }
300
-
301
- try {
302
- if (accum.every(c => typeof c === 'string')) {
303
- return Buffer.from((accum as unknown as string[]).join(''));
304
- }
305
-
306
- return Buffer.concat(accum as Buffer[], accumBytes);
307
- } catch (error: unknown) {
308
- const err = error instanceof Error ? error : new Error(String(error));
309
- throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${err.message}`, 'system', err as unknown as SystemError);
310
- }
311
- }
312
-
313
- /**
314
- * Clone body given Res/Req instance
315
- */
316
- export const clone = <T extends Request | Response>(instance: T, highWaterMark?: number) => {
317
- let p1: PassThrough;
318
- let p2: PassThrough;
319
- let {body} = instance[INTERNALS];
320
-
321
- if (instance.bodyUsed) {
322
- throw new Error('cannot clone body after it is used');
323
- }
324
-
325
- if ((body instanceof Stream) && (typeof (body as unknown as Record<string, unknown>).getBoundary !== 'function')) {
326
- p1 = new PassThrough({highWaterMark});
327
- p2 = new PassThrough({highWaterMark});
328
- body.pipe(p1);
329
- body.pipe(p2);
330
- instance[INTERNALS].stream = p1;
331
- body = p2;
332
- }
333
-
334
- return body;
335
- };
336
-
337
- /**
338
- * Extract a Content-Type value from a body.
339
- */
340
- export const extractContentType = (body: BodyInit | Readable | Blob | Buffer | null, request: Request | Response): string | null => {
341
- if (body === null) {
342
- return null;
343
- }
344
-
345
- if (typeof body === 'string') {
346
- return 'text/plain;charset=UTF-8';
347
- }
348
-
349
- if (isURLSearchParameters(body)) {
350
- return 'application/x-www-form-urlencoded;charset=UTF-8';
351
- }
352
-
353
- if (isBlob(body)) {
354
- return (body as Blob & globalThis.Blob).type || null;
355
- }
356
-
357
- if (Buffer.isBuffer(body) || isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
358
- return null;
359
- }
360
-
361
- if (body instanceof FormData) {
362
- return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
363
- }
364
-
365
- if (body instanceof Stream) {
366
- return null;
367
- }
368
-
369
- return 'text/plain;charset=UTF-8';
370
- };
371
-
372
- /**
373
- * Get total bytes of a body.
374
- */
375
- export const getTotalBytes = (request: Request): number | null => {
376
- const { body } = request[INTERNALS];
377
-
378
- if (body === null) {
379
- return 0;
380
- }
381
-
382
- if (isBlob(body)) {
383
- return (body as Blob).size;
384
- }
385
-
386
- if (Buffer.isBuffer(body)) {
387
- return body.length;
388
- }
389
-
390
- if (body && typeof (body as unknown as Record<string, unknown>).getLengthSync === 'function') {
391
- const streamBody = body as unknown as { getLengthSync(): number; hasKnownLength?(): boolean };
392
- return streamBody.hasKnownLength && streamBody.hasKnownLength() ? streamBody.getLengthSync() : null;
393
- }
394
-
395
- return null;
396
- };
397
-
398
- /**
399
- * Write a Body to a Node.js WritableStream.
400
- */
401
- export const writeToStream = async (dest: Writable, {body}: {body: Readable | null}): Promise<void> => {
402
- if (body === null) {
403
- dest.end();
404
- } else {
405
- await pipeline(body, dest);
406
- }
407
- };
408
-
409
- /**
410
- * Convert a Web ReadableStream to a Node.js Readable.
411
- */
412
- function readableStreamToReadable(webStream: ReadableStream): Readable {
413
- const reader = webStream.getReader();
414
- return new Readable({
415
- async read() {
416
- try {
417
- const { done, value } = await reader.read();
418
- if (done) {
419
- this.push(null);
420
- } else {
421
- this.push(Buffer.from(value));
422
- }
423
- } catch (err) {
424
- this.destroy(err as Error);
425
- }
426
- },
427
- destroy(_err, callback) {
428
- reader.cancel().then(() => callback(null), callback);
429
- }
430
- });
431
- }
432
-
433
- /**
434
- * Convert a Blob to an async iterable for Readable.from().
435
- */
436
- async function* blobToAsyncIterable(blob: Blob): AsyncIterable<Uint8Array> {
437
- if (typeof blob.stream === 'function') {
438
- const reader = (blob.stream() as unknown as ReadableStream).getReader();
439
- while (true) {
440
- const { done, value } = await reader.read();
441
- if (done) break;
442
- yield value;
443
- }
444
- } else {
445
- // Fallback: read the entire blob at once
446
- yield new Uint8Array(await blob.arrayBuffer());
447
- }
448
- }
@@ -1,10 +0,0 @@
1
- import { FetchBaseError } from './base.js';
2
-
3
- /**
4
- * AbortError interface for cancelled requests
5
- */
6
- export class AbortError extends FetchBaseError {
7
- constructor(message: string, type = 'aborted') {
8
- super(message, type);
9
- }
10
- }
@@ -1,22 +0,0 @@
1
- export class FetchBaseError extends Error {
2
-
3
- type?: 'aborted' | string;
4
-
5
- constructor(message: string, type?: string) {
6
- super(message);
7
- // Hide custom error implementation details from end-users
8
- if (typeof Error.captureStackTrace === 'function') {
9
- Error.captureStackTrace(this, this.constructor);
10
- }
11
-
12
- this.type = type;
13
- }
14
-
15
- get name() {
16
- return this.constructor.name;
17
- }
18
-
19
- get [Symbol.toStringTag]() {
20
- return this.constructor.name;
21
- }
22
- }
@@ -1,26 +0,0 @@
1
- import { FetchBaseError } from './base.js';
2
- import type { SystemError } from '../types/index.js';
3
-
4
- /**
5
- * FetchError interface for operational errors
6
- */
7
- export class FetchError extends FetchBaseError {
8
- code: string;
9
- errno: string;
10
- erroredSysCall: string;
11
-
12
- /**
13
- * @param message Error message for human
14
- * @param type Error type for machine
15
- * @param systemError For Node.js system error
16
- */
17
- constructor(message: string, type?: string, systemError?: SystemError) {
18
- super(message, type);
19
- // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code
20
- if (systemError) {
21
- // eslint-disable-next-line no-multi-assign
22
- this.code = this.errno = systemError.code;
23
- this.erroredSysCall = systemError.syscall;
24
- }
25
- }
26
- }