@gjsify/fetch 0.4.28 → 0.4.30

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 CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Blob } from './utils/blob-from.js';
2
- import { Readable, Writable } from 'node:stream';
2
+ import type { Writable } from 'node:stream';
3
+ import { Readable } from 'node:stream';
3
4
  import { Buffer } from 'node:buffer';
4
5
  import { FormData } from '@gjsify/formdata';
5
6
  import { FetchBaseError } from './errors/base.js';
package/lib/body.js CHANGED
@@ -20,15 +20,7 @@ const pipeline = (source, dest) => new Promise((resolve, reject) => {
20
20
  });
21
21
  const INTERNALS = Symbol('Body internals');
22
22
  function isAnyArrayBuffer(val) {
23
- return val instanceof ArrayBuffer ||
24
- (typeof SharedArrayBuffer !== 'undefined' && val instanceof SharedArrayBuffer);
25
- }
26
- function isBoxedPrimitive(val) {
27
- return (val instanceof String ||
28
- val instanceof Number ||
29
- val instanceof Boolean ||
30
- (typeof Symbol !== 'undefined' && val instanceof Symbol) ||
31
- (typeof BigInt !== 'undefined' && val instanceof BigInt));
23
+ return val instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && val instanceof SharedArrayBuffer);
32
24
  }
33
25
  /**
34
26
  * Body mixin
@@ -133,7 +125,9 @@ export default class Body {
133
125
  try {
134
126
  controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
135
127
  }
136
- catch { /* consumer cancelled — drop */ }
128
+ catch {
129
+ /* consumer cancelled — drop */
130
+ }
137
131
  });
138
132
  stream.on('end', () => {
139
133
  if (closed)
@@ -145,7 +139,9 @@ export default class Body {
145
139
  try {
146
140
  controller.close();
147
141
  }
148
- catch { /* already closed/cancelled */ }
142
+ catch {
143
+ /* already closed/cancelled */
144
+ }
149
145
  });
150
146
  stream.on('error', (err) => {
151
147
  if (closed)
@@ -154,13 +150,15 @@ export default class Body {
154
150
  try {
155
151
  controller.error(err);
156
152
  }
157
- catch { /* already closed/cancelled */ }
153
+ catch {
154
+ /* already closed/cancelled */
155
+ }
158
156
  });
159
157
  },
160
158
  cancel() {
161
159
  closed = true;
162
160
  stream.destroy();
163
- }
161
+ },
164
162
  });
165
163
  }
166
164
  return null;
