diff --git a/docs/rxjs-next/ARCHITECTURE.md b/docs/rxjs-next/ARCHITECTURE.md index e39bcf6dbb..1a87aed436 100644 --- a/docs/rxjs-next/ARCHITECTURE.md +++ b/docs/rxjs-next/ARCHITECTURE.md @@ -190,6 +190,11 @@ JavaScript does not expose the required DOM abort-algorithm hook; controllers without registered Observable algorithms delegate directly to the captured platform method. +If the realm's `AbortController` inherits its `abort` method, initialization +captures that callable and installs a configurable, writable, non-enumerable +bridge on the selected constructor's prototype, leaving the ancestor prototype +unchanged. Existing own method descriptors retain their attributes. + ### Shared native/fallback lifecycle contract P0.4 adds one self-contained contract under diff --git a/packages/observable-polyfill/src/index.ts b/packages/observable-polyfill/src/index.ts index 44da7e6618..991f2796a0 100644 --- a/packages/observable-polyfill/src/index.ts +++ b/packages/observable-polyfill/src/index.ts @@ -1380,7 +1380,14 @@ function initializeObservablePolyfill(): void { if (activeObservable === undefined) { const AbortControllerCtor = globalThis.AbortController; - const abortDescriptor = AbortControllerCtor && Object.getOwnPropertyDescriptor(AbortControllerCtor.prototype, 'abort'); + const abortDescriptor = + AbortControllerCtor && + (Object.getOwnPropertyDescriptor(AbortControllerCtor.prototype, 'abort') ?? { + configurable: true, + enumerable: false, + value: AbortControllerCtor.prototype.abort, + writable: true, + }); if (!AbortControllerCtor || !abortDescriptor || typeof abortDescriptor.value !== 'function') { throw new TypeError('Cannot initialize @rxjs/observable-polyfill: AbortController.prototype.abort is unavailable'); } diff --git a/packages/rxjs/test/import/fixture-scenario.mjs b/packages/rxjs/test/import/fixture-scenario.mjs index ea694eaf69..86c6b9ab46 100644 --- a/packages/rxjs/test/import/fixture-scenario.mjs +++ b/packages/rxjs/test/import/fixture-scenario.mjs @@ -49,6 +49,37 @@ if (scenario === 'missing-global-subpath') { assert.equal('filter' in root, false); assert.deepEqual(constructorDescriptions, ['rxjs.observable.polyfill.info.v1', 'rxjs.kernel.create.v1']); assert.deepEqual(prototypeDescriptions, ['Symbol.toStringTag', 'rxjs.kernel.create.v1']); +} else if (scenario === 'inherited-abort') { + Reflect.deleteProperty(globalThis, 'Observable'); + const NativeAbortController = globalThis.AbortController; + const nativeAbort = NativeAbortController.prototype.abort; + class WindowAbortController extends NativeAbortController {} + globalThis.AbortController = WindowAbortController; + assert.equal(Object.getOwnPropertyDescriptor(WindowAbortController.prototype, 'abort'), undefined); + + await import('rxjs'); + + assert.equal(typeof globalThis.Observable, 'function'); + assert.equal(NativeAbortController.prototype.abort, nativeAbort); + + const reason = new Error('cancelled'); + const events = []; + const controller = new AbortController(); + controller.signal.addEventListener('abort', () => events.push('abort')); + new Observable((subscriber) => { + subscriber.addTeardown(() => events.push(subscriber.signal.reason)); + }).subscribe(undefined, { signal: controller.signal }); + + controller.abort(reason); + + assert.equal(controller.signal.aborted, true); + assert.equal(controller.signal.reason, reason); + assert.deepEqual(events, [reason, 'abort']); + + const plainController = new AbortController(); + plainController.abort(reason); + assert.equal(plainController.signal.aborted, true); + assert.equal(plainController.signal.reason, reason); } else if (scenario === 'foreign-constructor') { class ForeignObservable {} class ForeignSubscriber {} diff --git a/packages/rxjs/test/import/fixtures.mjs b/packages/rxjs/test/import/fixtures.mjs index 8c7b799f9c..5bc1524c34 100644 --- a/packages/rxjs/test/import/fixtures.mjs +++ b/packages/rxjs/test/import/fixtures.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'; const scenarios = [ 'missing-global-subpath', 'root-core-only', + 'inherited-abort', 'foreign-constructor', 'earlier-version', 'event-target-when',