@gjsify/xmlhttprequest 0.23.0 → 0.24.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/xmlhttprequest",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "XMLHttpRequest and URL.createObjectURL/revokeObjectURL for GJS — backed by @gjsify/fetch and GLib",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -42,10 +42,10 @@
42
42
  "@girs/gio-2.0": "^4.1.0",
43
43
  "@girs/gjs": "^4.1.0",
44
44
  "@girs/glib-2.0": "^4.1.0",
45
- "@gjsify/fetch": "^0.23.0"
45
+ "@gjsify/fetch": "^0.24.1"
46
46
  },
47
47
  "devDependencies": {
48
- "@gjsify/cli": "^0.23.0",
48
+ "@gjsify/cli": "^0.24.1",
49
49
  "@types/node": "^25.9.2",
50
50
  "typescript": "^6.0.3"
51
51
  },
package/lib/index.js DELETED
@@ -1,306 +0,0 @@
1
- // XMLHttpRequest and URL.createObjectURL/revokeObjectURL for GJS
2
- // Backed by @gjsify/fetch (Soup 3.0) for HTTP and GLib for file:// + temp files.
3
- // Reference: https://xhr.spec.whatwg.org/
4
- // oxlint-disable typescript/no-explicit-any -- W3C XMLHttpRequest surface uses `any` deliberately: `response: any` in lib.dom matches our impl (the actual shape varies by `responseType`: text/json/arraybuffer/blob); the `on{load,error,progress,…}` handler return types are `=> any` in lib.dom; the simplified progress events we dispatch carry varying-shape payloads. The remaining `any`s in send()'s body parameter, fetch-callback narrowing at the Soup boundary, listener-map value type, and catch-block error sit inside this same W3C surface and a `gioAsync`-style introspection boundary.
5
- import GLib from 'gi://GLib?version=2.0';
6
- import System from 'system';
7
- import fetch from '@gjsify/fetch';
8
- let _blobCounter = 0;
9
- // ---------------------------------------------------------------------------
10
- // FakeBlob — carries raw bytes + temp file path for the createObjectURL cycle
11
- // ---------------------------------------------------------------------------
12
- export class FakeBlob {
13
- _tmpPath;
14
- type;
15
- size;
16
- constructor(type, size) {
17
- this.type = type;
18
- this.size = size;
19
- }
20
- async arrayBuffer() {
21
- if (this._tmpPath) {
22
- const [ok, contents] = GLib.file_get_contents(this._tmpPath);
23
- if (ok)
24
- return contents.buffer;
25
- }
26
- return new ArrayBuffer(0);
27
- }
28
- async text() {
29
- return new TextDecoder().decode(await this.arrayBuffer());
30
- }
31
- stream() {
32
- const self = this;
33
- return new ReadableStream({
34
- async start(controller) {
35
- const buf = await self.arrayBuffer();
36
- controller.enqueue(new Uint8Array(buf));
37
- controller.close();
38
- },
39
- });
40
- }
41
- }
42
- // ---------------------------------------------------------------------------
43
- // Helpers
44
- // ---------------------------------------------------------------------------
45
- function guessExt(url) {
46
- const lower = url.toLowerCase();
47
- if (lower.endsWith('.png'))
48
- return '.png';
49
- if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
50
- return '.jpg';
51
- if (lower.endsWith('.gif'))
52
- return '.gif';
53
- if (lower.endsWith('.svg'))
54
- return '.svg';
55
- if (lower.endsWith('.ttf'))
56
- return '.ttf';
57
- if (lower.endsWith('.otf'))
58
- return '.otf';
59
- if (lower.endsWith('.woff'))
60
- return '.woff';
61
- if (lower.endsWith('.woff2'))
62
- return '.woff2';
63
- if (lower.endsWith('.mp3'))
64
- return '.mp3';
65
- if (lower.endsWith('.wav'))
66
- return '.wav';
67
- if (lower.endsWith('.ogg'))
68
- return '.ogg';
69
- if (lower.endsWith('.tmx') || lower.endsWith('.xml'))
70
- return '.xml';
71
- if (lower.endsWith('.json'))
72
- return '.json';
73
- return '.bin';
74
- }
75
- function guessMime(url) {
76
- const lower = url.toLowerCase();
77
- if (lower.endsWith('.png'))
78
- return 'image/png';
79
- if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
80
- return 'image/jpeg';
81
- if (lower.endsWith('.gif'))
82
- return 'image/gif';
83
- if (lower.endsWith('.svg'))
84
- return 'image/svg+xml';
85
- if (lower.endsWith('.ttf'))
86
- return 'font/truetype';
87
- if (lower.endsWith('.otf'))
88
- return 'font/otf';
89
- if (lower.endsWith('.woff'))
90
- return 'font/woff';
91
- if (lower.endsWith('.woff2'))
92
- return 'font/woff2';
93
- if (lower.endsWith('.mp3'))
94
- return 'audio/mpeg';
95
- if (lower.endsWith('.wav'))
96
- return 'audio/wav';
97
- if (lower.endsWith('.ogg'))
98
- return 'audio/ogg';
99
- if (lower.endsWith('.json'))
100
- return 'application/json';
101
- return 'application/octet-stream';
102
- }
103
- /** Read a file:// URL synchronously using GLib. */
104
- async function readFileUrl(url) {
105
- const path = GLib.filename_from_uri(url)[0];
106
- const [ok, contents] = GLib.file_get_contents(path);
107
- if (!ok)
108
- throw new Error(`XMLHttpRequest: cannot read file ${path}`);
109
- // GLib-backed Uint8Array's .buffer may not be a standard JS ArrayBuffer.
110
- // Copy into a fresh Uint8Array so callers get a proper ArrayBuffer.
111
- const src = contents;
112
- const copy = new Uint8Array(src.byteLength);
113
- copy.set(src);
114
- return copy.buffer;
115
- }
116
- /** Write bytes to a temp file and return the path. */
117
- function writeToTmp(bytes, ext) {
118
- const tmpPath = GLib.build_filenamev([GLib.get_tmp_dir(), `gjsify-blob-${_blobCounter++}${ext}`]);
119
- GLib.file_set_contents(tmpPath, bytes);
120
- return tmpPath;
121
- }
122
- export class XMLHttpRequest {
123
- // State
124
- UNSENT = 0;
125
- OPENED = 1;
126
- HEADERS_RECEIVED = 2;
127
- LOADING = 3;
128
- DONE = 4;
129
- readyState = 0;
130
- status = 0;
131
- statusText = '';
132
- response = null;
133
- responseText = '';
134
- responseType = '';
135
- responseURL = '';
136
- timeout = 0;
137
- withCredentials = false;
138
- // Event handler properties
139
- onloadstart = null;
140
- onprogress = null;
141
- onload = null;
142
- onloadend = null;
143
- onerror = null;
144
- onabort = null;
145
- ontimeout = null;
146
- onreadystatechange = null;
147
- _url = '';
148
- _method = 'GET';
149
- _aborted = false;
150
- _listeners = {};
151
- open(method, url, _async = true) {
152
- this._method = method.toUpperCase();
153
- this._url = url;
154
- this._aborted = false;
155
- this.readyState = this.OPENED;
156
- }
157
- setRequestHeader(_name, _value) {
158
- // Not needed for the XHR→Blob→Image chain
159
- }
160
- overrideMimeType(_mime) { }
161
- send(_body) {
162
- let url = this._url;
163
- const responseType = this.responseType;
164
- const DEBUG = globalThis.__GJSIFY_DEBUG_XHR === true;
165
- // Root-relative URLs (start with '/', not '//') have no host to
166
- // resolve against in GJS. Rewrite to file:// relative to the program
167
- // directory — matches how an HTTP static server would resolve them.
168
- if (url.startsWith('/') && !url.startsWith('//')) {
169
- const prog = System.programPath ?? System.programInvocationName ?? '';
170
- if (prog) {
171
- const programDir = GLib.path_get_dirname(prog);
172
- url = `file://${programDir}${this._url}`;
173
- if (DEBUG)
174
- console.log(`[xmlhttprequest] rewrite ${this._url} → ${url}`);
175
- }
176
- }
177
- if (DEBUG)
178
- console.log(`[xmlhttprequest] ${this._method} ${url} responseType=${responseType}`);
179
- const doFetch = () => {
180
- // Use GLib direct read for file:// URLs (avoids Soup for local files)
181
- if (url.startsWith('file://')) {
182
- return readFileUrl(url);
183
- }
184
- return fetch(url, { method: this._method }).then((r) => {
185
- if (DEBUG)
186
- console.log(`[xmlhttprequest] fetch ok ${url} status=${r.status}`);
187
- this.status = r.status === 0 ? 200 : r.status;
188
- this.statusText = r.statusText || 'OK';
189
- this.responseURL = r.url || url;
190
- return r.arrayBuffer();
191
- });
192
- };
193
- this.readyState = this.LOADING;
194
- this._emit('loadstart', { loaded: 0, total: 0, lengthComputable: false });
195
- // Wrap doFetch() in Promise.resolve().then(...) so synchronous throws
196
- // (e.g. new URL('/path') when fetch parses the input) propagate into
197
- // the .catch chain instead of escaping send() as an uncaught exception.
198
- Promise.resolve()
199
- .then(doFetch)
200
- .then((arrBuf) => {
201
- if (this._aborted)
202
- return;
203
- if (this.status === 0)
204
- this.status = 200;
205
- if (!this.statusText)
206
- this.statusText = 'OK';
207
- this.readyState = this.DONE;
208
- const bytes = new Uint8Array(arrBuf);
209
- const len = bytes.byteLength;
210
- this._emit('progress', { loaded: len, total: len, lengthComputable: true });
211
- if (responseType === 'blob' || responseType === '') {
212
- // Write to temp file so HTMLImageElement.src can load it via file://
213
- const ext = guessExt(url);
214
- const tmpPath = writeToTmp(bytes, ext);
215
- const blob = new FakeBlob(guessMime(url), len);
216
- blob._tmpPath = tmpPath;
217
- this.response = blob;
218
- }
219
- else if (responseType === 'arraybuffer') {
220
- this.response = arrBuf;
221
- }
222
- else if (responseType === 'json') {
223
- this.response = JSON.parse(new TextDecoder().decode(bytes));
224
- }
225
- else {
226
- // 'text' or ''
227
- this.responseText = new TextDecoder().decode(bytes);
228
- this.response = this.responseText;
229
- }
230
- this._emit('load', { target: this, loaded: len, total: len, lengthComputable: true });
231
- this._emit('loadend', { target: this, loaded: len, total: len, lengthComputable: true });
232
- if (this.onreadystatechange)
233
- this.onreadystatechange({});
234
- })
235
- .catch((err) => {
236
- if (this._aborted)
237
- return;
238
- console.warn(`[xmlhttprequest] ${this._method} ${url} — ${err?.message ?? err}`);
239
- this.readyState = this.DONE;
240
- this._emit('error', { error: err });
241
- this._emit('loadend', {});
242
- });
243
- }
244
- abort() {
245
- this._aborted = true;
246
- this.readyState = this.DONE;
247
- this._emit('abort', {});
248
- this._emit('loadend', {});
249
- }
250
- addEventListener(type, fn) {
251
- (this._listeners[type] ??= []).push(fn);
252
- }
253
- removeEventListener(type, fn) {
254
- this._listeners[type] = (this._listeners[type] ?? []).filter((f) => f !== fn);
255
- }
256
- getResponseHeader(_name) {
257
- return null;
258
- }
259
- getAllResponseHeaders() {
260
- return '';
261
- }
262
- _emit(type, event) {
263
- // Look up the matching `on<type>` handler property. The Node-style
264
- // `XMLHttpRequest` surface declares specific `onload` / `onerror` /
265
- // `onprogress` / `ontimeout` / `onabort` / `onreadystatechange`
266
- // properties, but indexing by computed `'on' + type` defeats
267
- // structural typing. Cast through a typed view keyed on the union.
268
- const handler = this[('on' + type)];
269
- if (typeof handler === 'function')
270
- handler.call(this, { type, ...event });
271
- for (const fn of this._listeners[type] ?? [])
272
- fn.call(this, { type, ...event });
273
- }
274
- }
275
- export function installObjectURLSupport() {
276
- const urlPatch = URL;
277
- // Only install if not already a real implementation
278
- if (typeof urlPatch.createObjectURL !== 'function' || urlPatch.__gjsify_objecturl !== true) {
279
- const _objectURLPaths = new Map();
280
- urlPatch.createObjectURL = function (blob) {
281
- if (blob._tmpPath) {
282
- const url = `file://${blob._tmpPath}`;
283
- _objectURLPaths.set(url, blob._tmpPath);
284
- return url;
285
- }
286
- // Fallback for real Blobs (shouldn't happen in GJS but handle gracefully)
287
- console.warn('[createObjectURL] received non-FakeBlob — cannot create file:// URL');
288
- return 'file:///dev/null';
289
- };
290
- urlPatch.revokeObjectURL = function (url) {
291
- const path = _objectURLPaths.get(url);
292
- if (path) {
293
- try {
294
- // Optionally clean up temp file
295
- const file = GLib.build_filenamev([path]);
296
- GLib.unlink(file);
297
- }
298
- catch {
299
- // ignore cleanup errors
300
- }
301
- _objectURLPaths.delete(url);
302
- }
303
- };
304
- urlPatch.__gjsify_objecturl = true;
305
- }
306
- }
package/lib/register.js DELETED
@@ -1,12 +0,0 @@
1
- // Side-effect module: registers XMLHttpRequest, URL.createObjectURL and
2
- // URL.revokeObjectURL as globals on GJS.
3
- // On Node.js the alias layer routes this subpath to @gjsify/empty — the native
4
- // implementations are already present there.
5
- import { XMLHttpRequest, installObjectURLSupport } from './index.js';
6
- const g = globalThis;
7
- if (typeof g.XMLHttpRequest === 'undefined') {
8
- g.XMLHttpRequest = XMLHttpRequest;
9
- }
10
- // Patch URL.createObjectURL / revokeObjectURL
11
- // URL is expected to be on globalThis already (via @gjsify/node-globals or @gjsify/url).
12
- installObjectURLSupport();
File without changes
File without changes