From 6754c1caace58c5041d985ccb1d75e09a2ac1566 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:20:37 +0000 Subject: [PATCH] Make AbortSignal a real constructor (prototype, throwIfAborted, events) (#267) The AbortController/AbortSignal globals were minimal stubs: AbortSignal had no prototype and no throwIfAborted, and AbortController.abort() only set a flag without firing listeners. Pages that read AbortSignal.prototype or call throwIfAborted threw non-fatal TypeErrors. AbortSignal is now a class with a prototype, aborted/reason, throwIfAborted(), working addEventListener/onabort firing, and static abort()/timeout()/any(). AbortController.abort(reason) fires the abort event. Bootstrap-only, no perf impact. --- crates/obscura-js/js/bootstrap.js | 86 ++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index b715dd1..ea1c27c 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -3822,8 +3822,90 @@ globalThis.ToggleEvent = class ToggleEvent extends Event { }; _markNative(globalThis.ToggleEvent); -globalThis.AbortController = class AbortController { constructor(){this.signal={aborted:false,addEventListener(){},removeEventListener(){},onabort:null};} abort(){this.signal.aborted=true;} }; -globalThis.AbortSignal = { timeout(ms){return {aborted:false,addEventListener(){},removeEventListener(){}}; } }; +// AbortController / AbortSignal. AbortSignal is a real constructor with a +// prototype, so feature-detection and `AbortSignal.prototype` access work. It +// carries aborted/reason, supports throwIfAborted(), and fires "abort" to +// onabort and addEventListener listeners when the controller aborts. +(function () { + const BRAND = Symbol("AbortSignal"); + function emit(signal, evt) { + if (typeof signal.onabort === "function") { + try { signal.onabort.call(signal, evt); } catch (_) {} + } + for (const cb of signal._listeners.slice()) { + const fn = typeof cb === "function" ? cb : cb && cb.handleEvent; + if (typeof fn === "function") { try { fn.call(signal, evt); } catch (_) {} } + } + } + function fire(signal, reason) { + if (signal._aborted) return; + signal._aborted = true; + signal._reason = reason !== undefined + ? reason + : new DOMException("signal is aborted without reason", "AbortError"); + const evt = typeof Event === "function" ? new Event("abort") : { type: "abort" }; + try { evt.target = signal; evt.currentTarget = signal; } catch (_) {} + emit(signal, evt); + } + globalThis.AbortSignal = class AbortSignal { + constructor(brand) { + if (brand !== BRAND) { + throw new TypeError("Failed to construct 'AbortSignal': Illegal constructor"); + } + this._aborted = false; + this._reason = undefined; + this._listeners = []; + this.onabort = null; + } + get aborted() { return this._aborted; } + get reason() { return this._reason; } + throwIfAborted() { if (this._aborted) throw this._reason; } + addEventListener(type, cb) { + if (type === "abort" && cb != null) this._listeners.push(cb); + } + removeEventListener(type, cb) { + if (type !== "abort") return; + const i = this._listeners.indexOf(cb); + if (i >= 0) this._listeners.splice(i, 1); + } + dispatchEvent(evt) { + if (evt && evt.type === "abort") emit(this, evt); + return true; + } + static abort(reason) { + const s = new AbortSignal(BRAND); + s._aborted = true; + s._reason = reason !== undefined + ? reason + : new DOMException("signal is aborted without reason", "AbortError"); + return s; + } + static timeout(ms) { + const s = new AbortSignal(BRAND); + setTimeout(() => fire(s, new DOMException("signal timed out", "TimeoutError")), ms); + return s; + } + static any(signals) { + const s = new AbortSignal(BRAND); + const list = Array.from(signals || []); + for (const sig of list) { + if (sig && sig.aborted) { s._aborted = true; s._reason = sig.reason; return s; } + } + for (const sig of list) { + if (sig && typeof sig.addEventListener === "function") { + sig.addEventListener("abort", () => fire(s, sig.reason)); + } + } + return s; + } + }; + globalThis.AbortController = class AbortController { + constructor() { this.signal = new globalThis.AbortSignal(BRAND); } + abort(reason) { fire(this.signal, reason); } + }; + _markNative(globalThis.AbortSignal); + _markNative(globalThis.AbortController); +})(); // Normalize one Blob part to bytes. `native` newline normalization applies to // string parts when the Blob/File `endings` option is "native". function _blobPartToBytes(p, native) {