From 55fb9e31e875f39aab9067c35bd21415f1af01a9 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Tue, 20 Feb 2024 13:49:42 -0800 Subject: [PATCH] DOM: Reorder Observable completion events This CL "fixes" Observable unsubscription/teardown timing. As a matter of accidental historical precedent, Observables in JavaScript (but not in other languages) had implemented the "rule" that upon Subscriber#error() or Subscriber#complete(), the subscriber would: 1. First, invoke the appropriate Observer callback, if provided (i.e., complete() or error() callback). 2. Signal abort Subscriber#signal, which invokes any teardowns and also fires the `abort` event at the signal. However, after dom@chromium.org discussed this more with ben@benlesh.com, we came to the conclusion that the principle of "as soon as you know you will teardown, you MUST close the subscription and any upstream subscriptions" should be adhered. This means the above steps must be inverted. This is a small-in-size but medium-in-impact design change for the Observable concept, and led to a blog post [1] and an announcement [2] that the RxJS library intends to change its historical ordering of these events. This CL: 1. Inverts the order of the aforementioned steps in the Blink implementation. 2. Improves some tests that assert this new ordering. 3. Simplifies the takeUntil() operator in general. The Observable spec will be updated alongside this commit. [1]: https://benlesh.com/posts/observables-are-broken-and-so-is-javascript/ [2]: https://github.com/ReactiveX/rxjs/issues/7443 R=masonf@chromium.org Bug: 1485981 Change-Id: I376e66eef490808d264dc999862a801d591aa278 --- .../tentative/observable-constructor.any.js | 23 +++-- .../tentative/observable-takeUntil.any.js | 98 ++++++++++--------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/dom/observable/tentative/observable-constructor.any.js b/dom/observable/tentative/observable-constructor.any.js index f108e902b32d07..6b9d65b2c0e9fe 100644 --- a/dom/observable/tentative/observable-constructor.any.js +++ b/dom/observable/tentative/observable-constructor.any.js @@ -235,14 +235,18 @@ test(t => { source.subscribe({ complete: () => { - activeDuringComplete = innerSubscriber.active - abortedDuringComplete = innerSubscriber.active + activeDuringComplete = innerSubscriber.active; + abortedDuringComplete = innerSubscriber.signal.aborted; } }); assert_true(activeBeforeComplete, "Subscription is active before complete"); assert_false(abortedBeforeComplete, "Subscription is not aborted before complete"); - assert_false(activeDuringComplete, "Subscription is not active during complete"); - assert_false(abortedDuringComplete, "Subscription is not aborted during complete"); + assert_false(activeDuringComplete, + "Subscription becomes inactive during Subscriber#complete(), just " + + "before Observer#complete() callback is invoked"); + assert_true(abortedDuringComplete, + "Subscription's signal is aborted during Subscriber#complete(), just " + + "before Observer#complete() callback is invoked"); assert_false(activeAfterComplete, "Subscription is not active after complete"); assert_true(abortedAfterComplete, "Subscription is aborted after complete"); }, "Subscription is inactive after complete()"); @@ -269,13 +273,18 @@ test(t => { source.subscribe({ error: () => { - activeDuringError = innerSubscriber.active + activeDuringError = innerSubscriber.active; + abortedDuringError = innerSubscriber.signal.aborted; } }); assert_true(activeBeforeError, "Subscription is active before error"); assert_false(abortedBeforeError, "Subscription is not aborted before error"); - assert_false(activeDuringError, "Subscription is not active during error"); - assert_false(abortedDuringError, "Subscription is not aborted during error"); + assert_false(activeDuringError, + "Subscription becomes inactive during Subscriber#error(), just " + + "before Observer#error() callback is invoked"); + assert_true(abortedDuringError, + "Subscription's signal is aborted during Subscriber#error(), just " + + "before Observer#error() callback is invoked"); assert_false(activeAfterError, "Subscription is not active after error"); assert_true(abortedAfterError, "Subscription is not aborted after error"); }, "Subscription is inactive after error()"); diff --git a/dom/observable/tentative/observable-takeUntil.any.js b/dom/observable/tentative/observable-takeUntil.any.js index 6421777e09bc35..166b662195779e 100644 --- a/dom/observable/tentative/observable-takeUntil.any.js +++ b/dom/observable/tentative/observable-takeUntil.any.js @@ -32,74 +32,74 @@ promise_test(async () => { // `takeUntil()` operator, the spec responds to `notifier`'s `next()` by // unsubscribing from `notifier`, which is what this test asserts. promise_test(async () => { - const source = new Observable(subscriber => {}); + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); - let notifierSubscriberActiveBeforeNext; - let notifierSubscriberActiveAfterNext; - let teardownCalledAfterNext; - let notifierSignalAbortedAfterNext; const notifier = new Observable(subscriber => { - let teardownCalled; - subscriber.addTeardown(() => teardownCalled = true); + subscriber.addTeardown(() => results.push('notifier teardown')); + results.push('notifier subscribe callback'); // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. - notifierSubscriberActiveBeforeNext = subscriber.active; - subscriber.next('value'); - notifierSubscriberActiveAfterNext = subscriber.active; - teardownCalledAfterNext = (teardownCalled === true); + results.push(`notifer active before next(): ${subscriber.active}`); + subscriber.error('error'); + results.push(`notifer active after next(): ${subscriber.active}`); notifierSignalAbortedAfterNext = subscriber.signal.aborted; }); let nextOrErrorCalled = false; let completeCalled = false; source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, - complete: () => completeCalled = true, + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), }); - assert_true(notifierSubscriberActiveBeforeNext); - assert_false(notifierSubscriberActiveAfterNext); - assert_true(teardownCalledAfterNext); - assert_true(notifierSignalAbortedAfterNext); - assert_false(nextOrErrorCalled); - assert_true(completeCalled); -}, "takeUntil: notifier next() unsubscribes to notifier"); + + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before next(): true', + 'notifier teardown', + 'takeUntil() complete callback', + 'notifer active after next(): false', + ]); +}, "takeUntil: notifier next() unsubscribes from notifier"); // This test is identical to the one above, with the exception being that the // `notifier` calls `subscriber.error()` instead `subscriber.next()`. promise_test(async () => { - const source = new Observable(subscriber => {}); + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); - let notifierSubscriberActiveBeforeNext; - let notifierSubscriberActiveAfterNext; - let teardownCalledAfterNext; - let notifierSignalAbortedAfterNext; const notifier = new Observable(subscriber => { - let teardownCalled; - subscriber.addTeardown(() => teardownCalled = true); + subscriber.addTeardown(() => results.push('notifier teardown')); + results.push('notifier subscribe callback'); // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. - notifierSubscriberActiveBeforeNext = subscriber.active; - subscriber.error('error'); - notifierSubscriberActiveAfterNext = subscriber.active; - teardownCalledAfterNext = (teardownCalled === true); + results.push(`notifer active before next(): ${subscriber.active}`); + subscriber.next('value'); + results.push(`notifer active after next(): ${subscriber.active}`); notifierSignalAbortedAfterNext = subscriber.signal.aborted; }); - let nextOrErrorCalled = false; - let completeCalled = false; source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, - complete: () => completeCalled = true, + next: () => results.push('takeUntil() next callback'), + error: () => results.push('takeUntil() error callback'), + complete: () => results.push('takeUntil() complete callback'), }); - assert_true(notifierSubscriberActiveBeforeNext); - assert_false(notifierSubscriberActiveAfterNext); - assert_true(teardownCalledAfterNext); - assert_true(notifierSignalAbortedAfterNext); - assert_false(nextOrErrorCalled); - assert_true(completeCalled); -}, "takeUntil: notifier error() unsubscribes to notifier"); + + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before next(): true', + 'notifier teardown', + 'takeUntil() complete callback', + 'notifer active after next(): false', + ]); +}, "takeUntil: notifier error() unsubscribes from notifier"); // Test that `notifier` unsubscribes from source Observable. promise_test(async t => { @@ -130,9 +130,10 @@ promise_test(async t => { let notifierTeardownCalledBeforeCompleteCallback; await new Promise(resolve => { source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, + next: () => {nextOrErrorCalled = true; results.push('next callback');}, + error: () => {nextOrErrorCalled = true; results.push('error callback');}, complete: () => { + results.push('complete callback'); notifierTeardownCalledBeforeCompleteCallback = notifierTeardownCalled; resolve(); }, @@ -145,7 +146,7 @@ promise_test(async t => { // The notifier/source teardowns are not called by the time the outer // `Observer#complete()` callback is invoked, but they are all run *after* // (i.e., before `notifier`'s `subscriber.next()` returns internally). - assert_false(notifierTeardownCalledBeforeCompleteCallback); + assert_true(notifierTeardownCalledBeforeCompleteCallback); assert_true(notifierTeardownCalled); assert_array_equals(results, [ "notifier subscribed", @@ -153,7 +154,8 @@ promise_test(async t => { "notifier teardown", "notifier signal abort", "source teardown", - "source signal abort" + "source signal abort", + "complete callback", ]); }, "takeUntil: notifier next() unsubscribes from notifier & source observable");