You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With a suspended action on the other hand, we get all the benefit as if
55
+
With an action on the other hand, we get all the benefit as if
84
56
an abort signal was there without sacrificing any clarity in achieving it.
85
57
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.
58
+
## Action Constructor
89
59
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.
92
-
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
60
+
The [`action()`][action] function provides a callback based API to create Effection operations. 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
99
62
use [one of the examples from MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#examples)
100
63
that uses promises to make a crude replica of the global [`fetch()`][fetch]
101
64
function. It manually creates an XHR, and hooks up the `load` and `error` events
@@ -114,17 +77,19 @@ async function fetch(url) {
114
77
```
115
78
116
79
Consulting 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.
119
83
120
84
```js
121
85
function*fetch(url) {
122
-
returnyield*action(function*(resolve, reject) {
86
+
returnyield*action((resolve, reject)=> {
123
87
let xhr =newXMLHttpRequest();
124
88
xhr.open("GET", url);
125
89
xhr.onload= () =>resolve(xhr.responseText);
126
90
xhr.onerror= () =>reject(xhr.statusText);
127
91
xhr.send();
92
+
return () => { } // "finally" function place holder.
128
93
});
129
94
}
130
95
```
@@ -133,7 +98,7 @@ While this works works every bit as well as the promise based
133
98
implementation, it turns out that the example from MDN has a subtle
134
99
bug. In fact, it's the same subtle bug that afflicted the "racing
135
100
sleep" example in the [introduction to
136
-
operations](http://localhost:8000/docs/operations#cleanup). If
101
+
operations](../operations#cleanup). If
137
102
we no longer care about the outcome of our `fetch` operation, we will
138
103
"leak" its http request which will remain in flight until a response
139
104
is received. In the example below it does not matter which web request
@@ -147,119 +112,26 @@ await Promise.race([
147
112
])
148
113
```
149
114
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
152
117
passes out of scope.
153
118
154
-
```js {8-12} showLineNumbers
119
+
```js {8} showLineNumbers
155
120
function*fetch(url) {
156
-
returnyield*action(function*(resolve, reject) {
121
+
returnyield*action((resolve, reject)=> {
157
122
let xhr =newXMLHttpRequest();
158
123
xhr.open("GET", url);
159
124
xhr.onload= () =>resolve(xhr.responseText);
160
125
xhr.onerror= () =>reject(xhr.statusText);
161
126
xhr.send();
162
-
try {
163
-
yield*suspend();
164
-
} finally {
165
-
xhr.abort();
166
-
}
127
+
return () => { xhr.abort(); }; // called in all cases
167
128
});
168
129
}
169
130
```
170
131
171
132
>💡Almost every usage of the [promise concurrency primitives][promise-concurrency]
172
133
> will contain bugs born of leaked effects.
173
134
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
-
awaitrun(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
-
exportfunction multiply(...operations) {
239
-
returnaction(function* (resolve) {
240
-
let fetchNumbers =operations.map(operation=>function* () {
241
-
let num =yield*operation;
242
-
if (num===0) {
243
-
resolve(0);
244
-
}
245
-
returnnum;
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
0 commit comments