@@ -206,10 +204,12 @@ export default class Body {
206
204
  * Return raw response as Blob
207
205
  */
208
206
  async blob() {
209
- const ct = (this.headers?.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || '';
207
+ const ct = this.headers?.get('content-type') ||
208
+ (this[INTERNALS].body && this[INTERNALS].body.type) ||
209
+ '';
210
210
  const buf = await this.arrayBuffer();
211
211
  return new Blob([buf], {
212
- type: ct
212
+ type: ct,
213
213
  });
214
214
  }
215
215
  /**
@@ -271,11 +271,13 @@ async function consumeBody(data) {
271
271
  }
272
272
  catch (error) {
273
273
  const err = error instanceof Error ? error : new Error(String(error));
274
- const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err);
274
+ const error_ = error instanceof FetchBaseError
275
+ ? error
276
+ : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err);
275
277
  throw error_;
276
278
  }
277
279
  try {
278
- if (accum.every(c => typeof c === 'string')) {
280
+ if (accum.every((c) => typeof c === 'string')) {
279
281
  return Buffer.from(accum.join(''));
280
282
  }
281
283
  return Buffer.concat(accum, accumBytes);
@@ -295,7 +297,7 @@ export const clone = (instance, highWaterMark) => {
295
297
  if (instance.bodyUsed) {
296
298
  throw new Error('cannot clone body after it is used');
297
299
  }
298
- if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
300
+ if (body instanceof Stream && typeof body.getBoundary !== 'function') {
299
301
  p1 = new PassThrough({ highWaterMark });
300
302
  p2 = new PassThrough({ highWaterMark });
301
303
  body.pipe(p1);
@@ -385,7 +387,7 @@ function readableStreamToReadable(webStream) {
385
387
  },
386
388
  destroy(_err, callback) {
387
389
  reader.cancel().then(() => callback(null), callback);
388
- }
390
+ },
389
391
  });
390
392
  }
391
393
  /**
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{isAbortSignal as e}from"./utils/is.js";import t,{clone as n,extractContentType as r,getTotalBytes as i}from"./body.js";import a from"./headers.js";import{inputStreamToReadable as o,soupSendAsync as s}from"./utils/soup-helpers.js";import{DEFAULT_REFERRER_POLICY as c,determineRequestsReferrer as l,validateReferrerPolicy as u}from"./utils/referrer.js";import{URL as d}from"@gjsify/url";import f from"@girs/soup-3.0";import p from"@girs/glib-2.0";import m from"@girs/gio-2.0";const h=Symbol(`Request internals`),isRequest=e=>typeof e==`object`&&typeof e.url==`string`;var g=class Request extends t{cache;credentials;destination;get headers(){return this[h].headers}integrity;keepalive;get method(){return this[h].method}mode;get redirect(){return this[h].redirect}get referrer(){if(this[h].referrer===`no-referrer`)return``;if(this[h].referrer===`client`)return`about:client`;if(this[h].referrer)return this[h].referrer.toString()}get referrerPolicy(){return this[h].referrerPolicy}set referrerPolicy(e){this[h].referrerPolicy=u(e)}get signal(){return this[h].signal}get url(){return this[h].parsedURL.toString()}get _uri(){return p.Uri.parse(this.url,p.UriFlags.NONE)}get _session(){return this[h].session}get _message(){return this[h].message}get _inputStream(){return this[h].inputStream}get[Symbol.toStringTag](){return`Request`}[h];follow;compress=!1;counter=0;agent=``;highWaterMark=16384;insecureHTTPParser=!1;constructor(t,i){let o=t,s=i||{},c,l={};if(isRequest(t)?(c=new d(o.url),l=o):c=new d(t),c.username!==``||c.password!==``)throw TypeError(`${c} is an url with embedded credentials.`);let u=s.method||l.method||`GET`;if(/^(delete|get|head|options|post|put)$/i.test(u)&&(u=u.toUpperCase()),(i?.body!=null||isRequest(t)&&o.body!==null)&&(u===`GET`||u===`HEAD`))throw TypeError(`Request with GET/HEAD method cannot have body`);let m=i?.body?i.body:isRequest(t)&&o.body!==null?n(t):null;super(m,{size:s.size||0});let g=new a(i?.headers||o.headers||{});if(m!==null&&!g.has(`Content-Type`)){let e=r(m,this);e&&g.set(`Content-Type`,e)}let _=isRequest(t)?o.signal:null;if(i&&`signal`in i&&(_=i.signal),_!=null&&!e(_))throw TypeError(`Expected signal to be an instanceof AbortSignal or EventTarget`);let v=i?.referrer==null?o.referrer:i.referrer;if(v===``)v=`no-referrer`;else if(v){let e=new d(v);v=/^about:(\/\/)?client$/.test(e.toString())?`client`:e}else v=void 0;let y=c.protocol,b=null,x=null;(y===`http:`||y===`https:`)&&(b=new f.Session,x=new f.Message({method:u,uri:p.Uri.parse(c.toString(),p.UriFlags.NONE)})),this[h]={method:u,redirect:i?.redirect||o.redirect||`follow`,headers:g,parsedURL:c,signal:_,referrer:v,referrerPolicy:``,session:b,message:x},this.follow=s.follow===void 0?o.follow===void 0?20:o.follow:s.follow,this.compress=s.compress===void 0?o.compress===void 0?!0:o.compress:s.compress,this.counter=s.counter||o.counter||0,this.agent=s.agent||o.agent,this.highWaterMark=s.highWaterMark||o.highWaterMark||16384,this.insecureHTTPParser=s.insecureHTTPParser||o.insecureHTTPParser||!1,this.referrerPolicy=i?.referrerPolicy||o.referrerPolicy||``}async _send(e){let{session:t,message:n}=this[h];if(!t||!n)throw Error(`Cannot send request: no Soup session (non-HTTP URL?)`);try{t.remove_feature_by_type(f.ContentDecoder.$gtype)}catch{}e.headers._appendToSoupMessage(n);let r=this._rawBodyBuffer;if(r!==null&&r.byteLength>0){let t=e.headers.get(`content-type`)||null;n.set_request_body_from_bytes(t,new p.Bytes(r))}let i=new m.Cancellable;return this[h].inputStream=await s(t,n,p.PRIORITY_DEFAULT,i),this[h].readable=o(this[h].inputStream),{inputStream:this[h].inputStream,readable:this[h].readable,cancellable:i}}clone(){return new Request(this)}async arrayBuffer(){return super.arrayBuffer()}async blob(){return super.blob()}async formData(){return super.formData()}async json(){return super.json()}async text(){return super.text()}};Object.defineProperties(g.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0},referrer:{enumerable:!0},referrerPolicy:{enumerable:!0}});const getSoupRequestOptions=e=>{let{parsedURL:t}=e[h],n=new a(e[h].headers);n.has(`Accept`)||n.set(`Accept`,`*/*`);let r=null;if(e.body===null&&/^(post|put)$/i.test(e.method)&&(r=`0`),e.body!==null){let t=i(e);typeof t==`number`&&!Number.isNaN(t)&&(r=String(t))}r&&n.set(`Content-Length`,r),e.referrerPolicy===``&&(e.referrerPolicy=c),e.referrer&&e.referrer!==`no-referrer`?e[h].referrer=l(e):e[h].referrer=`no-referrer`,e[h].referrer instanceof d&&n.set(`Referer`,e.referrer),n.has(`User-Agent`)||n.set(`User-Agent`,`gjsify-fetch`),e.compress&&!n.has(`Accept-Encoding`)&&n.set(`Accept-Encoding`,`gzip, deflate`);let{agent:o}=e;return typeof o==`function`&&(o=o(t)),!n.has(`Connection`)&&!o&&n.set(`Connection`,`close`),{parsedURL:t,options:{headers:n}}};export{g as Request,g as default,getSoupRequestOptions};
1
+ import"./_virtual/_rolldown/runtime.js";import{isAbortSignal as e}from"./utils/is.js";import t,{clone as n,extractContentType as r,getTotalBytes as i}from"./body.js";import a from"./headers.js";import{inputStreamToReadable as o,soupSendAsync as s}from"./utils/soup-helpers.js";import{DEFAULT_REFERRER_POLICY as c,determineRequestsReferrer as l,validateReferrerPolicy as u}from"./utils/referrer.js";import{URL as d}from"@gjsify/url";import f from"@girs/soup-3.0";import p from"@girs/glib-2.0";import m from"@girs/gio-2.0";const h=Symbol(`Request internals`);let g=null;function getSharedSession(){if(g===null){g=new f.Session;try{g.remove_feature_by_type(f.ContentDecoder.$gtype)}catch{}}return g}const isRequest=e=>typeof e==`object`&&typeof e.url==`string`;var _=class Request extends t{cache;credentials;destination;get headers(){return this[h].headers}integrity;keepalive;get method(){return this[h].method}mode;get redirect(){return this[h].redirect}get referrer(){if(this[h].referrer===`no-referrer`)return``;if(this[h].referrer===`client`)return`about:client`;if(this[h].referrer)return this[h].referrer.toString()}get referrerPolicy(){return this[h].referrerPolicy}set referrerPolicy(e){this[h].referrerPolicy=u(e)}get signal(){return this[h].signal}get url(){return this[h].parsedURL.toString()}get _uri(){return p.Uri.parse(this.url,p.UriFlags.NONE)}get _session(){return this[h].session}get _message(){return this[h].message}get _inputStream(){return this[h].inputStream}get[Symbol.toStringTag](){return`Request`}[h];follow;compress=!1;counter=0;agent=``;highWaterMark=16384;insecureHTTPParser=!1;constructor(t,i){let o=t,s=i||{},c,l={};if(isRequest(t)?(c=new d(o.url),l=o):c=new d(t),c.username!==``||c.password!==``)throw TypeError(`${c} is an url with embedded credentials.`);let u=s.method||l.method||`GET`;if(/^(delete|get|head|options|post|put)$/i.test(u)&&(u=u.toUpperCase()),(i?.body!=null||isRequest(t)&&o.body!==null)&&(u===`GET`||u===`HEAD`))throw TypeError(`Request with GET/HEAD method cannot have body`);let m=i?.body?i.body:isRequest(t)&&o.body!==null?n(t):null;super(m,{size:s.size||0});let g=new a(i?.headers||o.headers||{});if(m!==null&&!g.has(`Content-Type`)){let e=r(m,this);e&&g.set(`Content-Type`,e)}let _=isRequest(t)?o.signal:null;if(i&&`signal`in i&&(_=i.signal),_!=null&&!e(_))throw TypeError(`Expected signal to be an instanceof AbortSignal or EventTarget`);let v=i?.referrer==null?o.referrer:i.referrer;if(v===``)v=`no-referrer`;else if(v){let e=new d(v);v=/^about:(\/\/)?client$/.test(e.toString())?`client`:e}else v=void 0;let y=c.protocol,b=null,x=null;(y===`http:`||y===`https:`)&&(b=getSharedSession(),x=new f.Message({method:u,uri:p.Uri.parse(c.toString(),p.UriFlags.NONE)})),this[h]={method:u,redirect:i?.redirect||o.redirect||`follow`,headers:g,parsedURL:c,signal:_,referrer:v,referrerPolicy:``,session:b,message:x},this.follow=s.follow===void 0?o.follow===void 0?20:o.follow:s.follow,this.compress=s.compress===void 0?o.compress===void 0?!0:o.compress:s.compress,this.counter=s.counter||o.counter||0,this.agent=s.agent||o.agent,this.highWaterMark=s.highWaterMark||o.highWaterMark||16384,this.insecureHTTPParser=s.insecureHTTPParser||o.insecureHTTPParser||!1,this.referrerPolicy=i?.referrerPolicy||o.referrerPolicy||``}async _send(e){let{session:t,message:n}=this[h];if(!t||!n)throw Error(`Cannot send request: no Soup session (non-HTTP URL?)`);e.headers._appendToSoupMessage(n);let r=this._rawBodyBuffer;if(r!==null&&r.byteLength>0){let t=e.headers.get(`content-type`)||null;n.set_request_body_from_bytes(t,new p.Bytes(r))}let i=new m.Cancellable;return this[h].inputStream=await s(t,n,p.PRIORITY_DEFAULT,i),this[h].readable=o(this[h].inputStream),{inputStream:this[h].inputStream,readable:this[h].readable,cancellable:i}}clone(){return new Request(this)}async arrayBuffer(){return super.arrayBuffer()}async blob(){return super.blob()}async formData(){return super.formData()}async json(){return super.json()}async text(){return super.text()}};Object.defineProperties(_.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0},referrer:{enumerable:!0},referrerPolicy:{enumerable:!0}});const getSoupRequestOptions=e=>{let{parsedURL:t}=e[h],n=new a(e[h].headers);n.has(`Accept`)||n.set(`Accept`,`*/*`);let r=null;if(e.body===null&&/^(post|put)$/i.test(e.method)&&(r=`0`),e.body!==null){let t=i(e);typeof t==`number`&&!Number.isNaN(t)&&(r=String(t))}r&&n.set(`Content-Length`,r),e.referrerPolicy===``&&(e.referrerPolicy=c),e.referrer&&e.referrer!==`no-referrer`?e[h].referrer=l(e):e[h].referrer=`no-referrer`,e[h].referrer instanceof d&&n.set(`Referer`,e.referrer),n.has(`User-Agent`)||n.set(`User-Agent`,`gjsify-fetch`),e.compress&&!n.has(`Accept-Encoding`)&&n.set(`Accept-Encoding`,`gzip, deflate`);let{agent:o}=e;return typeof o==`function`&&(o=o(t)),!n.has(`Connection`)&&!o&&n.set(`Connection`,`close`),{parsedURL:t,options:{headers:n}}};export{_ as Request,_ as default,getSoupRequestOptions};
@@ -1 +1 @@
1
- import"../_virtual/_rolldown/runtime.js";import{URL as e}from"@gjsify/url";import{isIP as t}from"node:net";function stripURLForUseAsAReferrer(t,n=!1){if(t==null||t===`no-referrer`)return`no-referrer`;let r=new e(t);return/^(about|blob|data):$/.test(r.protocol)?`no-referrer`:(r.username=``,r.password=``,r.hash=``,n&&(r.pathname=``,r.search=``),r)}const n=new Set([``,`no-referrer`,`no-referrer-when-downgrade`,`same-origin`,`origin`,`strict-origin`,`origin-when-cross-origin`,`strict-origin-when-cross-origin`,`unsafe-url`]),r=`strict-origin-when-cross-origin`;function validateReferrerPolicy(e){if(!n.has(e))throw TypeError(`Invalid referrerPolicy: ${e}`);return e}function isOriginPotentiallyTrustworthy(e){if(/^(http|ws)s:$/.test(e.protocol))return!0;let n=e.host.replace(/(^\[)|(]$)/g,``),r=t(n);return r===4&&/^127\./.test(n)||r===6&&/^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(n)?!0:e.host===`localhost`||e.host.endsWith(`.localhost`)?!1:e.protocol===`file:`}function isUrlPotentiallyTrustworthy(t){return/^about:(blank|srcdoc)$/.test(t.toString())||(typeof t==`string`&&(t=new e(t)),t.protocol===`data:`)||/^(blob|filesystem):$/.test(t.protocol)?!0:isOriginPotentiallyTrustworthy(t)}function determineRequestsReferrer(t,n={}){let{referrerURLCallback:r,referrerOriginCallback:i}=n;if(t.referrer===`no-referrer`||t.referrerPolicy===``)return null;let a=t.referrerPolicy;if(t.referrer===`about:client`)return`no-referrer`;let o=new e(t.referrer),s=stripURLForUseAsAReferrer(o),c=stripURLForUseAsAReferrer(o,!0);s.toString().length>4096&&(s=c),r&&(s=r(s)),i&&(c=i(c));let l=new e(t.url);switch(a){case`no-referrer`:return`no-referrer`;case`origin`:return c;case`unsafe-url`:return s;case`strict-origin`:return isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:c.toString();case`strict-origin-when-cross-origin`:return s.origin===l.origin?s:isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:c;case`same-origin`:return s.origin===l.origin?s:`no-referrer`;case`origin-when-cross-origin`:return s.origin===l.origin?s:c;case`no-referrer-when-downgrade`:return isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:s;default:throw TypeError(`Invalid referrerPolicy: ${a}`)}}function parseReferrerPolicyFromHeader(e){let t=(e.get(`referrer-policy`)||``).split(/[,\s]+/),r=``;for(let e of t)e&&n.has(e)&&(r=e);return r}export{r as DEFAULT_REFERRER_POLICY,n as ReferrerPolicy,determineRequestsReferrer,isOriginPotentiallyTrustworthy,isUrlPotentiallyTrustworthy,parseReferrerPolicyFromHeader,stripURLForUseAsAReferrer,validateReferrerPolicy};
1
+ import"../_virtual/_rolldown/runtime.js";import{URL as e}from"@gjsify/url";import{isIP as t}from"node:net";function stripURLForUseAsAReferrer(t,n=!1){if(t==null||t===`no-referrer`)return`no-referrer`;let r=new e(t);return/^(about|blob|data):$/.test(r.protocol)?`no-referrer`:(r.username=``,r.password=``,r.hash=``,n&&(r.pathname=``,r.search=``),r)}const n=new Set([``,`no-referrer`,`no-referrer-when-downgrade`,`same-origin`,`origin`,`strict-origin`,`origin-when-cross-origin`,`strict-origin-when-cross-origin`,`unsafe-url`]),r=`strict-origin-when-cross-origin`;function validateReferrerPolicy(e){if(!n.has(e))throw TypeError(`Invalid referrerPolicy: ${e}`);return e}function isOriginPotentiallyTrustworthy(e){if(/^(http|ws)s:$/.test(e.protocol))return!0;let n=e.host.replace(/(^\[)|(]$)/g,``),r=t(n);return r===4&&n.startsWith(`127.`)||r===6&&/^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(n)?!0:e.host===`localhost`||e.host.endsWith(`.localhost`)?!1:e.protocol===`file:`}function isUrlPotentiallyTrustworthy(t){return/^about:(blank|srcdoc)$/.test(t.toString())||(typeof t==`string`&&(t=new e(t)),t.protocol===`data:`)||/^(blob|filesystem):$/.test(t.protocol)?!0:isOriginPotentiallyTrustworthy(t)}function determineRequestsReferrer(t,n={}){let{referrerURLCallback:r,referrerOriginCallback:i}=n;if(t.referrer===`no-referrer`||t.referrerPolicy===``)return null;let a=t.referrerPolicy;if(t.referrer===`about:client`)return`no-referrer`;let o=new e(t.referrer),s=stripURLForUseAsAReferrer(o),c=stripURLForUseAsAReferrer(o,!0);s.toString().length>4096&&(s=c),r&&(s=r(s)),i&&(c=i(c));let l=new e(t.url);switch(a){case`no-referrer`:return`no-referrer`;case`origin`:return c;case`unsafe-url`:return s;case`strict-origin`:return isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:c.toString();case`strict-origin-when-cross-origin`:return s.origin===l.origin?s:isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:c;case`same-origin`:return s.origin===l.origin?s:`no-referrer`;case`origin-when-cross-origin`:return s.origin===l.origin?s:c;case`no-referrer-when-downgrade`:return isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:s;default:throw TypeError(`Invalid referrerPolicy: ${a}`)}}function parseReferrerPolicyFromHeader(e){let t=(e.get(`referrer-policy`)||``).split(/[,\s]+/),r=``;for(let e of t)e&&n.has(e)&&(r=e);return r}export{r as DEFAULT_REFERRER_POLICY,n as ReferrerPolicy,determineRequestsReferrer,isOriginPotentiallyTrustworthy,isUrlPotentiallyTrustworthy,parseReferrerPolicyFromHeader,stripURLForUseAsAReferrer,validateReferrerPolicy};
package/lib/headers.js CHANGED
@@ -54,8 +54,8 @@ export default class Headers {
54
54
  }
55
55
  }
56
56
  else {
57
- throw new TypeError('Failed to construct \'Headers\': The provided value is not of type ' +
58
- '\'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)\'');
57
+ throw new TypeError("Failed to construct 'Headers': The provided value is not of type " +
58
+ "'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'");
59
59
  }
60
60
  }
61
61
  append(name, value) {
package/lib/index.js CHANGED
@@ -15,7 +15,7 @@ import { isRedirect } from './utils/is-redirect.js';
15
15
  import { FormData } from '@gjsify/formdata';
16
16
  import { isDomainOrSubdomain, isSameProtocol } from './utils/is.js';
17
17
  import { parseReferrerPolicyFromHeader } from './utils/referrer.js';
18
- import { Blob, File, } from './utils/blob-from.js';
18
+ import { Blob, File } from './utils/blob-from.js';
19
19
  import { URL } from '@gjsify/url';
20
20
  export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
21
21
  export { Blob, File };
@@ -30,9 +30,7 @@ function rewriteRootRelativeUrl(input) {
30
30
  const DEBUG = _g.__GJSIFY_DEBUG_FETCH === true;
31
31
  try {
32
32
  // GJS-only: derive program dir from System.programInvocationName.
33
- const programPath = _g.imports?.system?.programPath
34
- ?? _g.imports?.system?.programInvocationName
35
- ?? '';
33
+ const programPath = _g.imports?.system?.programPath ?? _g.imports?.system?.programInvocationName ?? '';
36
34
  if (!programPath)
37
35
  return input;
38
36
  const dir = GLib.path_get_dirname(programPath);
@@ -136,7 +134,7 @@ export default async function fetch(url, init = {}) {
136
134
  readable.destroy(new AbortError('The operation was aborted.'));
137
135
  });
138
136
  // Handle stream errors
139
- readable.on('error', (error) => {
137
+ readable.on('error', (_error) => {
140
138
  finalize();
141
139
  // Error is consumed by the body when read
142
140
  });
@@ -183,7 +181,7 @@ export default async function fetch(url, init = {}) {
183
181
  signal: request.signal,
184
182
  size: request.size,
185
183
  referrer: request.referrer,
186
- referrerPolicy: request.referrerPolicy
184
+ referrerPolicy: request.referrerPolicy,
187
185
  };
188
186
  // Don't forward sensitive headers to different domains/protocols
189
187
  if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
@@ -222,7 +220,7 @@ export default async function fetch(url, init = {}) {
222
220
  headers,
223
221
  size: request.size,
224
222
  counter: request.counter,
225
- highWaterMark: request.highWaterMark
223
+ highWaterMark: request.highWaterMark,
226
224
  };
227
225
  // Handle content encoding (decompression)
228
226
  const codings = headers.get('Content-Encoding');
@@ -232,7 +230,11 @@ export default async function fetch(url, init = {}) {
232
230
  // 3. no Content-Encoding header
233
231
  // 4. no content response (204)
234
232
  // 5. content not modified response (304)
235
- if (!request.compress || request.method === 'HEAD' || codings === null || statusCode === 204 || statusCode === 304) {
233
+ if (!request.compress ||
234
+ request.method === 'HEAD' ||
235
+ codings === null ||
236
+ statusCode === 204 ||
237
+ statusCode === 304) {
236
238
  finalize();
237
239
  return new Response(readable, responseOptions);
238
240
  }
package/lib/index.spec.js CHANGED
@@ -7,7 +7,7 @@ import { describe, it, expect, on } from '@gjsify/unit';
7
7
  export default async () => {
8
8
  await describe('fetch', async () => {
9
9
  await it('fetch should be a function', async () => {
10
- expect(typeof fetch).toBe("function");
10
+ expect(typeof fetch).toBe('function');
11
11
  });
12
12
  });
13
13
  await describe('Headers', async () => {
@@ -37,7 +37,10 @@ export default async () => {
37
37
  expect(h.has('x-remove')).toBe(false);
38
38
  });
39
39
  await it('should construct from array pairs', async () => {
40
- const h = new Headers([['a', '1'], ['b', '2']]);
40
+ const h = new Headers([
41
+ ['a', '1'],
42
+ ['b', '2'],
43
+ ]);
41
44
  expect(h.get('a')).toBe('1');
42
45
  expect(h.get('b')).toBe('2');
43
46
  });
@@ -71,15 +74,17 @@ export default async () => {
71
74
  });
72
75
  await describe('Headers forEach', async () => {
73
76
  await it('should iterate all headers with forEach', async () => {
74
- const h = new Headers({ 'a': '1', 'b': '2' });
77
+ const h = new Headers({ a: '1', b: '2' });
75
78
  const collected = [];
76
- h.forEach((value, key) => { collected.push(`${key}:${value}`); });
79
+ h.forEach((value, key) => {
80
+ collected.push(`${key}:${value}`);
81
+ });
77
82
  expect(collected.length).toBe(2);
78
83
  expect(collected[0]).toBe('a:1');
79
84
  expect(collected[1]).toBe('b:2');
80
85
  });
81
86
  await it('should return correct values() iterator', async () => {
82
- const h = new Headers({ 'x': 'hello', 'y': 'world' });
87
+ const h = new Headers({ x: 'hello', y: 'world' });
83
88
  const values = [...h.values()];
84
89
  expect(values.length).toBe(2);
85
90
  });
@@ -93,7 +98,7 @@ export default async () => {
93
98
  expect(entries[1][0]).toBe('x-custom');
94
99
  });
95
100
  await it('should support spread operator', async () => {
96
- const h = new Headers({ 'a': '1' });
101
+ const h = new Headers({ a: '1' });
97
102
  const arr = [...h];
98
103
  expect(arr.length).toBe(1);
99
104
  expect(arr[0][0]).toBe('a');
@@ -108,7 +113,7 @@ export default async () => {
108
113
  expect(() => h.append('invalid header', 'value')).toThrow();
109
114
  });
110
115
  await it('should support entries() for destructuring', async () => {
111
- const h = new Headers({ 'host': 'example.com', 'accept': '*/*' });
116
+ const h = new Headers({ host: 'example.com', accept: '*/*' });
112
117
  const obj = {};
113
118
  for (const [k, v] of h.entries())
114
119
  obj[k] = v;
@@ -128,7 +133,7 @@ export default async () => {
128
133
  });
129
134
  await it('should set custom headers', async () => {
130
135
  const r = new Request('https://example.com', {
131
- headers: { 'X-Custom': 'test' }
136
+ headers: { 'X-Custom': 'test' },
132
137
  });
133
138
  expect(r.headers.get('x-custom')).toBe('test');
134
139
  });
@@ -178,7 +183,7 @@ export default async () => {
178
183
  });
