1- In this section, we'll cover the first two fundamental operations in Effection:
2- ` suspend() ` and ` action() ` , and how we can use them in tandem to serve as a safe
3- alternative to the [ Promise constructor] [ promise-constructor ] .
1+ In this section, we'll see how we can use ` action ` as a safe alternative to
2+ the [ Promise constructor] [ promise-constructor ] .
43
5- ## Suspend
4+ ## Example: Sleep
65
7- Simple in concept, yet bearing enormous practical weight, the ` suspend() `
8- operation is fundamental to Effection. It pauses the current
9- operation until it [ passes out of scope] [ scope ] , at which point it will return
10- immediately.
11-
12- Let's revisit our simplified sleep operation from the [ introduction to
13- operations] ( ./operations ) :
6+ Let's revisit our sleep operation from the [ introduction to
7+ operations] ( ../operations ) :
148
159``` js
16- export function sleep (duration ) {
17- return action (function * (resolve ) {
10+ export function sleep (duration : number ): Operation<void> {
11+ return action ((resolve ) => {
1812 let timeoutId = setTimeout (resolve, duration);
19- try {
20- yield * suspend ();
21- } finally {
22- clearTimeout (timeoutId);
23- }
13+ return () => clearTimeout (timeoutId);
2414 });
2515}
2616```
2717
2818As we saw, no matter how the sleep operation ends, it always executes the
29- ` finally {} ` block on its way out; thereby clearing out the ` setTimeout `
30- callback.
31-
32- It's worth noting that we say the suspend operation will return immediately,
33- we really mean it. The operation will proceed to return from the suspension
34- point via _ as direct a path as possible_ , as though it were returning a value.
35-
36- ``` js {6}
37- export function sleep (duration ) {
38- return action (function * (resolve ) {
39- let timeoutId = setTimeout (resolve, duration);
40- try {
41- yield * suspend ();
42- console .log (' you will never ever see this printed!' );
43- } finally {
44- clearTimeout (timeoutId);
45- }
46- });
47- }
48- ```
19+ ` clearTimeout() ` on its way out.
4920
5021If we wanted to replicate our ` sleep() ` functionality with promises, we'd need
5122to do something like accept an [ ` AbortSignal ` ] [ abort-signal ] as a second
@@ -80,22 +51,14 @@ await Promise.all([sleep(10, signal), sleep(1000, signal)]);
8051controller .abort ();
8152```
8253
83- With a suspended action on the other hand, we get all the benefit as if
54+ With an action on the other hand, we get all the benefit as if
8455an abort signal was there without sacrificing any clarity in achieving it.
8556
86- > 💡Fun Fact: ` suspend() ` is the only true 'async' operation in Effection. If an
87- > operation does not include a call to ` suspend() ` , either by itself or via a
88- > sub-operation, then that operation is synchronous.
89-
90- Most often, [ but not always] [ spawn-suspend ] , you encounter ` suspend() ` in the
91- context of an action as the pivot between that action's setup and teardown.
57+ ## Action Constructor
9258
93- ## Actions
94-
95- The second fundamental operation, [ ` action() ` ] [ action ] , serves two
96- purposes. The first is to adapt callback-based APIs and make them available as
97- operations. In this regard, it is very much like the
98- [ promise constructor] [ promise-constructor ] . To see this correspondance, let's
59+ The [ ` action() ` ] [ action ] function provides a callback based API to create Effection operations.
60+ You don't need it all that often, but when you do it functions almost exactly like the
61+ [ promise constructor] [ promise-constructor ] . To see this, let's
9962use [ one of the examples from MDN] ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#examples )
10063that uses promises to make a crude replica of the global [ ` fetch() ` ] [ fetch ]
10164function. It manually creates an XHR, and hooks up the ` load ` and ` error ` events
@@ -114,17 +77,19 @@ async function fetch(url) {
11477```
11578
11679Consulting the [ Async Rosetta Stone] ( async-rosetta-stone ) , we can substitute the async
117- constructs for their Effection counterparts to arrive at a line for line
118- translation.
80+ constructs for their Effection counterparts to arrive at an (almost) line for line
81+ translation. The only significant difference is that unlike the promise constructor, an
82+ action constructor _ must_ return a "finally" function to exit the action.
11983
12084``` js
12185function * fetch (url ) {
122- return yield * action (function * (resolve , reject ) {
86+ return yield * action ((resolve , reject ) => {
12387 let xhr = new XMLHttpRequest ();
12488 xhr .open (" GET" , url);
12589 xhr .onload = () => resolve (xhr .responseText );
12690 xhr .onerror = () => reject (xhr .statusText );
12791 xhr .send ();
92+ return () => {}; // "finally" function place holder.
12893 });
12994}
13095```
@@ -133,7 +98,7 @@ While this works works every bit as well as the promise based
13398implementation, it turns out that the example from MDN has a subtle
13499bug. In fact, it's the same subtle bug that afflicted the "racing
135100sleep" example in the [ introduction to
136- operations] ( http://localhost:8000/docs /operations#cleanup) . If
101+ operations] ( .. /operations#cleanup) . If
137102we no longer care about the outcome of our ` fetch ` operation, we will
138103"leak" its http request which will remain in flight until a response
139104is received. In the example below it does not matter which web request
@@ -143,124 +108,32 @@ until _both_ requests are have received a response.
143108``` js
144109await Promise .race ([
145110 fetch (" https://openweathermap.org" ),
146- fetch (" https://open-meteo.org" )
147- ])
111+ fetch (" https://open-meteo.org" ),
112+ ]);
148113```
149114
150- With Effection, this is easily fixed by suspending the operation, and making
151- sure that the request is cancelled when it is either resolved, rejected, or
115+ With Effection, this is easily fixed by calling ` abort() ` in the finally function to
116+ make sure that the request is cancelled when it is either resolved, rejected, or
152117passes out of scope.
153118
154- ``` js {8-12 } showLineNumbers
119+ ``` js {8} showLineNumbers
155120function * fetch (url ) {
156- return yield * action (function * (resolve , reject ) {
121+ return yield * action ((resolve , reject ) => {
157122 let xhr = new XMLHttpRequest ();
158123 xhr .open (" GET" , url);
159124 xhr .onload = () => resolve (xhr .responseText );
160125 xhr .onerror = () => reject (xhr .statusText );
161126 xhr .send ();
162- try {
163- yield * suspend ();
164- } finally {
127+ return () => {
165128 xhr .abort ();
166- }
129+ }; // called in all cases
167130 });
168131}
169132```
170133
171- > 💡Almost every usage of the [ promise concurrency primitives] [ promise-concurrency ]
134+ > 💡Almost every usage of the [ promise concurrency primitives] [ promise-concurrency ]
172135> will contain bugs born of leaked effects.
173136
174- As we've seen, actions can do anything that a promise can do (and more safely
175- at that), but they also have a super power that promises do not. If you recall
176- from the very beginning of this article, a key difference in Effection is that
177- operations are values which, unlike promises, do not represent runtime state.
178- Rather, they are "recipes" of what to do, and in order to do them, they need to
179- be run either explicitly with ` run() ` or by including them with ` yield* ` .
180-
181- This means that when every operation runs, it is bound to an explicit
182- lexical context; which is a fancy way of saying that ___ running an
183- operation can only ever return control to a single location___ . A
184- promise on the other hand, because it accepts an unlimited number of
185- callbacks via ` then() ` , ` catch() ` , and ` finally() ` , can return control
186- to an unlimited number of locations. This may seem a small thing, but
187- it is very powerful. To demonstrate, consider the following set of
188- nested actions.
189-
190- ``` js
191- await run (function * () {
192- yield * action (function * (resolve ) {
193- try {
194- yield * action (function * () {
195- try {
196- yield * action (function * () { resolve () });
197- } finally {
198- console .log (' inner' )
199- }
200- });
201- } finally {
202- console .log (' middle' );
203- }
204- });
205- console .log (' outer' );
206- });
207- ```
208-
209- When we run it, it outputs the strings ` inner ` , ` middle ` , and ` outer ` in order.
210- Notice however, that we never actually resolved the inner actions, only the
211- outer one, and yet every single piece of teardown code is executed as expected
212- as the call stack unwinds and it proceeds back to line 2. This means you can use
213- actions to "capture" a specific location in your code as an "escape point" and
214- return to it an any moment, but still feel confident that you won't leak any
215- effects when you do.
216-
217- Let's consider a slightly more practical example of when this functionality
218- could come in handy. Let's say we have a bunch of numbers scattered across the
219- network that we want to fetch and multiply together. We want to write an
220- to muliply these numbers that will use a list of operations that retreive the
221- numbers for us.
222-
223- In order to be time efficient we want to fetch all the numbers
224- concurrently so we use the [ ` all() ` ] [ all ] operation. However, because
225- this is multiplication, if any one of the numbers is zero, then the
226- entire result is zero, so if at any point we discover that there is a
227- ` 0 ` in any of the inputs to the computation, there really is no
228- further point in continuing because the answer will be zero no matter
229- how we slice it. It would save us time and money if there were a
230- mechanism to "short-circuit" the operation and proceed directly to
231- zero, and in fact there is!
232-
233- The answer is with an action.
234-
235- ``` ts
236- import { action , all } from " effection" ;
237-
238- export function multiply(... operations ) {
239- return action (function * (resolve ) {
240- let fetchNumbers = operations .map (operation => function * () {
241- let num = yield * operation ;
242- if (num === 0 ) {
243- resolve (0 );
244- }
245- return num ;
246- });
247-
248- let values = yield * all (fetchNumbers );
249-
250- let result = values .reduce ((current , value ) => current * value , 1 );
251-
252- resolve (result );
253- });
254- }
255- ```
256-
257- We wrap each operation that retrieves a number into one that _ immediately_
258- ejects from the entire action with a result of zero the _ moment_ that any zero
259- is detected in _ any_ of the results. The action will yield zero, but before
260- returning control back to its caller, it will ensure that all outstanding
261- requests are completely shutdown so that we can be guaranteed not to leak any
262- effects.
263-
264137[ abort-signal ] : https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
265138[ fetch ] : https://developer.mozilla.org/en-US/docs/Web/API/fetch
266139[ promise-constructor ] : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
0 commit comments