@gjsify/fetch 0.35.0 → 0.36.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/lib/body.d.ts +18 -0
- package/lib/body.js +23 -1
- package/lib/esm/body.js +1 -1
- package/lib/index.spec.js +32 -0
- package/package.json +8 -8
package/lib/body.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export default class Body {
|
|
|
19
19
|
[INTERNALS]: {
|
|
20
20
|
body: null | Buffer | Readable | Blob;
|
|
21
21
|
stream: Readable | null;
|
|
22
|
+
/** Memoized `body` wrapper — see the getter for why identity matters. */
|
|
23
|
+
webStream: ReadableStream<Uint8Array> | null;
|
|
22
24
|
boundary: string;
|
|
23
25
|
disturbed: boolean;
|
|
24
26
|
error: null | FetchBaseError;
|
|
@@ -28,6 +30,22 @@ export default class Body {
|
|
|
28
30
|
size?: number;
|
|
29
31
|
headers?: unknown;
|
|
30
32
|
});
|
|
33
|
+
/**
|
|
34
|
+
* The body as a `ReadableStream`.
|
|
35
|
+
*
|
|
36
|
+
* Per the Fetch spec `body` is an ATTRIBUTE holding one stream, so every
|
|
37
|
+
* read must return the SAME object — `res.body === res.body`. This used to
|
|
38
|
+
* build a fresh wrapper per access, each one subscribing to the same
|
|
39
|
+
* already-flowing `Readable`: the first reader got every byte and every
|
|
40
|
+
* later one got an immediate `done`, with no error anywhere.
|
|
41
|
+
*
|
|
42
|
+
* That is not a theoretical spec detail. Consumers legitimately touch
|
|
43
|
+
* `.body` several times in a row — three.js' `FileLoader` guards with
|
|
44
|
+
* `response.body === undefined`, then `response.body.getReader === undefined`,
|
|
45
|
+
* and only then calls `response.body.getReader()`, so it read the THIRD,
|
|
46
|
+
* empty stream. Downstream: an empty response text, an LDraw model that
|
|
47
|
+
* parsed to zero meshes, and a viewer showing nothing but its clear colour.
|
|
48
|
+
*/
|
|
31
49
|
get body(): ReadableStream<Uint8Array> | null;
|
|
32
50
|
get _stream(): Readable;
|
|
33
51
|
/** Return the raw body buffer without consuming the stream (used by Request._send). */
|
package/lib/body.js
CHANGED
|
@@ -34,6 +34,7 @@ export default class Body {
|
|
|
34
34
|
[INTERNALS] = {
|
|
35
35
|
body: null,
|
|
36
36
|
stream: null,
|
|
37
|
+
webStream: null,
|
|
37
38
|
boundary: '',
|
|
38
39
|
disturbed: false,
|
|
39
40
|
error: null,
|
|
@@ -110,14 +111,33 @@ export default class Body {
|
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* The body as a `ReadableStream`.
|
|
116
|
+
*
|
|
117
|
+
* Per the Fetch spec `body` is an ATTRIBUTE holding one stream, so every
|
|
118
|
+
* read must return the SAME object — `res.body === res.body`. This used to
|
|
119
|
+
* build a fresh wrapper per access, each one subscribing to the same
|
|
120
|
+
* already-flowing `Readable`: the first reader got every byte and every
|
|
121
|
+
* later one got an immediate `done`, with no error anywhere.
|
|
122
|
+
*
|
|
123
|
+
* That is not a theoretical spec detail. Consumers legitimately touch
|
|
124
|
+
* `.body` several times in a row — three.js' `FileLoader` guards with
|
|
125
|
+
* `response.body === undefined`, then `response.body.getReader === undefined`,
|
|
126
|
+
* and only then calls `response.body.getReader()`, so it read the THIRD,
|
|
127
|
+
* empty stream. Downstream: an empty response text, an LDraw model that
|
|
128
|
+
* parsed to zero meshes, and a viewer showing nothing but its clear colour.
|
|
129
|
+
*/
|
|
113
130
|
get body() {
|
|
114
131
|
const stream = this[INTERNALS].stream;
|
|
115
132
|
if (!stream)
|
|
116
133
|
return null;
|
|
134
|
+
const memoized = this[INTERNALS].webStream;
|
|
135
|
+
if (memoized)
|
|
136
|
+
return memoized;
|
|
117
137
|
// If ReadableStream is available, wrap the Readable into one
|
|
118
138
|
if (typeof ReadableStream !== 'undefined') {
|
|
119
139
|
let closed = false;
|
|
120
|
-
|
|
140
|
+
const webStream = new ReadableStream({
|
|
121
141
|
start(controller) {
|
|
122
142
|
stream.on('data', (chunk) => {
|
|
123
143
|
if (closed)
|
|
@@ -160,6 +180,8 @@ export default class Body {
|
|
|
160
180
|
stream.destroy();
|
|
161
181
|
},
|
|
162
182
|
});
|
|
183
|
+
this[INTERNALS].webStream = webStream;
|
|
184
|
+
return webStream;
|
|
163
185
|
}
|
|
164
186
|
return null;
|
|
165
187
|
}
|
package/lib/esm/body.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{__name as e}from"./_virtual/_rolldown/runtime.js";import{Blob as t}from"./utils/blob-from.js";import{FetchBaseError as n}from"./errors/base.js";import{FetchError as r}from"./errors/fetch-error.js";import{isBlob as i,isURLSearchParameters as a}from"./utils/is.js";import{URLSearchParams as o}from"@gjsify/url";import{Buffer as s}from"node:buffer";import{PassThrough as c,Readable as l,Stream as u,pipeline as d}from"node:stream";import{FormData as f,formDataToBlob as p}from"@gjsify/formdata";const m=e((e,t)=>new Promise((n,r)=>{d(e,t,e=>{e?r(e):n()})}),`pipeline`),h=Symbol(`Body internals`);function isAnyArrayBuffer(e){return e instanceof ArrayBuffer||typeof SharedArrayBuffer<`u`&&e instanceof SharedArrayBuffer}var Body=class{[h]={body:null,stream:null,boundary:``,disturbed:!1,error:null};size=0;constructor(e,t={size:0}){if(this.size=t.size||0,e==null)this[h].body=null;else if(a(e))this[h].body=s.from(e.toString());else if(i(e))this[h].body=e;else if(s.isBuffer(e))this[h].body=e;else if(isAnyArrayBuffer(e))this[h].body=s.from(e);else if(ArrayBuffer.isView(e))this[h].body=s.from(e.buffer,e.byteOffset,e.byteLength);else if(e instanceof l)this[h].body=e;else if(typeof ReadableStream<`u`&&e instanceof ReadableStream)this[h].body=readableStreamToReadable(e);else if(e instanceof f){let t=p(e);this[h].body=t,this[h].boundary=t.type?.split(`boundary=`)?.[1]??``}else typeof e==`string`?this[h].body=s.from(e):e instanceof o?this[h].body=s.from(e.toString()):(console.warn(`Unknown body type "${typeof e}", try to parse the body to string!`),this[h].body=s.from(String(e)));let c=this[h].body;s.isBuffer(c)?this[h].stream=l.from(c):i(c)?this[h].stream=l.from(blobToAsyncIterable(c)):c instanceof l&&(this[h].stream=c),c instanceof u&&c.on(`error`,e=>{let t=e instanceof n?e:new r(`Invalid response body while trying to fetch ${this.url}: ${e.message}`,`system`,e);this[h].error=t})}get body(){let e=this[h].stream;if(!e)return null;if(typeof ReadableStream<`u`){let t=!1
|
|
1
|
+
import{__name as e}from"./_virtual/_rolldown/runtime.js";import{Blob as t}from"./utils/blob-from.js";import{FetchBaseError as n}from"./errors/base.js";import{FetchError as r}from"./errors/fetch-error.js";import{isBlob as i,isURLSearchParameters as a}from"./utils/is.js";import{URLSearchParams as o}from"@gjsify/url";import{Buffer as s}from"node:buffer";import{PassThrough as c,Readable as l,Stream as u,pipeline as d}from"node:stream";import{FormData as f,formDataToBlob as p}from"@gjsify/formdata";const m=e((e,t)=>new Promise((n,r)=>{d(e,t,e=>{e?r(e):n()})}),`pipeline`),h=Symbol(`Body internals`);function isAnyArrayBuffer(e){return e instanceof ArrayBuffer||typeof SharedArrayBuffer<`u`&&e instanceof SharedArrayBuffer}var Body=class{[h]={body:null,stream:null,webStream:null,boundary:``,disturbed:!1,error:null};size=0;constructor(e,t={size:0}){if(this.size=t.size||0,e==null)this[h].body=null;else if(a(e))this[h].body=s.from(e.toString());else if(i(e))this[h].body=e;else if(s.isBuffer(e))this[h].body=e;else if(isAnyArrayBuffer(e))this[h].body=s.from(e);else if(ArrayBuffer.isView(e))this[h].body=s.from(e.buffer,e.byteOffset,e.byteLength);else if(e instanceof l)this[h].body=e;else if(typeof ReadableStream<`u`&&e instanceof ReadableStream)this[h].body=readableStreamToReadable(e);else if(e instanceof f){let t=p(e);this[h].body=t,this[h].boundary=t.type?.split(`boundary=`)?.[1]??``}else typeof e==`string`?this[h].body=s.from(e):e instanceof o?this[h].body=s.from(e.toString()):(console.warn(`Unknown body type "${typeof e}", try to parse the body to string!`),this[h].body=s.from(String(e)));let c=this[h].body;s.isBuffer(c)?this[h].stream=l.from(c):i(c)?this[h].stream=l.from(blobToAsyncIterable(c)):c instanceof l&&(this[h].stream=c),c instanceof u&&c.on(`error`,e=>{let t=e instanceof n?e:new r(`Invalid response body while trying to fetch ${this.url}: ${e.message}`,`system`,e);this[h].error=t})}get body(){let e=this[h].stream;if(!e)return null;let t=this[h].webStream;if(t)return t;if(typeof ReadableStream<`u`){let t=!1,n=new ReadableStream({start(n){e.on(`data`,e=>{if(!t)try{n.enqueue(e instanceof Uint8Array?e:new Uint8Array(e))}catch{}}),e.on(`end`,()=>{if(!t){t=!0;try{n.close()}catch{}}}),e.on(`error`,e=>{if(!t){t=!0;try{n.error(e)}catch{}}})},cancel(){t=!0,e.destroy()}});return this[h].webStream=n,n}return null}get _stream(){return this[h].stream}get _rawBodyBuffer(){let e=this[h].body;return e===null?null:s.isBuffer(e)?e:e instanceof Uint8Array?s.from(e.buffer,e.byteOffset,e.byteLength):null}get bodyUsed(){return this[h].disturbed}async arrayBuffer(){let{buffer:e,byteOffset:t,byteLength:n}=await consumeBody(this);return e.slice(t,t+n)}async formData(){let e=this.headers?.get(`content-type`);if(e?.startsWith(`application/x-www-form-urlencoded`)){let e=new f,t=new o(await this.text());for(let[n,r]of t)e.append(n,r);return e}let{toFormData:t}=await import(`./utils/multipart-parser.js`);return t(this.body,e)}async blob(){let e=this.headers?.get(`content-type`)||this[h].body&&this[h].body.type||``;return new t([await this.arrayBuffer()],{type:e})}async json(){let e=await this.text();return JSON.parse(e)}async text(){let e=await consumeBody(this);return new TextDecoder().decode(e)}};Object.defineProperties(Body.prototype,{body:{enumerable:!0},bodyUsed:{enumerable:!0},arrayBuffer:{enumerable:!0},blob:{enumerable:!0},json:{enumerable:!0},text:{enumerable:!0}});async function consumeBody(e){if(e[h].disturbed)throw TypeError(`body used already for: ${e.url}`);if(e[h].disturbed=!0,e[h].error)throw e[h].error;let{_stream:t}=e;if(t===null||!(t instanceof u))return s.alloc(0);let i=[],a=0;try{for await(let n of t){if(e.size>0&&a+n.length>e.size){let n=new r(`content size at ${e.url} over limit: ${e.size}`,`max-size`);throw t.destroy(n),n}a+=n.length,i.push(n)}}catch(t){let i=t instanceof Error?t:Error(String(t));throw t instanceof n?t:new r(`Invalid response body while trying to fetch ${e.url}: ${i.message}`,`system`,i)}try{return i.every(e=>typeof e==`string`)?s.from(i.join(``)):s.concat(i,a)}catch(t){let n=t instanceof Error?t:Error(String(t));throw new r(`Could not create Buffer from response body for ${e.url}: ${n.message}`,`system`,n)}}const clone=(e,t)=>{let n,r,{body:i}=e[h];if(e.bodyUsed)throw Error(`cannot clone body after it is used`);return i instanceof u&&typeof i.getBoundary!=`function`&&(n=new c({highWaterMark:t}),r=new c({highWaterMark:t}),i.pipe(n),i.pipe(r),e[h].stream=n,i=r),i},extractContentType=(e,t)=>e===null?null:typeof e==`string`?`text/plain;charset=UTF-8`:a(e)?`application/x-www-form-urlencoded;charset=UTF-8`:i(e)?e.type||null:s.isBuffer(e)||isAnyArrayBuffer(e)||ArrayBuffer.isView(e)?null:e instanceof f?`multipart/form-data; boundary=${t[h].boundary}`:e instanceof u?null:`text/plain;charset=UTF-8`,getTotalBytes=e=>{let{body:t}=e[h];if(t===null)return 0;if(i(t))return t.size;if(s.isBuffer(t))return t.length;if(t&&typeof t.getLengthSync==`function`){let e=t;return e.hasKnownLength&&e.hasKnownLength()?e.getLengthSync():null}return null},writeToStream=async(e,{body:t})=>{t===null?e.end():await m(t,e)};function readableStreamToReadable(e){let t=e.getReader();return new l({async read(){try{let{done:e,value:n}=await t.read();e?this.push(null):this.push(s.from(n))}catch(e){this.destroy(e)}},destroy(e,n){t.cancel().then(()=>n(null),n)}})}async function*blobToAsyncIterable(e){if(typeof e.stream==`function`){let t=e.stream().getReader();for(;;){let{done:e,value:n}=await t.read();if(e)break;yield n}}else yield new Uint8Array(await e.arrayBuffer())}export{clone,Body as default,extractContentType,getTotalBytes,writeToStream};
|
package/lib/index.spec.js
CHANGED
|
@@ -197,6 +197,38 @@ export default async () => {
|
|
|
197
197
|
await r.text();
|
|
198
198
|
expect(r.bodyUsed).toBe(true);
|
|
199
199
|
});
|
|
200
|
+
// `body` is an ATTRIBUTE holding one stream, not a factory. Reading it
|
|
201
|
+
// twice used to build two wrappers over the same flowing Readable, so
|
|
202
|
+
// the first reader drained everything and every later one saw an
|
|
203
|
+
// immediate `done` — silently, with no error. three.js' FileLoader
|
|
204
|
+
// reads `.body` three times in its feature guard before taking a
|
|
205
|
+
// reader, and so received an empty document.
|
|
206
|
+
await it('should return the same body stream on every read', async () => {
|
|
207
|
+
const r = new Response('stable body');
|
|
208
|
+
expect(r.body).toBe(r.body);
|
|
209
|
+
});
|
|
210
|
+
await it('should deliver the full body to a reader taken after re-reading body', async () => {
|
|
211
|
+
const r = new Response('the whole payload');
|
|
212
|
+
// Touch `.body` twice first, exactly like a feature guard does.
|
|
213
|
+
expect(r.body !== undefined).toBe(true);
|
|
214
|
+
expect(typeof r.body?.getReader).toBe('function');
|
|
215
|
+
const reader = r.body.getReader();
|
|
216
|
+
const chunks = [];
|
|
217
|
+
for (;;) {
|
|
218
|
+
const { done, value } = await reader.read();
|
|
219
|
+
if (done)
|
|
220
|
+
break;
|
|
221
|
+
chunks.push(value);
|
|
222
|
+
}
|
|
223
|
+
const total = chunks.reduce((n, c) => n + c.byteLength, 0);
|
|
224
|
+
const joined = new Uint8Array(total);
|
|
225
|
+
let offset = 0;
|
|
226
|
+
for (const c of chunks) {
|
|
227
|
+
joined.set(c, offset);
|
|
228
|
+
offset += c.byteLength;
|
|
229
|
+
}
|
|
230
|
+
expect(new TextDecoder().decode(joined)).toBe('the whole payload');
|
|
231
|
+
});
|
|
200
232
|
await it('Response.error() should return error response', async () => {
|
|
201
233
|
const r = Response.error();
|
|
202
234
|
expect(r.type).toBe('error');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Web and Node.js fetch module for Gjs",
|
|
5
5
|
"module": "lib/esm/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"fetch"
|
|
52
52
|
],
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@gjsify/cli": "^0.
|
|
55
|
-
"@gjsify/unit": "^0.
|
|
54
|
+
"@gjsify/cli": "^0.36.0",
|
|
55
|
+
"@gjsify/unit": "^0.36.0",
|
|
56
56
|
"@types/node": "^25.9.2",
|
|
57
57
|
"typescript": "^6.0.3"
|
|
58
58
|
},
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"@girs/gjs": "^4.1.0",
|
|
62
62
|
"@girs/glib-2.0": "^4.1.0",
|
|
63
63
|
"@girs/soup-3.0": "^4.1.0",
|
|
64
|
-
"@gjsify/dom-events": "^0.
|
|
65
|
-
"@gjsify/formdata": "^0.
|
|
66
|
-
"@gjsify/http": "^0.
|
|
67
|
-
"@gjsify/url": "^0.
|
|
68
|
-
"@gjsify/utils": "^0.
|
|
64
|
+
"@gjsify/dom-events": "^0.36.0",
|
|
65
|
+
"@gjsify/formdata": "^0.36.0",
|
|
66
|
+
"@gjsify/http": "^0.36.0",
|
|
67
|
+
"@gjsify/url": "^0.36.0",
|
|
68
|
+
"@gjsify/utils": "^0.36.0"
|
|
69
69
|
},
|
|
70
70
|
"gjsify": {
|
|
71
71
|
"runtimes": {
|