179
184
  await it('should parse json body', async () => {
180
185
  const r = new Response('{"key": "value"}');
181
- const json = await r.json();
186
+ const json = (await r.json());
182
187
  expect(json.key).toBe('value');
183
188
  });
184
189
  await it('should parse arrayBuffer body', async () => {
@@ -207,7 +212,7 @@ export default async () => {
207
212
  expect(r.status).toBe(200);
208
213
  const ct = r.headers.get('content-type') || '';
209
214
  expect(ct.includes('application/json')).toBe(true);
210
- const data = await r.json();
215
+ const data = (await r.json());
211
216
  expect(data.message).toBe('ok');
212
217
  });
213
218
  await it('should clone a response', async () => {
@@ -265,22 +270,30 @@ export default async () => {
265
270
  xhr.send();
266
271
  });
267
272
  await it('responseType="arraybuffer" yields ArrayBuffer', async () => {
268
- const xhr = await runXhr((x) => { x.responseType = 'arraybuffer'; });
273
+ const xhr = await runXhr((x) => {
274
+ x.responseType = 'arraybuffer';
275
+ });
269
276
  expect(xhr.response instanceof ArrayBuffer).toBe(true);
270
277
  expect(xhr.response.byteLength).toBe(5);
271
278
  });
272
279
  await it('responseType="text" yields decoded string', async () => {
273
- const xhr = await runXhr((x) => { x.responseType = 'text'; });
280
+ const xhr = await runXhr((x) => {
281
+ x.responseType = 'text';
282
+ });
274
283
  expect(xhr.response).toBe('hello');
275
284
  expect(xhr.responseText).toBe('hello');
276
285
  });
277
286
  await it('default responseType "" yields text', async () => {
278
- const xhr = await runXhr(() => { });
287
+ const xhr = await runXhr(() => {
288
+ /* responseType left at "" */
289
+ });
279
290
  expect(xhr.response).toBe('hello');
280
291
  expect(xhr.responseText).toBe('hello');
281
292
  });
282
293
  await it('responseType="blob" attaches _tmpPath for URL.createObjectURL', async () => {
283
- const xhr = await runXhr((x) => { x.responseType = 'blob'; });
294
+ const xhr = await runXhr((x) => {
295
+ x.responseType = 'blob';
296
+ });
284
297
  const blob = xhr.response;
285
298
  expect(blob instanceof Blob).toBe(true);
286
299
  expect(typeof blob._tmpPath).toBe('string');
package/lib/request.d.ts CHANGED
@@ -2,8 +2,8 @@ import GLib from '@girs/glib-2.0';
2
2
  import Soup from '@girs/soup-3.0';
3
3
  import Gio from '@girs/gio-2.0';
4
4
  import { URL } from '@gjsify/url';
5
- import { Blob } from './utils/blob-from.js';
6
- import { Readable } from 'node:stream';
5
+ import type { Blob } from './utils/blob-from.js';
6
+ import type { Readable } from 'node:stream';
7
7
  import Headers from './headers.js';
8
8
  import Body from './body.js';
9
9
  import type { FormData } from '@gjsify/formdata';
package/lib/request.js CHANGED
@@ -12,12 +12,61 @@ import Body, { clone, extractContentType, getTotalBytes } from './body.js';
12
12
  import { isAbortSignal } from './utils/is.js';
13
13
  import { validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY } from './utils/referrer.js';
14
14
  const INTERNALS = Symbol('Request internals');
15
+ /**
16
+ * Process-wide shared `Soup.Session`.
17
+ *
18
+ * Why a singleton and not a fresh `new Soup.Session()` per request:
19
+ *
20
+ * libsoup's connection manager keeps each open connection registered on a
21
+ * per-host list (`SoupHost.conns`). That list is only emptied when the
22
+ * connection's `disconnected` signal is processed on the main loop. When a
23
+ * `Soup.Session` is *finalized* its connection manager is torn down
24
+ * (`soup_connection_manager_free` → `soup_host_free`), which asserts the host
25
+ * has no live connections — `g_warn_if_fail (host->conns == NULL)`. If a
26
+ * session is dropped while a connection is still registered (the disconnect
27
+ * hasn't been pumped yet), libsoup prints:
28
+ *
29
+ * (gjs:…): libsoup-CRITICAL **: runtime check failed: (host->conns == NULL)
30
+ *
31
+ * Under GJS this is a SpiderMonkey-GC race: a per-request session (the old
32
+ * behavior — one `new Soup.Session()` per `Request`) becomes garbage as soon as
33
+ * the `Response` body is read, and a GC sweep can finalize it on the very same
34
+ * turn the connection is still being cleaned up. The same class of GLib/Soup
35
+ * BoxedInstance GC race is mitigated elsewhere in gjsify (e.g. `@gjsify/timers`
36
+ * uses `GLib.timeout_add` instead of holding `GLib.Source` boxed instances).
37
+ *
38
+ * Holding one long-lived session at module scope keeps it permanently reachable
39
+ * (never GC-finalized for the lifetime of the program), so the teardown path
40
+ * that fires the assertion is never reached mid-flight. It also lets libsoup
41
+ * pool and reuse keep-alive connections across requests — a real win for
42
+ * heavy fetch users like `@gjsify/npm-registry` during `gjsify install`.
43
+ *
44
+ * Lazily created so merely importing `@gjsify/fetch` (e.g. for `Headers`/
45
+ * `Request`/`Response` types on Node, or in a browser bundle) does not
46
+ * instantiate a Soup object.
47
+ */
48
+ let sharedSession = null;
49
+ function getSharedSession() {
50
+ if (sharedSession === null) {
51
+ sharedSession = new Soup.Session();
52
+ // Soup auto-adds a ContentDecoder to new sessions, but it decodes the
53
+ // body without removing the Content-Encoding header, causing
54
+ // double-decompression when index.ts also runs DecompressionStream.
55
+ // Remove it once here so our JS-level decompression handles everything.
56
+ try {
57
+ sharedSession.remove_feature_by_type(Soup.ContentDecoder.$gtype);
58
+ }
59
+ catch {
60
+ /* not present */
61
+ }
62
+ }
63
+ return sharedSession;
64
+ }
15
65
  /**
16
66
  * Check if `obj` is an instance of Request.
17
67
  */
18
68
  const isRequest = (obj) => {
19
- return (typeof obj === 'object' &&
20
- typeof obj.url === 'string');
69
+ return typeof obj === 'object' && typeof obj.url === 'string';
21
70
  };
22
71
  /** This Fetch API interface represents a resource request. */
23
72
  export class Request extends Body {
@@ -124,9 +173,13 @@ export class Request extends Body {
124
173
  (method === 'GET' || method === 'HEAD')) {
125
174
  throw new TypeError('Request with GET/HEAD method cannot have body');
126
175
  }
127
- const inputBody = init?.body ? init.body : (isRequest(input) && inputRL.body !== null ? clone(input) : null);
176
+ const inputBody = init?.body
177
+ ? init.body
178
+ : isRequest(input) && inputRL.body !== null
179
+ ? clone(input)
180
+ : null;
128
181
  super(inputBody, {
129
- size: initRL.size || 0
182
+ size: initRL.size || 0,
130
183
  });
131
184
  const headers = new Headers((init?.headers || inputRL.headers || {}));
132
185
  if (inputBody !== null && !headers.has('Content-Type')) {
@@ -135,9 +188,7 @@ export class Request extends Body {
135
188
  headers.set('Content-Type', contentType);
136
189
  }
137
190
  }
138
- let signal = isRequest(input) ?
139
- inputRL.signal :
140
- null;
191
+ let signal = isRequest(input) ? inputRL.signal : null;
141
192
  if (init && 'signal' in init) {
142
193
  signal = init.signal;
143
194
  }
@@ -164,7 +215,11 @@ export class Request extends Body {
164
215
  let session = null;
165
216
  let message = null;
166
217
  if (scheme === 'http:' || scheme === 'https:') {
167
- session = new Soup.Session();
218
+ // Reuse the process-wide shared session (see getSharedSession docs):
219
+ // a per-request session that gets GC-finalized while a connection is
220
+ // still registered on its host triggers libsoup's
221
+ // `host->conns == NULL` CRITICAL.
222
+ session = getSharedSession();
168
223
  message = new Soup.Message({
169
224
  method,
170
225
  uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.NONE),
@@ -182,8 +237,14 @@ export class Request extends Body {
182
237
  message,
183
238
  };
184
239
  // Node-fetch-only options
185
- this.follow = initRL.follow === undefined ? (inputRL.follow === undefined ? 20 : inputRL.follow) : initRL.follow;
186
- this.compress = initRL.compress === undefined ? (inputRL.compress === undefined ? true : inputRL.compress) : initRL.compress;
240
+ this.follow =
241
+ initRL.follow === undefined ? (inputRL.follow === undefined ? 20 : inputRL.follow) : initRL.follow;
242
+ this.compress =
243
+ initRL.compress === undefined
244
+ ? inputRL.compress === undefined
245
+ ? true
246
+ : inputRL.compress
247
+ : initRL.compress;
187
248
  this.counter = initRL.counter || inputRL.counter || 0;
188
249
  this.agent = initRL.agent || inputRL.agent;
189
250
  this.highWaterMark = initRL.highWaterMark || inputRL.highWaterMark || 16384;
@@ -200,14 +261,9 @@ export class Request extends Body {
200
261
  if (!session || !message) {
201
262
  throw new Error('Cannot send request: no Soup session (non-HTTP URL?)');
202
263
  }
203
- // Soup auto-adds ContentDecoder to new sessions, but it decodes the body
204
- // without removing the Content-Encoding header, causing double-decompression
205
- // if we also run DecompressionStream below. Remove it so our JS-level
206
- // decompression in index.ts handles everything correctly.
207
- try {
208
- session.remove_feature_by_type(Soup.ContentDecoder.$gtype);
209
- }
210
- catch { /* not present */ }
264
+ // ContentDecoder is removed once on the shared session in
265
+ // getSharedSession() (so the Content-Encoding header survives for our
266
+ // JS-level DecompressionStream in index.ts). Nothing per-request here.
211
267
  options.headers._appendToSoupMessage(message);
212
268
  // Attach the request body to the Soup message (needed for POST/PUT/PATCH).
213
269
  // Use _rawBodyBuffer to read the body without consuming the stream (the
@@ -224,7 +280,7 @@ export class Request extends Body {
224
280
  return {
225
281
  inputStream: this[INTERNALS].inputStream,
226
282
  readable: this[INTERNALS].readable,
227
- cancellable
283
+ cancellable,
228
284
  };
229
285
  }
230
286
  /**
@@ -257,7 +313,7 @@ Object.defineProperties(Request.prototype, {
257
313
  clone: { enumerable: true },
258
314
  signal: { enumerable: true },
259
315
  referrer: { enumerable: true },
260
- referrerPolicy: { enumerable: true }
316
+ referrerPolicy: { enumerable: true },
261
317
  });
262
318
  export default Request;
263
319
  /**
@@ -322,6 +378,6 @@ export const getSoupRequestOptions = (request) => {
322
378
  };
323
379
  return {
324
380
  parsedURL,
325
- options
381
+ options,
326
382
  };
327
383
  };
package/lib/response.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import Gio from '@girs/gio-2.0';
2
2
  import Headers from './headers.js';
3
3
  import Body from './body.js';
4
- import { Blob } from './utils/blob-from.js';
4
+ import type { Blob } from './utils/blob-from.js';
5
5
  import type { Readable } from 'node:stream';
6
6
  declare const INTERNALS: unique symbol;
7
7
  interface ResponseOptions {
package/lib/response.js CHANGED
@@ -38,7 +38,7 @@ export class Response extends Body {
38
38
  statusText: options.statusText || '',
39
39
  headers,
40
40
  counter: options.counter,
41
- highWaterMark: options.highWaterMark
41
+ highWaterMark: options.highWaterMark,
42
42
  };
43
43
  }
44
44
  get type() {
@@ -83,7 +83,7 @@ export class Response extends Body {
83
83
  ok: this.ok,
84
84
  redirected: this.redirected,
85
85
  size: this.size,
86
- highWaterMark: this.highWaterMark
86
+ highWaterMark: this.highWaterMark,
87
87
  });
88
88
  }
89
89
  /**
@@ -97,9 +97,9 @@ export class Response extends Body {
97
97
  }
98
98
  return new Response(null, {
99
99
  headers: {
100
- location: new URL(url).toString()
100
+ location: new URL(url).toString(),
101
101
  },
102
- status
102
+ status,
103
103
  });
104
104
  }
105
105
  static error() {
@@ -153,6 +153,6 @@ Object.defineProperties(Response.prototype, {
153
153
  redirected: { enumerable: true },
154
154
  statusText: { enumerable: true },
155
155
  headers: { enumerable: true },
156
- clone: { enumerable: true }
156
+ clone: { enumerable: true },
157
157
  });
158
158
  export default Response;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,118 @@
1
+ // Soup.Session lifecycle for GJS for @gjsify/fetch — original regression test.
2
+ //
3
+ // Regression: @gjsify/fetch used to create a fresh `new Soup.Session()` inside
4
+ // every Request constructor. Once the Response body was read, that per-request
5
+ // session became unreachable and SpiderMonkey could finalize it on the next GC
6
+ // sweep while a libsoup connection was still registered on its host. libsoup's
7
+ // connection-manager teardown then asserted `host->conns == NULL` and printed:
8
+ //
9
+ // (gjs:…): libsoup-CRITICAL **: runtime check failed: (host->conns == NULL)
10
+ //
11
+ // on stderr during heavy fetch use (notably @gjsify/npm-registry's packument/
12
+ // tarball fetches inside `gjsify install`). The fix holds ONE process-wide
13
+ // `Soup.Session` at module scope so it is never GC-finalized mid-flight.
14
+ //
15
+ // This file is GJS-only (`.gjs.spec.ts`). The suite body runs under
16
+ // `on('Gjs', …)`, so it is a no-op on Node. To keep the Node bundle free of
17
+ // `gi://*` / `system` imports (the same `test.mts` aggregator drives
18
+ // `test:node`), all GJS runtime objects are read from `globalThis.imports`
19
+ // (the GJS bootstrap) rather than statically imported — type-only imports give
20
+ // us the real shapes at compile time and are stripped at runtime.
21
+ import { describe, it, expect, on } from '@gjsify/unit';
22
+ export default async () => {
23
+ await on('Gjs', async () => {
24
+ // Read GJS runtime objects lazily (no static gi:// import → Node bundle
25
+ // stays clean). `imports` is the GJS bootstrap global.
26
+ const gjs = globalThis.imports;
27
+ const Soup = gjs.gi.Soup;
28
+ const GLib = gjs.gi.GLib;
29
+ const Gio = gjs.gi.Gio;
30
+ const System = gjs.system;
31
+ // Request + fetch are installed as globals by `@gjsify/fetch/register`,
32
+ // pulled into the test bundle by test.mts.
33
+ const RequestCtor = globalThis.Request;
34
+ const fetchFn = globalThis.fetch;
35
+ await describe('@gjsify/fetch — Soup.Session lifecycle (host->conns regression)', async () => {
36
+ await it('reuses one shared Soup.Session across HTTP requests (no per-request session)', () => {
37
+ // Two distinct HTTP Requests must share the very same Soup.Session
38
+ // instance. A per-request session is what triggered the GC race;
39
+ // a shared singleton can never be finalized while in use.
40
+ const a = new RequestCtor('http://example.com/a');
41
+ const b = new RequestCtor('https://example.org/b');
42
+ expect(a._session).toBeTruthy();
43
+ expect(b._session).toBeTruthy();
44
+ expect(a._session === b._session).toBe(true);
45
+ expect(a._session instanceof Soup.Session).toBe(true);
46
+ });
47
+ await it('non-HTTP requests do not allocate a Soup.Session', () => {
48
+ const dataReq = new RequestCtor('data:text/plain,hi');
49
+ expect(dataReq._session).toBeNull();
50
+ });
51
+ await it('many fetches + GC against a local server emit no host->conns / libsoup-CRITICAL warning', async () => {
52
+ // Exercise the live fetch path in-process first (the exact
53
+ // lifecycle: fetch → read body → drop refs → GC).
54
+ const server = new Soup.Server({});
55
+ server.add_handler(null, (_srv, msg) => {
56
+ msg.set_status(200, null);
57
+ msg.set_response('application/json', Soup.MemoryUse.COPY, new TextEncoder().encode('{"ok":true}'));
58
+ });
59
+ server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY);
60
+ const base = server.get_uris()[0].to_string();
61
+ for (let i = 0; i < 12; i++) {
62
+ const res = await fetchFn(base);
63
+ await res.json();
64
+ System.gc();
65
+ }
66
+ System.gc();
67
+ System.gc();
68
+ // Stronger assertion: run the same loop in a child gjs process and
69
+ // capture its stderr, so we can assert the libsoup warning is
70
+ // absent (GJS cannot redirect its own fd 2). The worker imports
71
+ // this package's built bundle by absolute path. If the bundle is
72
+ // not built (running specs from src only), skip the subprocess —
73
+ // the shared-session invariant above is the load-bearing check.
74
+ const pkgRoot = GLib.path_get_dirname(GLib.path_get_dirname(GLib.filename_from_uri(import.meta.url)[0]));
75
+ const fetchEntry = `${pkgRoot}/lib/esm/index.js`;
76
+ if (!GLib.file_test(fetchEntry, GLib.FileTest.EXISTS)) {
77
+ expect(true).toBe(true);
78
+ return;
79
+ }
80
+ const worker = `
81
+ imports.gi.versions.Soup = '3.0';
82
+ const { Soup, GLib } = imports.gi;
83
+ const System = imports.system;
84
+ const { default: fetch } = await import(${JSON.stringify('file://' + fetchEntry)});
85
+ const server = new Soup.Server({});
86
+ server.add_handler(null, (_s, msg) => {
87
+ msg.set_status(200, null);
88
+ msg.set_response('application/json', Soup.MemoryUse.COPY,
89
+ new TextEncoder().encode('{"ok":true}'));
90
+ });
91
+ server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY);
92
+ const base = server.get_uris()[0].to_string();
93
+ for (let i = 0; i < 20; i++) {
94
+ const res = await fetch(base);
95
+ await res.json();
96
+ System.gc();
97
+ }
98
+ System.gc(); System.gc();
99
+ `;
100
+ const proc = Gio.Subprocess.new(['gjs', '-m', '-c', worker], Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDOUT_SILENCE);
101
+ const stderrBytes = await new Promise((resolve, reject) => {
102
+ proc.communicate_async(null, null, (p, r) => {
103
+ try {
104
+ const [, , stderr] = p.communicate_finish(r);
105
+ resolve(stderr ? stderr.toArray() : new Uint8Array());
106
+ }
107
+ catch (e) {
108
+ reject(e instanceof Error ? e : new Error(String(e)));
109
+ }
110
+ });
111
+ });
112
+ const stderr = new TextDecoder().decode(stderrBytes);
113
+ expect(stderr.includes('host->conns')).toBe(false);
114
+ expect(stderr.includes('libsoup-CRITICAL')).toBe(false);
115
+ });
116
+ });
117
+ });
118
+ };
@@ -1,2 +1 @@
1
- ;
2
1
  export {};
@@ -1,2 +1,2 @@
1
1
  import { Blob, File } from 'node:buffer';
2
- export { Blob, File, };
2
+ export { Blob, File };
@@ -1,4 +1,4 @@
1
1
  // Re-export Blob/File from buffer (which provides the polyfill on GJS)
2
2
  // Reference: Node.js buffer.Blob (available since v18)
3
3
  import { Blob, File } from 'node:buffer';
4
- export { Blob, File, };
4
+ export { Blob, File };
@@ -5,6 +5,6 @@ const redirectStatus = new Set([301, 302, 303, 307, 308]);
5
5
  * @param {number} code - Status code
6
6
  * @return {boolean}
7
7
  */
8
- export const isRedirect = code => {
8
+ export const isRedirect = (code) => {
9
9
  return redirectStatus.has(code);
10
10
  };
package/lib/utils/is.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Blob } from './blob-from.js';
1
+ import type { Blob } from './blob-from.js';
2
2
  /**
3
3
  * Check if `obj` is a URLSearchParams object
4
4
  * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143
package/lib/utils/is.js CHANGED
@@ -11,7 +11,7 @@ const NAME = Symbol.toStringTag;
11
11
  * @param {*} object - Object to check for
12
12
  * @return {boolean}
13
13
  */
14
- export const isURLSearchParameters = object => {
14
+ export const isURLSearchParameters = (object) => {
15
15
  return (typeof object === 'object' &&
16
16
  typeof object.append === 'function' &&
17
17
  typeof object.delete === 'function' &&
@@ -44,8 +44,7 @@ export const isAbortSignal = (object) => {
44
44
  if (typeof object !== 'object' || object === null)
45
45
  return false;
46
46
  const obj = object;
47
- return (obj[NAME] === 'AbortSignal' ||
48
- obj[NAME] === 'EventTarget');
47
+ return obj[NAME] === 'AbortSignal' || obj[NAME] === 'EventTarget';
49
48
  };
50
49
  /**
51
50
  * isDomainOrSubdomain reports whether sub is a subdomain (or exact match) of
@@ -29,7 +29,7 @@ var S;
29
29
  let f = 1;
30
30
  const F = {
31
31
  PART_BOUNDARY: f,
32
- LAST_BOUNDARY: f *= 2
32
+ LAST_BOUNDARY: (f *= 2),
33
33
  };
34
34
  const LF = 10;
35
35
  const CR = 13;
@@ -38,7 +38,7 @@ const HYPHEN = 45;
38
38
  const COLON = 58;
39
39
  const A = 97;
40
40
  const Z = 122;
41
- const lower = c => c | 0x20;
41
+ const lower = (c) => c | 0x20;
42
42
  const noop = (..._args) => { };
43
43
  class MultipartParser {
44
44
  index = 0;
@@ -1,5 +1,5 @@
1
1
  import { URL } from '@gjsify/url';
2
- import Request from '../request.js';
2
+ import type Request from '../request.js';
3
3
  /**
4
4
  * @external URL
5
5
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL URL}
@@ -13,7 +13,7 @@ import Request from '../request.js';
13
13
  * @param url
14
14
  * @param originOnly
15
15
  */
16
- export declare function stripURLForUseAsAReferrer(url: null | URL | "no-referrer", originOnly?: boolean): URL | "no-referrer";
16
+ export declare function stripURLForUseAsAReferrer(url: null | URL | 'no-referrer', originOnly?: boolean): URL | "no-referrer";
17
17
  /**
18
18
  * @see {@link https://w3c.github.io/webappsec-referrer-policy/#enumdef-referrerpolicy enum ReferrerPolicy}
19
19
  */
@@ -15,7 +15,8 @@ import { isIP } from 'node:net';
15
15
  */
16
16
  export function stripURLForUseAsAReferrer(url, originOnly = false) {
17
17
  // 1. If url is null, return no referrer.
18
- if (url == null || url === "no-referrer") { // eslint-disable-line no-eq-null, eqeqeq
18
+ if (url == null || url === 'no-referrer') {
19
+ // eslint-disable-line no-eq-null, eqeqeq
19
20
  return 'no-referrer';
20
21
  }
21
22
  const u = new URL(url);
@@ -55,7 +56,7 @@ export const ReferrerPolicy = new Set([
55
56
  'strict-origin',
56
57
  'origin-when-cross-origin',
57
58
  'strict-origin-when-cross-origin',
58
- 'unsafe-url'
59
+ 'unsafe-url',
59
60
  ]);
60
61
  /**
61
62
  * @see {@link https://w3c.github.io/webappsec-referrer-policy/#default-referrer-policy default referrer policy}
@@ -89,7 +90,7 @@ export function isOriginPotentiallyTrustworthy(url) {
89
90
  // 4. If origin's host component matches one of the CIDR notations 127.0.0.0/8 or ::1/128 [RFC4632], return "Potentially Trustworthy".
90
91
  const hostIp = url.host.replace(/(^\[)|(]$)/g, '');
91
92
  const hostIPVersion = isIP(hostIp);
92
- if (hostIPVersion === 4 && /^127\./.test(hostIp)) {
93
+ if (hostIPVersion === 4 && hostIp.startsWith('127.')) {
93
94
  return true;
94
95
  }
95
96
  if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) {
@@ -1,5 +1,5 @@
1
- import Gio from '@girs/gio-2.0';
2
- import Soup from '@girs/soup-3.0';
1
+ import type Gio from '@girs/gio-2.0';
2
+ import type Soup from '@girs/soup-3.0';
3
3
  import { Readable } from 'node:stream';
4
4
  import type { ReadableOptions } from 'node:stream';
5
5
  /**
package/lib/xhr.js CHANGED
@@ -43,10 +43,7 @@ function guessBlobExt(url) {
43
43
  }
44
44
  }
45
45
  function writeBlobToTempFile(bytes, url) {
46
- const tmpPath = GLib.build_filenamev([
47
- GLib.get_tmp_dir(),
48
- `gjsify-blob-${_blobCounter++}${guessBlobExt(url)}`,
49
- ]);
46
+ const tmpPath = GLib.build_filenamev([GLib.get_tmp_dir(), `gjsify-blob-${_blobCounter++}${guessBlobExt(url)}`]);
50
47
  GLib.file_set_contents(tmpPath, bytes);
51
48
  return tmpPath;
52
49
  }
@@ -119,7 +116,9 @@ export class XMLHttpRequest extends EventTarget {
119
116
  if (this._aborted)
120
117
  return;
121
118
  const headersInit = {};
122
- this._headers.forEach((v, k) => { headersInit[k] = v; });
119
+ this._headers.forEach((v, k) => {
120
+ headersInit[k] = v;
121
+ });
123
122
  const fetchOptions = {
124
123
  method: this._method,
125
124
  headers: headersInit,
@@ -198,7 +197,7 @@ export class XMLHttpRequest extends EventTarget {
198
197
  const text = await res.text();
199
198
  // Strip UTF-8 BOM (U+FEFF) — browsers do this automatically; required
200
199
  // for JSON.parse to succeed on BOM-prefixed JSON responses.
201
- const stripped = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
200
+ const stripped = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
202
201
  this.responseText = stripped;
203
202
  this.response = stripped;
204
203
  break;
@@ -248,7 +247,9 @@ export class XMLHttpRequest extends EventTarget {
248
247
  if (this.onloadend)
249
248
  this.onloadend(new ProgressEvent('loadend'));
250
249
  }
251
- overrideMimeType(_mime) { }
250
+ overrideMimeType(_mime) {
251
+ /* no-op */
252
+ }
252
253
  _onTimeout() {
253
254
  if (this._aborted)
254
255
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fetch",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
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",
@@ -48,8 +48,8 @@
48
48
  "fetch"
49
49
  ],
50
50
  "devDependencies": {
51
- "@gjsify/cli": "^0.4.28",
52
- "@gjsify/unit": "^0.4.28",
51
+ "@gjsify/cli": "^0.4.30",
52
+ "@gjsify/unit": "^0.4.30",
53
53
  "@types/node": "^25.9.1",
54
54
  "typescript": "^6.0.3"
55
55
  },
@@ -58,9 +58,9 @@
58
58
  "@girs/gjs": "4.0.1",
59
59
  "@girs/glib-2.0": "2.88.0-4.0.1",
60
60
  "@girs/soup-3.0": "3.6.6-4.0.1",
61
- "@gjsify/formdata": "^0.4.28",
62
- "@gjsify/http": "^0.4.28",
63
- "@gjsify/url": "^0.4.28",
64
- "@gjsify/utils": "^0.4.28"
61
+ "@gjsify/formdata": "^0.4.30",
62
+ "@gjsify/http": "^0.4.30",
63
+ "@gjsify/url": "^0.4.30",
64
+ "@gjsify/utils": "^0.4.30"
65
65
  }
66
66
  }