diff --git a/docs/rxjs-next/ARCHITECTURE.md b/docs/rxjs-next/ARCHITECTURE.md index e39bcf6dbb..d2b1932720 100644 --- a/docs/rxjs-next/ARCHITECTURE.md +++ b/docs/rxjs-next/ARCHITECTURE.md @@ -179,6 +179,11 @@ iterators use explicit protocol loops so the fallback can preserve iterator method sampling, `return(reason)`, abort timing, and pending-result behavior that `for await...of` intentionally hides. +For subclasses that supply their own Subscriber implementation, promise +rejections use the public `subscriber.error()` method. Polyfill Subscribers +retain the internal error-reporting path that omits source-location information +for promise rejections. + The structure and behavior pass the pinned Observable WPT revision in window, dedicated-worker, same-origin iframe, and Web IDL coverage. D-045 supersedes D-042: the fallback again enforces the pinned revision's required-argument rule diff --git a/docs/rxjs-next/PROJECT_PLAN.md b/docs/rxjs-next/PROJECT_PLAN.md index f33b06389c..2094cd3cc0 100644 --- a/docs/rxjs-next/PROJECT_PLAN.md +++ b/docs/rxjs-next/PROJECT_PLAN.md @@ -3718,6 +3718,7 @@ conformance implementation depends on a runnable harness. remains the sole `NEXT` item until the command publishes and verifies `9.0.0-beta.0`, package access disallows automation tokens, and the immutable GitHub Release is recorded. + ### 2026-08-05 — P6.11 callback receiver removal - Audited the complete RxJS production source and removed callback `thisArg` @@ -3734,3 +3735,20 @@ conformance implementation depends on a runnable harness. the release performance floor. - Recorded D-059, marked P6.11 `DONE`, and retained P6.10 as the sole `NEXT` item. + +### 2026-09-16 — ColdObservable promise rejection fix + +- At the user's request, fixed `ColdObservable.from(promise)` rejection + delivery. Subclass-provided Subscribers use their public `error()` method; + polyfill Subscribers retain the existing source-location reporting behavior. +- Added regression coverage for multiple direct subscribers, a missing error + handler, and cancellation before rejection delivery. The original code + failed two assertions and produced four unhandled rejections; the fix passes. +- Validation passed: 753 RxJS source tests, 52 polyfill tests, both package + builds and public type checks, and both lints (existing warnings only). + The original reported reproduction now delivers the error without an + unhandled rejection. +- Pinned WPT validation passed all 52 URLs, 525 upstream subtests, and 52 RxJS + identity attestations at WPT `6a009d73f0d315941b90cac13a9523a2a08c631b` + in Chrome `150.0.7871.126`. +- P6.10 remains the sole `NEXT` item. diff --git a/packages/observable-polyfill/src/index.ts b/packages/observable-polyfill/src/index.ts index 44da7e6618..b454d496cf 100644 --- a/packages/observable-polyfill/src/index.ts +++ b/packages/observable-polyfill/src/index.ts @@ -569,7 +569,13 @@ class ObservableImpl implements Subscribable { subscriber.next(output); subscriber.complete(); }, - (error) => (subscriber as Subscriber)[errorSubscriber](error, false) + (error) => { + if (subscriber instanceof Subscriber) { + subscriber[errorSubscriber](error, false); + } else { + subscriber.error(error); + } + } ); }); } diff --git a/packages/rxjs/src/cold-observable.spec.ts b/packages/rxjs/src/cold-observable.spec.ts index a33f3b513e..bad609138a 100644 --- a/packages/rxjs/src/cold-observable.spec.ts +++ b/packages/rxjs/src/cold-observable.spec.ts @@ -45,6 +45,47 @@ describe('ColdObservable', () => { expect(reported).toEqual([expected]); }); + it('delivers promise rejections to every direct subscriber', async () => { + const expected = new Error('promise failed'); + const results: unknown[] = []; + const source = ColdObservable.from(Promise.reject(expected)); + const observer = { + next: (value: unknown) => results.push(value), + error: (error: unknown) => results.push(error), + complete: () => results.push('complete'), + }; + + expect(source).toBeInstanceOf(ColdObservable); + source.subscribe(observer); + source.subscribe(observer); + await Promise.resolve(); + + expect(results).toEqual([expected, expected]); + expect(reported).toEqual([]); + }); + + it('reports promise rejections when the observer has no error handler', async () => { + const expected = new Error('promise failed'); + + ColdObservable.from(Promise.reject(expected)).subscribe(); + await Promise.resolve(); + + expect(reported).toEqual([expected]); + }); + + it('ignores promise rejections after the observer aborts', async () => { + const expected = new Error('promise failed'); + const errors: unknown[] = []; + const controller = new AbortController(); + + ColdObservable.from(Promise.reject(expected)).subscribe({ error: (error) => errors.push(error) }, { signal: controller.signal }); + controller.abort(); + await Promise.resolve(); + + expect(errors).toEqual([]); + expect(reported).toEqual([]); + }); + it('is a platform Observable subclass', () => { const source = new ColdObservable(() => {});