From 3d032d7bf62bc315d06f15b953c015ad0ad54cb3 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 28 Jun 2024 14:43:28 -0400 Subject: [PATCH 1/4] Spec the `finally()` operator --- spec.bs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/spec.bs b/spec.bs index d60fbfa..d2016aa 100644 --- a/spec.bs +++ b/spec.bs @@ -1545,7 +1545,87 @@ For now, see
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|.
From 085161383ea5008d284bf5594e2e6f9a64b80dbc Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 21 Feb 2025 13:02:13 -0500 Subject: [PATCH 2/4] Simplify --- spec.bs | 62 +++------------------------------------------------------ 1 file changed, 3 insertions(+), 59 deletions(-) diff --git a/spec.bs b/spec.bs index d2016aa..565362c 100644 --- a/spec.bs +++ b/spec.bs @@ -1550,20 +1550,7 @@ For now, see 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. Run |subscriber|'s {{Subscriber/addTeardown()}} method with |callback|. 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows: @@ -1572,55 +1559,12 @@ For now, see ignore>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. + :: 1. Run |subscriber|'s {{Subscriber/error()}} method, given the passed in error. : [=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. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=]. 1. Subscribe to |sourceObservable| given |sourceObserver| and |options|. From 21a13aef468589241259c778535cd73b12284fc9 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 21 Feb 2025 13:02:35 -0500 Subject: [PATCH 3/4] Wrap --- spec.bs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec.bs b/spec.bs index 565362c..d1cfe12 100644 --- a/spec.bs +++ b/spec.bs @@ -1559,12 +1559,14 @@ For now, see ignore>value. : [=internal observer/error steps=] - :: 1. Run |subscriber|'s {{Subscriber/error()}} method, given the passed in error. + :: 1. Run |subscriber|'s {{Subscriber/error()}} method, given the passed in error. : [=internal observer/complete steps=] 1. Run |subscriber|'s {{Subscriber/complete()}} method. - 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=]. + 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is + |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=]. 1. Subscribe to |sourceObservable| given |sourceObserver| and |options|. From da2f4601f91aee7f56b2b065ef949d050099ad24 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 21 Feb 2025 13:11:17 -0500 Subject: [PATCH 4/4] Markdown --- spec.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec.bs b/spec.bs index d1cfe12..a6e3845 100644 --- a/spec.bs +++ b/spec.bs @@ -1563,7 +1563,7 @@ For now, see ignore>error. : [=internal observer/complete steps=] - 1. Run |subscriber|'s {{Subscriber/complete()}} method. + :: 1. Run |subscriber|'s {{Subscriber/complete()}} method. 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].