From 84aca48589cf8bf2550dc8e1c4cfd3d99bc6ae1a Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Thu, 24 Sep 2020 16:00:46 +1000 Subject: [PATCH 01/21] fix: add undefined to config Promise type And fix a spelling error. --- src/internal/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/config.ts b/src/internal/config.ts index a87f35fd4e..bff124f8d1 100644 --- a/src/internal/config.ts +++ b/src/internal/config.ts @@ -2,7 +2,7 @@ /** * The global configuration object for RxJS, used to configure things - * like what Promise contructor should used to create Promises + * like what Promise constructor should used to create Promises */ export const config = { /** @@ -24,7 +24,7 @@ export const config = { * Promise constructor. If you need a Promise implementation other than native promises, * please polyfill/patch Promises as you see appropriate. */ - Promise: undefined! as PromiseConstructorLike, + Promise: undefined as PromiseConstructorLike | undefined, /** * If true, turns on synchronous error rethrowing, which is a deprecated behavior From 5974a8ceba7b5f96a58201d8024c215a9eef04d5 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Thu, 24 Sep 2020 16:32:11 +1000 Subject: [PATCH 02/21] feat: add stopped registration point --- src/internal/config.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/internal/config.ts b/src/internal/config.ts index bff124f8d1..8d5d875702 100644 --- a/src/internal/config.ts +++ b/src/internal/config.ts @@ -1,5 +1,7 @@ /** @prettier */ +import { Subscriber } from './Subscriber'; + /** * The global configuration object for RxJS, used to configure things * like what Promise constructor should used to create Promises @@ -16,6 +18,20 @@ export const config = { */ onUnhandledError: null as ((err: any) => void) | null, + /** + * A registration point for notifications that cannot be sent to subscribers because they + * have completed, errored or have been explicitly unsubscribed. By default, next, complete + * and error notifications sent to stopped subscribers are noops. However, sometimes callers + * might want a different behavior. For example, with sources that attempt to report errors + * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead. + * This will _always_ be called asynchronously on another job in the runtime. This is because + * we do not want errors thrown in this user-configured handler to interfere with the + * behavior of the library. + */ + onStoppedNotification: undefined as + | ((notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, subscriber: Subscriber) => void) + | undefined, + /** * The promise constructor used by default for methods such as * {@link toPromise} and {@link forEach} From 674ddf00c9abe1374b6055d3155c0868c84d77f7 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Thu, 24 Sep 2020 17:21:15 +1000 Subject: [PATCH 03/21] chore: wire up Subscriber --- src/internal/Subscriber.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index 478bb4146a..e922e62359 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -62,7 +62,9 @@ export class Subscriber extends Subscription implements Observer { * @return {void} */ next(value?: T): void { - if (!this.isStopped) { + if (this.isStopped) { + handleStoppedNotification({ kind: 'N', value }, this); + } else { this._next(value!); } } @@ -74,10 +76,12 @@ export class Subscriber extends Subscription implements Observer { * @param {any} [err] The `error` exception. * @return {void} */ - error(err?: any): void { - if (!this.isStopped) { + error(error?: any): void { + if (this.isStopped) { + handleStoppedNotification({ kind: 'E', error }, this); + } else { this.isStopped = true; - this._error(err); + this._error(error); } } @@ -88,7 +92,9 @@ export class Subscriber extends Subscription implements Observer { * @return {void} */ complete(): void { - if (!this.isStopped) { + if (this.isStopped) { + handleStoppedNotification({ kind: 'C' }, this); + } else { this.isStopped = true; this._complete(); } @@ -181,6 +187,18 @@ function defaultErrorHandler(err: any) { reportUnhandledError(err); } +/** + * A handler for notifications that cannot be sent to a stopped subscriber. + * @param notification The notification being sent + * @param subscriber The stopped subscriber + */ +function handleStoppedNotification( + notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, + subscriber: Subscriber +) { + config.onStoppedNotification?.(notification, subscriber); +} + /** * The observer used as a stub for subscriptions where the user did not * pass any arguments to `subscribe`. Comes with the default error handling From f010cb30da176a6bc961d847aec1db6074c27a34 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Thu, 24 Sep 2020 17:34:04 +1000 Subject: [PATCH 04/21] chore: make call async --- src/internal/Subscriber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index e922e62359..579e2d6de7 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -196,7 +196,7 @@ function handleStoppedNotification( notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, subscriber: Subscriber ) { - config.onStoppedNotification?.(notification, subscriber); + setTimeout(() => config.onStoppedNotification?.(notification, subscriber)); } /** From b6d751a447ab6d632b9975c4d0bd272bc8a844a2 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 19:18:03 +1000 Subject: [PATCH 05/21] chore: use notification exports --- src/internal/Subscriber.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index 579e2d6de7..b18102011a 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -5,6 +5,7 @@ import { isSubscription, Subscription } from './Subscription'; import { config } from './config'; import { reportUnhandledError } from './util/reportUnhandledError'; import { noop } from './util/noop'; +import { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './Notification'; /** * Implements the {@link Observer} interface and extends the @@ -63,7 +64,7 @@ export class Subscriber extends Subscription implements Observer { */ next(value?: T): void { if (this.isStopped) { - handleStoppedNotification({ kind: 'N', value }, this); + handleStoppedNotification(nextNotification(value), this); } else { this._next(value!); } @@ -76,12 +77,12 @@ export class Subscriber extends Subscription implements Observer { * @param {any} [err] The `error` exception. * @return {void} */ - error(error?: any): void { + error(err?: any): void { if (this.isStopped) { - handleStoppedNotification({ kind: 'E', error }, this); + handleStoppedNotification(errorNotification(err), this); } else { this.isStopped = true; - this._error(error); + this._error(err); } } @@ -93,7 +94,7 @@ export class Subscriber extends Subscription implements Observer { */ complete(): void { if (this.isStopped) { - handleStoppedNotification({ kind: 'C' }, this); + handleStoppedNotification(COMPLETE_NOTIFICATION, this); } else { this.isStopped = true; this._complete(); From 4eb7175abfcdd7f34169fd7caba4f73c1de3ff60 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 19:19:02 +1000 Subject: [PATCH 06/21] chore: check callback before setTimeout --- src/internal/Subscriber.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index b18102011a..79a0928e47 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -197,7 +197,8 @@ function handleStoppedNotification( notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, subscriber: Subscriber ) { - setTimeout(() => config.onStoppedNotification?.(notification, subscriber)); + const { onStoppedNotification } = config; + onStoppedNotification && setTimeout(() => onStoppedNotification(notification, subscriber)); } /** From ee128f12ae79bcee060a8cc0b3915a199683af72 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 19:21:01 +1000 Subject: [PATCH 07/21] chore: use null --- src/internal/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/config.ts b/src/internal/config.ts index 8d5d875702..67c565b768 100644 --- a/src/internal/config.ts +++ b/src/internal/config.ts @@ -28,9 +28,9 @@ export const config = { * we do not want errors thrown in this user-configured handler to interfere with the * behavior of the library. */ - onStoppedNotification: undefined as + onStoppedNotification: null as | ((notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, subscriber: Subscriber) => void) - | undefined, + | null, /** * The promise constructor used by default for methods such as From 7baf2f5e3e3ee2042b183705f0898fac0c15f878 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 19:24:27 +1000 Subject: [PATCH 08/21] chore: api_guardian:update --- api_guard/dist/types/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index e69478ab60..94db9646c4 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -112,7 +112,8 @@ export declare function concat(...inputsAndSchedul export declare const config: { onUnhandledError: ((err: any) => void) | null; - Promise: PromiseConstructorLike; + stoppedObserver: import("./types").NextObserver | import("./types").ErrorObserver | import("./types").CompletionObserver | undefined; + Promise: PromiseConstructorLike | undefined; useDeprecatedSynchronousErrorHandling: boolean; useDeprecatedNextContext: boolean; }; From 3639842345b55bc6e6af4c55ff2658a1f6896722 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 19:58:46 +1000 Subject: [PATCH 09/21] refactor: move factories to avoid circular dep --- src/internal/Notification.ts | 40 ++----------------------- src/internal/NotificationFactories.ts | 42 +++++++++++++++++++++++++++ src/internal/Subscriber.ts | 2 +- 3 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 src/internal/NotificationFactories.ts diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index 0062de6e51..a0a40e7b2a 100644 --- a/src/internal/Notification.ts +++ b/src/internal/Notification.ts @@ -1,4 +1,5 @@ /** @prettier */ + import { PartialObserver, ObservableNotification, CompleteNotification, NextNotification, ErrorNotification } from './types'; import { Observable } from './Observable'; import { EMPTY } from './observable/empty'; @@ -239,41 +240,4 @@ export function observeNotification(notification: ObservableNotification, kind === 'N' ? observer.next?.(value!) : kind === 'E' ? observer.error?.(error) : observer.complete?.(); } -/** - * A completion object optimized for memory use and created to be the - * same "shape" as other notifications in v8. - * @internal - */ -export const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)(); - -/** - * Internal use only. Creates an optimized error notification that is the same "shape" - * as other notifications. - * @internal - */ -export function errorNotification(error: any): ErrorNotification { - return createNotification('E', undefined, error) as any; -} - -/** - * Internal use only. Creates an optimized next notification that is the same "shape" - * as other notifications. - * @internal - */ -export function nextNotification(value: T) { - return createNotification('N', value, undefined) as NextNotification; -} - -/** - * Ensures that all notifications created internally have the same "shape" in v8. - * - * TODO: This is only exported to support a crazy legacy test in `groupBy`. - * @internal - */ -export function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) { - return { - kind, - value, - error, - }; -} +export * from './NotificationFactories'; diff --git a/src/internal/NotificationFactories.ts b/src/internal/NotificationFactories.ts new file mode 100644 index 0000000000..02658aac38 --- /dev/null +++ b/src/internal/NotificationFactories.ts @@ -0,0 +1,42 @@ +/** @prettier */ + +import { CompleteNotification, NextNotification, ErrorNotification } from './types'; + +/** + * A completion object optimized for memory use and created to be the + * same "shape" as other notifications in v8. + * @internal + */ +export const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)(); + +/** + * Internal use only. Creates an optimized error notification that is the same "shape" + * as other notifications. + * @internal + */ +export function errorNotification(error: any): ErrorNotification { + return createNotification('E', undefined, error) as any; +} + +/** + * Internal use only. Creates an optimized next notification that is the same "shape" + * as other notifications. + * @internal + */ +export function nextNotification(value: T) { + return createNotification('N', value, undefined) as NextNotification; +} + +/** + * Ensures that all notifications created internally have the same "shape" in v8. + * + * TODO: This is only exported to support a crazy legacy test in `groupBy`. + * @internal + */ +export function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) { + return { + kind, + value, + error, + }; +} diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index 79a0928e47..665fc4a2a4 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -5,7 +5,7 @@ import { isSubscription, Subscription } from './Subscription'; import { config } from './config'; import { reportUnhandledError } from './util/reportUnhandledError'; import { noop } from './util/noop'; -import { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './Notification'; +import { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories'; /** * Implements the {@link Observer} interface and extends the From d726e4acda65cb98720e1f67ea4e2961a939b648 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 20:08:05 +1000 Subject: [PATCH 10/21] chore: api_guardian:update --- api_guard/dist/types/index.d.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index 94db9646c4..72ac4bb0fe 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -112,7 +112,15 @@ export declare function concat(...inputsAndSchedul export declare const config: { onUnhandledError: ((err: any) => void) | null; - stoppedObserver: import("./types").NextObserver | import("./types").ErrorObserver | import("./types").CompletionObserver | undefined; + onStoppedNotification: ((notification: { + kind: 'N'; + value: any; + } | { + kind: 'E'; + error: any; + } | { + kind: 'C'; + }, subscriber: Subscriber) => void) | null; Promise: PromiseConstructorLike | undefined; useDeprecatedSynchronousErrorHandling: boolean; useDeprecatedNextContext: boolean; From 939d0c97ef9ffbfaa701db33d969a58e592c89f5 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 25 Sep 2020 20:10:52 +1000 Subject: [PATCH 11/21] chore: remove empty lines --- src/internal/Notification.ts | 1 - src/internal/NotificationFactories.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index a0a40e7b2a..320fb30040 100644 --- a/src/internal/Notification.ts +++ b/src/internal/Notification.ts @@ -1,5 +1,4 @@ /** @prettier */ - import { PartialObserver, ObservableNotification, CompleteNotification, NextNotification, ErrorNotification } from './types'; import { Observable } from './Observable'; import { EMPTY } from './observable/empty'; diff --git a/src/internal/NotificationFactories.ts b/src/internal/NotificationFactories.ts index 02658aac38..bdf79c1b97 100644 --- a/src/internal/NotificationFactories.ts +++ b/src/internal/NotificationFactories.ts @@ -1,5 +1,4 @@ /** @prettier */ - import { CompleteNotification, NextNotification, ErrorNotification } from './types'; /** From c5fd79854e6a6e0b2829b78365f76f530a0f0f0f Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 26 Sep 2020 12:00:47 +1000 Subject: [PATCH 12/21] chore: remove canReportError and add tests --- spec/config-spec.ts | 168 +++++++++++++++++++++++-------- spec/util/canReportError-spec.ts | 24 ----- src/internal/Observable.ts | 22 +--- 3 files changed, 125 insertions(+), 89 deletions(-) delete mode 100644 spec/util/canReportError-spec.ts diff --git a/spec/config-spec.ts b/spec/config-spec.ts index 248bceae2d..2fcc4f190f 100644 --- a/spec/config-spec.ts +++ b/spec/config-spec.ts @@ -23,19 +23,19 @@ describe('config', () => { let called = false; const results: any[] = []; - config.onUnhandledError = err => { + config.onUnhandledError = (err) => { called = true; expect(err).to.equal('bad'); - done() + done(); }; - const source = new Observable(subscriber => { + const source = new Observable((subscriber) => { subscriber.next(1); subscriber.error('bad'); }); source.subscribe({ - next: value => results.push(value), + next: (value) => results.push(value), }); expect(called).to.be.false; expect(results).to.deep.equal([1]); @@ -45,31 +45,31 @@ describe('config', () => { let called = false; const results: any[] = []; - config.onUnhandledError = err => { + config.onUnhandledError = (err) => { called = true; expect(err).to.equal('bad'); - done() + done(); }; - const source = new Observable(subscriber => { + const source = new Observable((subscriber) => { subscriber.next(1); subscriber.error('bad'); }); - source.subscribe(value => results.push(value)); + source.subscribe((value) => results.push(value)); expect(called).to.be.false; expect(results).to.deep.equal([1]); }); it('should call asynchronously if an error is emitted and not handled by the consumer in the empty case', (done) => { let called = false; - config.onUnhandledError = err => { + config.onUnhandledError = (err) => { called = true; expect(err).to.equal('bad'); - done() + done(); }; - const source = new Observable(subscriber => { + const source = new Observable((subscriber) => { subscriber.error('bad'); }); @@ -77,41 +77,88 @@ describe('config', () => { expect(called).to.be.false; }); - it('should call asynchronously if a subscription setup errors after the subscription is closed by an error', (done) => { + /** + * This test is added so people know this behavior is _intentional_. It's part of the contract of observables + * and, while I'm not sure I like it, it might start surfacing untold numbers of errors, and break + * node applications if we suddenly changed this to start throwing errors on other jobs for instances + * where users accidentally called `subscriber.error` twice. Likewise, would we report an error + * for two calls of `complete`? This is really something a build-time tool like a linter should + * capture. Not a run time error reporting event. + */ + it('should not be called if two errors are sent to the subscriber', (done) => { let called = false; - config.onUnhandledError = err => { + config.onUnhandledError = () => { called = true; - expect(err).to.equal('bad'); - done() }; - const source = new Observable(subscriber => { + const source = new Observable((subscriber) => { + subscriber.error('handled'); + subscriber.error('swallowed'); + }); + + let syncSentError: any; + source.subscribe({ + error: (err) => { + syncSentError = err; + }, + }); + + expect(syncSentError).to.equal('handled'); + // This timeout would be scheduled _after_ any error timeout that might be scheduled + // (But we're not scheduling that), so this is just an artificial delay to make sure the + // behavior sticks. + setTimeout(() => { + expect(called).to.be.false; + done(); + }); + }); + }); + + describe('onStoppedNotification', () => { + afterEach(() => { + config.onStoppedNotification = null; + }); + + it('should default to null', () => { + expect(config.onStoppedNotification).to.be.null; + }); + + it('should be called asynchronously if a subscription setup errors after the subscription is closed by an error', (done) => { + let called = false; + config.onStoppedNotification = (notification) => { + called = true; + expect(notification.kind).to.equal('E'); + expect(notification).to.have.property('error', 'bad'); + done(); + }; + + const source = new Observable((subscriber) => { subscriber.error('handled'); throw 'bad'; }); let syncSentError: any; source.subscribe({ - error: err => { + error: (err) => { syncSentError = err; - } + }, }); expect(syncSentError).to.equal('handled'); expect(called).to.be.false; }); - it('should call asynchronously if a subscription setup errors after the subscription is closed by a completion', (done) => { + it('should be called asynchronously if a subscription setup errors after the subscription is closed by a completion', (done) => { let called = false; let completed = false; - - config.onUnhandledError = err => { + config.onStoppedNotification = (notification) => { called = true; - expect(err).to.equal('bad'); - done() + expect(notification.kind).to.equal('E'); + expect(notification).to.have.property('error', 'bad'); + done(); }; - const source = new Observable(subscriber => { + const source = new Observable((subscriber) => { subscriber.complete(); throw 'bad'; }); @@ -122,47 +169,80 @@ describe('config', () => { }, complete: () => { completed = true; - } + }, }); expect(completed).to.be.true; expect(called).to.be.false; }); - /** - * Thie test is added so people know this behavior is _intentional_. It's part of the contract of observables - * and, while I'm not sure I like it, it might start surfacing untold numbers of errors, and break - * node applications if we suddenly changed this to start throwing errors on other jobs for instances - * where users accidentally called `subscriber.error` twice. Likewise, would we report an error - * for two calls of `complete`? This is really something a build-time tool like a linter should - * capture. Not a run time error reporting event. - */ - it('should not be called if two errors are sent to the subscriber', (done) => { + it('should be called if a next is sent to the stopped subscriber', (done) => { let called = false; - config.onUnhandledError = () => { + config.onStoppedNotification = (notification) => { called = true; + expect(notification.kind).to.equal('N'); + expect(notification).to.have.property('value', 2); + done(); }; - const source = new Observable(subscriber => { + const source = new Observable((subscriber) => { + subscriber.next(1); + subscriber.complete(); + subscriber.next(2); + }); + + let syncSentValue: any; + source.subscribe({ + next: (value) => { + syncSentValue = value; + }, + }); + + expect(syncSentValue).to.equal(1); + expect(called).to.be.false; + }); + + it('should be called if two errors are sent to the subscriber', (done) => { + let called = false; + config.onStoppedNotification = (notification) => { + called = true; + expect(notification.kind).to.equal('E'); + expect(notification).to.have.property('error', 'swallowed'); + done(); + }; + + const source = new Observable((subscriber) => { subscriber.error('handled'); subscriber.error('swallowed'); }); let syncSentError: any; source.subscribe({ - error: err => { + error: (err) => { syncSentError = err; - } + }, }); expect(syncSentError).to.equal('handled'); - // This timeout would be scheduled _after_ any error timeout that might be scheduled - // (But we're not scheduling that), so this is just an artificial delay to make sure the - // behavior sticks. - setTimeout(() => { - expect(called).to.be.false; + expect(called).to.be.false; + }); + + it('should be called if two completes are sent to the subscriber', (done) => { + let called = false; + config.onStoppedNotification = (notification) => { + called = true; + expect(notification.kind).to.equal('C'); done(); + }; + + const source = new Observable((subscriber) => { + subscriber.complete(); + subscriber.complete(); }); + + source.subscribe(); + + expect(called).to.be.false; }); }); -}); \ No newline at end of file +}); diff --git a/spec/util/canReportError-spec.ts b/spec/util/canReportError-spec.ts deleted file mode 100644 index dec8869e61..0000000000 --- a/spec/util/canReportError-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect } from 'chai'; -import { noop, Subscriber } from 'rxjs'; -import { canReportError } from 'rxjs/internal/Observable'; -import { SafeSubscriber } from 'rxjs/internal/Subscriber'; - -describe('canReportError', () => { - it('should report errors to an observer if possible', () => { - const subscriber = new SafeSubscriber(noop, noop); - expect(canReportError(subscriber)).to.be.true; - }); - - it('should not report errors to a stopped observer', () => { - const subscriber = new SafeSubscriber(noop, noop); - subscriber.error(new Error('kaboom')); - expect(canReportError(subscriber)).to.be.false; - }); - - it('should not report errors an observer with a stopped destination', () => { - const destination = new SafeSubscriber(noop, noop); - const subscriber = new Subscriber(destination); - destination.error(new Error('kaboom')); - expect(canReportError(subscriber)).to.be.false; - }); -}); diff --git a/src/internal/Observable.ts b/src/internal/Observable.ts index bdb3d5311a..a099e4b149 100644 --- a/src/internal/Observable.ts +++ b/src/internal/Observable.ts @@ -235,11 +235,8 @@ export class Observable implements Subscribable { } catch (err) { if (config.useDeprecatedSynchronousErrorHandling) { throw err; - } else { - // If an error is thrown during subscribe, but our subscriber is closed, so we cannot notify via the - // subscription "error" channel, it is an unhandled error and we need to report it appropriately. - canReportError(sink) ? sink.error(err) : reportUnhandledError(err); } + sink.error(err); } } @@ -484,23 +481,6 @@ function getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) { return promiseCtor ?? config.Promise ?? Promise; } -/** - * Determines whether the subscriber is closed or stopped or has a - * destination that is closed or stopped - in which case errors will - * need to be reported via a different mechanism. - * @param subscriber the subscriber to check - */ -export function canReportError(subscriber: Subscriber): boolean { - while (subscriber) { - const { closed, destination, isStopped } = subscriber as any; - if (closed || isStopped) { - return false; - } - subscriber = destination && destination instanceof Subscriber ? destination : null!; - } - return true; -} - function isObserver(value: any): value is Observer { return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete); } From dfb1e67ce49ed038f93c10fb8e291b9055330a86 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 26 Sep 2020 12:09:01 +1000 Subject: [PATCH 13/21] test: use onStoppedNotification --- spec/Observable-spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/Observable-spec.ts b/spec/Observable-spec.ts index 32c8fd31a4..9fc41f7ed7 100644 --- a/spec/Observable-spec.ts +++ b/spec/Observable-spec.ts @@ -965,12 +965,13 @@ describe('Observable.lift', () => { }); it('should not swallow internal errors', (done) => { - config.onUnhandledError = (err) => { - expect(err).to.equal('bad'); - config.onUnhandledError = null; + config.onStoppedNotification = (notification) => { + expect(notification.kind).to.equal('E'); + expect(notification).to.have.property('error', 'bad'); + config.onStoppedNotification = null; done(); }; - + new Observable(subscriber => { subscriber.error('test'); throw 'bad'; From d03ddb95f0847362188369e7fcefaffa0c3eb910 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 26 Sep 2020 12:12:54 +1000 Subject: [PATCH 14/21] chore: remove unused import --- src/internal/Observable.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/internal/Observable.ts b/src/internal/Observable.ts index a099e4b149..ea64715de8 100644 --- a/src/internal/Observable.ts +++ b/src/internal/Observable.ts @@ -8,7 +8,6 @@ import { TeardownLogic, OperatorFunction, PartialObserver, Subscribable, Observe import { observable as Symbol_observable } from './symbol/observable'; import { pipeFromArray } from './util/pipe'; import { config } from './config'; -import { reportUnhandledError } from './util/reportUnhandledError'; import { isFunction } from './util/isFunction'; /** From 015dfb06e60dbb3b81c2e2c0430221e968d4c6b3 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Tue, 29 Sep 2020 16:50:49 +1000 Subject: [PATCH 15/21] refactor: import notification factories directly --- spec/operators/groupBy-spec.ts | 2 +- spec/schedulers/TestScheduler-spec.ts | 2 +- src/internal/Notification.ts | 2 -- src/internal/testing/TestScheduler.ts | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/operators/groupBy-spec.ts b/spec/operators/groupBy-spec.ts index df671f32ad..6b31555c9a 100644 --- a/spec/operators/groupBy-spec.ts +++ b/spec/operators/groupBy-spec.ts @@ -3,7 +3,7 @@ import { groupBy, delay, tap, map, take, mergeMap, materialize, skip, ignoreElem import { TestScheduler } from 'rxjs/testing'; import { ReplaySubject, of, Observable, Operator, Observer, interval, Subject } from 'rxjs'; import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; -import { createNotification } from 'rxjs/internal/Notification'; +import { createNotification } from 'rxjs/internal/NotificationFactories'; declare const rxTestScheduler: TestScheduler; diff --git a/spec/schedulers/TestScheduler-spec.ts b/spec/schedulers/TestScheduler-spec.ts index 67c3c3e425..10b2e8ccd6 100644 --- a/spec/schedulers/TestScheduler-spec.ts +++ b/spec/schedulers/TestScheduler-spec.ts @@ -3,7 +3,7 @@ import { hot, cold, expectObservable, expectSubscriptions, time } from '../helpe import { TestScheduler } from 'rxjs/testing'; import { Observable, NEVER, EMPTY, Subject, of, merge, animationFrameScheduler, asapScheduler, asyncScheduler, interval } from 'rxjs'; import { delay, debounceTime, concatMap, mergeMap, mapTo, take } from 'rxjs/operators'; -import { nextNotification, COMPLETE_NOTIFICATION, errorNotification } from 'rxjs/internal/Notification'; +import { nextNotification, COMPLETE_NOTIFICATION, errorNotification } from 'rxjs/internal/NotificationFactories'; import { animationFrameProvider } from 'rxjs/internal/scheduler/animationFrameProvider'; import { immediateProvider } from 'rxjs/internal/scheduler/immediateProvider'; import { intervalProvider } from 'rxjs/internal/scheduler/intervalProvider'; diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index 320fb30040..8302a1359d 100644 --- a/src/internal/Notification.ts +++ b/src/internal/Notification.ts @@ -238,5 +238,3 @@ export function observeNotification(notification: ObservableNotification, } kind === 'N' ? observer.next?.(value!) : kind === 'E' ? observer.error?.(error) : observer.complete?.(); } - -export * from './NotificationFactories'; diff --git a/src/internal/testing/TestScheduler.ts b/src/internal/testing/TestScheduler.ts index 41468184db..89d594053e 100644 --- a/src/internal/testing/TestScheduler.ts +++ b/src/internal/testing/TestScheduler.ts @@ -6,7 +6,7 @@ import { SubscriptionLog } from './SubscriptionLog'; import { Subscription } from '../Subscription'; import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler'; import { ObservableNotification } from '../types'; -import { COMPLETE_NOTIFICATION, errorNotification, nextNotification } from '../Notification'; +import { COMPLETE_NOTIFICATION, errorNotification, nextNotification } from '../NotificationFactories'; import { dateTimestampProvider } from '../scheduler/dateTimestampProvider'; import { performanceTimestampProvider } from '../scheduler/performanceTimestampProvider'; import { animationFrameProvider } from '../scheduler/animationFrameProvider'; From 7b6e0d99355890dd2caac4bc280a93fbd2ac5d6e Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Tue, 29 Sep 2020 17:02:44 +1000 Subject: [PATCH 16/21] refactor: use delegatable provider --- src/internal/Subscriber.ts | 3 ++- src/internal/util/reportUnhandledError.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index 665fc4a2a4..c2aa534bce 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -6,6 +6,7 @@ import { config } from './config'; import { reportUnhandledError } from './util/reportUnhandledError'; import { noop } from './util/noop'; import { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories'; +import { immediateProvider } from './scheduler/immediateProvider'; /** * Implements the {@link Observer} interface and extends the @@ -198,7 +199,7 @@ function handleStoppedNotification( subscriber: Subscriber ) { const { onStoppedNotification } = config; - onStoppedNotification && setTimeout(() => onStoppedNotification(notification, subscriber)); + onStoppedNotification && immediateProvider.setImmediate(() => onStoppedNotification(notification, subscriber)); } /** diff --git a/src/internal/util/reportUnhandledError.ts b/src/internal/util/reportUnhandledError.ts index ab670538a0..28605cb1dd 100644 --- a/src/internal/util/reportUnhandledError.ts +++ b/src/internal/util/reportUnhandledError.ts @@ -1,5 +1,6 @@ /** @prettier */ import { config } from '../config'; +import { immediateProvider } from '../scheduler/immediateProvider'; /** * Handles an error on another job either with the user-configured {@link onUnhandledError}, @@ -11,7 +12,7 @@ import { config } from '../config'; * @param err the error to report */ export function reportUnhandledError(err: any) { - setTimeout(() => { + immediateProvider.setImmediate(() => { const { onUnhandledError } = config; if (onUnhandledError) { // Execute the user-configured error handler. From 96ec5c0f65605c0324d268cce1d86d4a90d8ccfe Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Tue, 29 Sep 2020 22:40:00 +1000 Subject: [PATCH 17/21] test: replace setTimeout with the provider --- spec/config-spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/config-spec.ts b/spec/config-spec.ts index 2fcc4f190f..31ab46268a 100644 --- a/spec/config-spec.ts +++ b/spec/config-spec.ts @@ -3,6 +3,7 @@ import { config } from '../src/internal/config'; import { expect } from 'chai'; import { Observable } from 'rxjs'; +import { immediateProvider } from 'rxjs/internal/scheduler/immediateProvider'; describe('config', () => { it('should have a Promise property that defaults to nothing', () => { @@ -104,10 +105,10 @@ describe('config', () => { }); expect(syncSentError).to.equal('handled'); - // This timeout would be scheduled _after_ any error timeout that might be scheduled - // (But we're not scheduling that), so this is just an artificial delay to make sure the - // behavior sticks. - setTimeout(() => { + // When called, onUnhandledError is called on a micro task, so delay the + // the assertion of the expectation until after the point at which + // onUnhandledError would have been called. + immediateProvider.setImmediate(() => { expect(called).to.be.false; done(); }); From 2d3d9d142efae58b2f5ef1fc253bbb7acd78c318 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 3 Oct 2020 07:02:34 +1000 Subject: [PATCH 18/21] chore: add and use timeout provider --- spec/config-spec.ts | 6 ++-- spec/schedulers/TestScheduler-spec.ts | 26 ++++++++++++-- src/internal/Subscriber.ts | 4 +-- src/internal/scheduler/timeoutProvider.ts | 28 +++++++++++++++ src/internal/testing/TestScheduler.ts | 44 +++++++++++++++++++---- src/internal/util/reportUnhandledError.ts | 4 +-- 6 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/internal/scheduler/timeoutProvider.ts diff --git a/spec/config-spec.ts b/spec/config-spec.ts index 31ab46268a..0e0cf88e0c 100644 --- a/spec/config-spec.ts +++ b/spec/config-spec.ts @@ -3,7 +3,7 @@ import { config } from '../src/internal/config'; import { expect } from 'chai'; import { Observable } from 'rxjs'; -import { immediateProvider } from 'rxjs/internal/scheduler/immediateProvider'; +import { timeoutProvider } from 'rxjs/internal/scheduler/timeoutProvider'; describe('config', () => { it('should have a Promise property that defaults to nothing', () => { @@ -105,10 +105,10 @@ describe('config', () => { }); expect(syncSentError).to.equal('handled'); - // When called, onUnhandledError is called on a micro task, so delay the + // When called, onUnhandledError is called on a timeout, so delay the // the assertion of the expectation until after the point at which // onUnhandledError would have been called. - immediateProvider.setImmediate(() => { + timeoutProvider.setTimeout(() => { expect(called).to.be.false; done(); }); diff --git a/spec/schedulers/TestScheduler-spec.ts b/spec/schedulers/TestScheduler-spec.ts index 10b2e8ccd6..4873dfb12e 100644 --- a/spec/schedulers/TestScheduler-spec.ts +++ b/spec/schedulers/TestScheduler-spec.ts @@ -7,6 +7,7 @@ import { nextNotification, COMPLETE_NOTIFICATION, errorNotification } from 'rxjs import { animationFrameProvider } from 'rxjs/internal/scheduler/animationFrameProvider'; import { immediateProvider } from 'rxjs/internal/scheduler/immediateProvider'; import { intervalProvider } from 'rxjs/internal/scheduler/intervalProvider'; +import { timeoutProvider } from 'rxjs/internal/scheduler/timeoutProvider'; declare const rxTestScheduler: TestScheduler; @@ -680,22 +681,41 @@ describe('TestScheduler', () => { }); }); - it('should schedule immediates before intervals', () => { + it('should schedule timeouts', () => { + const testScheduler = new TestScheduler(assertDeepEquals); + testScheduler.run(() => { + const values: string[] = []; + const { setTimeout } = timeoutProvider; + setTimeout(() => { + values.push(`a@${testScheduler.now()}`); + }, 1); + expect(values).to.deep.equal([]); + testScheduler.schedule(() => { + expect(values).to.deep.equal(['a@1']); + }, 10); + }); + }); + + it('should schedule immediates before intervals and timeouts', () => { const testScheduler = new TestScheduler(assertDeepEquals); testScheduler.run(() => { const values: string[] = []; const { setImmediate } = immediateProvider; const { setInterval, clearInterval } = intervalProvider; + const { setTimeout } = timeoutProvider; const handle = setInterval(() => { values.push(`a@${testScheduler.now()}`); clearInterval(handle); }, 0); - setImmediate(() => { + setTimeout(() => { values.push(`b@${testScheduler.now()}`); + }, 0); + setImmediate(() => { + values.push(`c@${testScheduler.now()}`); }); expect(values).to.deep.equal([]); testScheduler.schedule(() => { - expect(values).to.deep.equal(['b@0', 'a@0']); + expect(values).to.deep.equal(['c@0', 'a@0', 'b@0']); }, 10); }); }); diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index c2aa534bce..1fcbb78efc 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -6,7 +6,7 @@ import { config } from './config'; import { reportUnhandledError } from './util/reportUnhandledError'; import { noop } from './util/noop'; import { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories'; -import { immediateProvider } from './scheduler/immediateProvider'; +import { timeoutProvider } from './scheduler/timeoutProvider'; /** * Implements the {@link Observer} interface and extends the @@ -199,7 +199,7 @@ function handleStoppedNotification( subscriber: Subscriber ) { const { onStoppedNotification } = config; - onStoppedNotification && immediateProvider.setImmediate(() => onStoppedNotification(notification, subscriber)); + onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber)); } /** diff --git a/src/internal/scheduler/timeoutProvider.ts b/src/internal/scheduler/timeoutProvider.ts new file mode 100644 index 0000000000..aa76be0b28 --- /dev/null +++ b/src/internal/scheduler/timeoutProvider.ts @@ -0,0 +1,28 @@ +/** @prettier */ +type SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => number; +type ClearTimeoutFunction = (handle: number) => void; + +interface TimeoutProvider { + setTimeout: SetTimeoutFunction; + clearTimeout: ClearTimeoutFunction; + delegate: + | { + setTimeout: SetTimeoutFunction; + clearTimeout: ClearTimeoutFunction; + } + | undefined; +} + +export const timeoutProvider: TimeoutProvider = { + // When accessing the delegate, use the variable rather than `this` so that + // the functions can be called without being bound to the provider. + setTimeout(...args) { + const { delegate } = timeoutProvider; + return (delegate?.setTimeout || setTimeout)(...args); + }, + clearTimeout(handle) { + const { delegate } = timeoutProvider; + return (delegate?.clearTimeout || clearTimeout)(handle); + }, + delegate: undefined, +}; diff --git a/src/internal/testing/TestScheduler.ts b/src/internal/testing/TestScheduler.ts index 89d594053e..97c66a5199 100644 --- a/src/internal/testing/TestScheduler.ts +++ b/src/internal/testing/TestScheduler.ts @@ -12,6 +12,7 @@ import { performanceTimestampProvider } from '../scheduler/performanceTimestampP import { animationFrameProvider } from '../scheduler/animationFrameProvider'; import { immediateProvider } from '../scheduler/immediateProvider'; import { intervalProvider } from '../scheduler/intervalProvider'; +import { timeoutProvider } from '../scheduler/timeoutProvider'; const defaultMaxFrame: number = 750; @@ -488,19 +489,19 @@ export class TestScheduler extends VirtualTimeScheduler { handle: number; handler: () => void; subscription: Subscription; - type: 'immediate' | 'interval'; + type: 'immediate' | 'interval' | 'timeout'; }>(); const run = () => { // Whenever a scheduled run is executed, it must run a single immediate // or interval action - with immediate actions being prioritized over - // interval actions. + // interval and timeout actions. const now = this.now(); const scheduledRecords = Array.from(scheduleLookup.values()); const scheduledRecordsDue = scheduledRecords.filter(({ due }) => due <= now); - const immediates = scheduledRecordsDue.filter(({ type }) => type === 'immediate'); - if (immediates.length > 0) { - const { handle, handler } = immediates[0]; + const dueImmediates = scheduledRecordsDue.filter(({ type }) => type === 'immediate'); + if (dueImmediates.length > 0) { + const { handle, handler } = dueImmediates[0]; scheduleLookup.delete(handle); handler(); return; @@ -517,6 +518,13 @@ export class TestScheduler extends VirtualTimeScheduler { handler(); return; } + const dueTimeouts = scheduledRecordsDue.filter(({ type }) => type === 'timeout'); + if (dueTimeouts.length > 0) { + const { handle, handler } = dueTimeouts[0]; + scheduleLookup.delete(handle); + handler(); + return; + } throw new Error('Expected a due immediate or interval'); }; @@ -564,7 +572,29 @@ export class TestScheduler extends VirtualTimeScheduler { } }; - return { immediate, interval }; + const timeout = { + setTimeout: (handler: () => void, duration = 0) => { + const handle = ++lastHandle; + scheduleLookup.set(handle, { + due: this.now() + duration, + duration, + handle, + handler, + subscription: this.schedule(run, duration), + type: 'timeout', + }); + return handle; + }, + clearTimeout: (handle: number) => { + const value = scheduleLookup.get(handle); + if (value) { + value.subscription.unsubscribe(); + scheduleLookup.delete(handle); + } + } + }; + + return { immediate, interval, timeout }; } /** @@ -590,6 +620,7 @@ export class TestScheduler extends VirtualTimeScheduler { dateTimestampProvider.delegate = this; immediateProvider.delegate = delegates.immediate; intervalProvider.delegate = delegates.interval; + timeoutProvider.delegate = delegates.timeout; performanceTimestampProvider.delegate = this; const helpers: RunHelpers = { @@ -613,6 +644,7 @@ export class TestScheduler extends VirtualTimeScheduler { dateTimestampProvider.delegate = undefined; immediateProvider.delegate = undefined; intervalProvider.delegate = undefined; + timeoutProvider.delegate = undefined; performanceTimestampProvider.delegate = undefined; } } diff --git a/src/internal/util/reportUnhandledError.ts b/src/internal/util/reportUnhandledError.ts index 28605cb1dd..cf7e4377c6 100644 --- a/src/internal/util/reportUnhandledError.ts +++ b/src/internal/util/reportUnhandledError.ts @@ -1,6 +1,6 @@ /** @prettier */ import { config } from '../config'; -import { immediateProvider } from '../scheduler/immediateProvider'; +import { timeoutProvider } from '../scheduler/timeoutProvider'; /** * Handles an error on another job either with the user-configured {@link onUnhandledError}, @@ -12,7 +12,7 @@ import { immediateProvider } from '../scheduler/immediateProvider'; * @param err the error to report */ export function reportUnhandledError(err: any) { - immediateProvider.setImmediate(() => { + timeoutProvider.setTimeout(() => { const { onUnhandledError } = config; if (onUnhandledError) { // Execute the user-configured error handler. From 12d0d680d014c0105f8850e0f31ab37db115ba19 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 3 Oct 2020 07:09:14 +1000 Subject: [PATCH 19/21] chore: use ObservableNotification type --- src/internal/Subscriber.ts | 7 ++----- src/internal/config.ts | 6 ++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index 1fcbb78efc..637b1e58ce 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -1,6 +1,6 @@ /** @prettier */ import { isFunction } from './util/isFunction'; -import { Observer, PartialObserver } from './types'; +import { Observer, PartialObserver, ObservableNotification } from './types'; import { isSubscription, Subscription } from './Subscription'; import { config } from './config'; import { reportUnhandledError } from './util/reportUnhandledError'; @@ -194,10 +194,7 @@ function defaultErrorHandler(err: any) { * @param notification The notification being sent * @param subscriber The stopped subscriber */ -function handleStoppedNotification( - notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, - subscriber: Subscriber -) { +function handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) { const { onStoppedNotification } = config; onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber)); } diff --git a/src/internal/config.ts b/src/internal/config.ts index 67c565b768..9f4bacd2dc 100644 --- a/src/internal/config.ts +++ b/src/internal/config.ts @@ -1,6 +1,6 @@ /** @prettier */ - import { Subscriber } from './Subscriber'; +import { ObservableNotification } from './types'; /** * The global configuration object for RxJS, used to configure things @@ -28,9 +28,7 @@ export const config = { * we do not want errors thrown in this user-configured handler to interfere with the * behavior of the library. */ - onStoppedNotification: null as - | ((notification: { kind: 'N'; value: any } | { kind: 'E'; error: any } | { kind: 'C' }, subscriber: Subscriber) => void) - | null, + onStoppedNotification: null as ((notification: ObservableNotification, subscriber: Subscriber) => void) | null, /** * The promise constructor used by default for methods such as From 5516a82b86685e619276479b4573d01b3bb79452 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 3 Oct 2020 07:15:44 +1000 Subject: [PATCH 20/21] chore: update api_guardian files --- api_guard/dist/types/index.d.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index 72ac4bb0fe..7e9db4977c 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -112,15 +112,7 @@ export declare function concat(...inputsAndSchedul export declare const config: { onUnhandledError: ((err: any) => void) | null; - onStoppedNotification: ((notification: { - kind: 'N'; - value: any; - } | { - kind: 'E'; - error: any; - } | { - kind: 'C'; - }, subscriber: Subscriber) => void) | null; + onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null; Promise: PromiseConstructorLike | undefined; useDeprecatedSynchronousErrorHandling: boolean; useDeprecatedNextContext: boolean; From fe27cd4639331b0e2bcad2485ca8a0a530bd1072 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 31 Oct 2020 10:55:10 +1000 Subject: [PATCH 21/21] chore: comment the delegation of setTimeout, et al. --- src/internal/testing/TestScheduler.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/internal/testing/TestScheduler.ts b/src/internal/testing/TestScheduler.ts index 97c66a5199..254d36f051 100644 --- a/src/internal/testing/TestScheduler.ts +++ b/src/internal/testing/TestScheduler.ts @@ -528,6 +528,18 @@ export class TestScheduler extends VirtualTimeScheduler { throw new Error('Expected a due immediate or interval'); }; + // The following objects are the delegates that replace conventional + // runtime implementations with TestScheduler implementations. + // + // The immediate delegate is depended upon by the asapScheduler. + // + // The interval delegate is depended upon by the asyncScheduler. + // + // The timeout delegate is not depended upon by any scheduler, but it's + // included here because the onUnhandledError and onStoppedNotification + // configuration points use setTimeout to avoid producer interference. It's + // inclusion allows for the testing of these configuration points. + const immediate = { setImmediate: (handler: () => void) => { const handle = ++lastHandle;