@gjsify/fetch 0.4.29 → 0.4.31

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.
@@ -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};
package/lib/request.js CHANGED
@@ -12,6 +12,56 @@ 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
  */
@@ -165,7 +215,11 @@ export class Request extends Body {
165
215
  let session = null;
166
216
  let message = null;
167
217
  if (scheme === 'http:' || scheme === 'https:') {
168
- 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();
169
223
  message = new Soup.Message({
170
224
  method,
171
225
  uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.NONE),
@@ -207,16 +261,9 @@ export class Request extends Body {
207
261
  if (!session || !message) {
208
262
  throw new Error('Cannot send request: no Soup session (non-HTTP URL?)');
209
263
  }
210
- // Soup auto-adds ContentDecoder to new sessions, but it decodes the body
211
- // without removing the Content-Encoding header, causing double-decompression
212
- // if we also run DecompressionStream below. Remove it so our JS-level
213
- // decompression in index.ts handles everything correctly.
214
- try {
215
- session.remove_feature_by_type(Soup.ContentDecoder.$gtype);
216
- }
217
- catch {
218
- /* not present */
219
- }
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.
220
267
  options.headers._appendToSoupMessage(message);
221
268
  // Attach the request body to the Soup message (needed for POST/PUT/PATCH).
222
269
  // Use _rawBodyBuffer to read the body without consuming the stream (the
@@ -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
+ };
@@ -13,7 +13,13 @@ import type 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): "no-referrer" | (URL & {
17
+ username: string;
18
+ password: string;
19
+ hash: string;
20
+ pathname: string;
21
+ search: string;
22
+ });
17
23
  /**
18
24
  * @see {@link https://w3c.github.io/webappsec-referrer-policy/#enumdef-referrerpolicy enum ReferrerPolicy}
19
25
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fetch",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
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.29",
52
- "@gjsify/unit": "^0.4.29",
51
+ "@gjsify/cli": "^0.4.31",
52
+ "@gjsify/unit": "^0.4.31",
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.29",
62
- "@gjsify/http": "^0.4.29",
63
- "@gjsify/url": "^0.4.29",
64
- "@gjsify/utils": "^0.4.29"
61
+ "@gjsify/formdata": "^0.4.31",
62
+ "@gjsify/http": "^0.4.31",
63
+ "@gjsify/url": "^0.4.31",
64
+ "@gjsify/utils": "^0.4.31"
65
65
  }
66
66
  }