From 551b8df117d0133c9c8a489506c0d1954e693563 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 26 Aug 2026 17:14:42 +0100 Subject: [PATCH] polyfill abort signal methods --- src/platform/polyfills.ts | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts index 9765281312..213ef0925a 100644 --- a/src/platform/polyfills.ts +++ b/src/platform/polyfills.ts @@ -1,3 +1,76 @@ import 'fast-text-encoding' +/* + * React Native installs abort-controller@3 as its AbortController global. That + * package predates AbortSignal.reason and AbortSignal.throwIfAborted(), while + * newer libraries such as @atproto/lex-client expect the modern DOM API. + * + * Patch the shared prototype once so every signal - including signals created + * by dependencies - has browser-compatible abort behavior. The reason is + * stored before dispatching the abort event so listeners can read it + * synchronously, and the first abort reason wins as required by the DOM spec. + */ +const abortReasons = new WeakMap() +const abortSignalPrototype = AbortSignal.prototype as AbortSignal & { + readonly reason?: unknown + throwIfAborted?: () => void +} + +function createDefaultAbortReason(): unknown { + if (typeof DOMException !== 'undefined') { + return new DOMException('This operation was aborted', 'AbortError') + } + const error = new Error('This operation was aborted') + error.name = 'AbortError' + return error +} + +if (!('reason' in abortSignalPrototype)) { + const originalAbort = AbortController.prototype.abort + + Object.defineProperty(AbortController.prototype, 'abort', { + configurable: true, + writable: true, + value: function abort(this: AbortController, reason?: unknown) { + const signal = this.signal + if (!signal.aborted) { + abortReasons.set( + signal, + reason === undefined ? createDefaultAbortReason() : reason, + ) + } + try { + originalAbort.call(this) + } catch (error) { + if (!signal.aborted) abortReasons.delete(signal) + throw error + } + }, + }) + + Object.defineProperty(AbortSignal.prototype, 'reason', { + configurable: true, + get(this: AbortSignal) { + if (!this.aborted) return undefined + let reason = abortReasons.get(this) + if (reason === undefined) { + reason = createDefaultAbortReason() + abortReasons.set(this, reason) + } + return reason + }, + }) +} + +if (typeof abortSignalPrototype.throwIfAborted !== 'function') { + Object.defineProperty(AbortSignal.prototype, 'throwIfAborted', { + configurable: true, + writable: true, + value: function throwIfAborted(this: AbortSignal) { + if (!this.aborted) return + throw (this as AbortSignal & {readonly reason: unknown}).reason + }, + }) +} + export {}