diff --git a/spec.bs b/spec.bs index a82b81c..3260061 100644 --- a/spec.bs +++ b/spec.bs @@ -1105,7 +1105,87 @@ For now, see [https://github.com/wicg/observable#operators](https://github.com/w
The finally(|callback|) method steps are: - 1. TODO: Spec this and use |callback|. + 1. Let |sourceObservable| be [=this=]. + + 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an + algorithm that takes a {{Subscriber}} |subscriber| and does the following: + + 1. Let |finally callback steps| be the following steps: + + 1. [=Invoke=] |callback|. + + If an exception |E| was thrown, then run + |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps. + + 1. [=AbortSignal/add|Add the algorithm=] |finally callback steps| to |subscriber|'s + [=Subscriber/signal=]. + + Note: This is necessary to ensure |callback| gets invoked on *consumer-initiated* + unsubscription. In that case, |subscriber|'s [=Subscriber/signal=] gets + [=AbortSignal/signal abort|aborted=], and neither the |sourceObserver|'s + [=internal observer/error steps=] nor [=internal observer/complete steps=] are invoked. + + 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows: + + : [=internal observer/next steps=] + :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in value. + + : [=internal observer/error steps=] + :: 1. Run the |finally callback steps|. + +
+

This "manual" invocation of |finally callback steps| is necessary to ensure + that |callback| is invoked on producer-initiated unsubscription. Without this, + we'd simply delegate to {{Subscriber/error()}} below, which first [=close a + subscription|closes=] the subscription, *and then* [=AbortSignal/signal + abort|aborts=] |subscriber|'s [=Subscriber/signal=].

+ +

That means when |finally callback steps| eventually runs as a result of + abortion, |subscriber| would already be [=Subscriber/active|inactive=]. So if + |callback| throws an error during, it would never be plumbed through to + {{Subscriber/error()}} (that method is a no-op once + [=Subscriber/active|inactive=]). See the following example which exercises this + case exactly:

+ +
+const controller = new AbortController();
+const observable = new Observable(subscriber => {
+  subscriber.complete();
+});
+
+observable
+  .finally(() => {
+    throw new Error('finally error');
+  })
+  .subscribe({
+    error: e => console.log('erorr passed through'),
+  }, {signal: controller.signal});
+
+controller.abort(); // Logs 'error passed through'.
+                  
+
+ + 1. Run |subscriber|'s {{Subscriber/error()}} method, given the passed in error. + + Note: The |finally callback steps| possibly calls |subscriber|'s + {{Subscriber/error()}} method first, if |callback| throws an error. In that case, it + is still safe to call it again unconditionally, because the subscription will + already be closed, making the call a no-op. + + : [=internal observer/complete steps=] + :: 1. Run the |finally callback steps|. + + 1. Run |subscriber|'s {{Subscriber/complete()}} method. + + 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is + |subscriber|'s [=Subscriber/signal=]. + + 1. Subscribe to |sourceObservable| + given |sourceObserver| and |options|. + + 1. Return |observable|.