Skip to content

Commit d885096

Browse files
committed
Renamed actions page made it last in the list
1 parent 806ecce commit d885096

File tree

2 files changed

+28
-156
lines changed

2 files changed

+28
-156
lines changed

docs/actions.mdx

Lines changed: 25 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,23 @@
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 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.
6+
Let's revisit our sleep operation from the [introduction to
7+
operations](../operations):
118

12-
Let's revisit our simplified sleep operation from the [introduction to
13-
operations](./operations):
149

1510
```js
16-
export function sleep(duration) {
17-
return action(function* (resolve) {
11+
export function sleep(duration: number): Operation<void> {
12+
return action((resolve) => {
1813
let timeoutId = setTimeout(resolve, duration);
19-
try {
20-
yield* suspend();
21-
} finally {
22-
clearTimeout(timeoutId);
23-
}
14+
return () => clearTimeout(timeoutId);
2415
});
2516
}
2617
```
2718

2819
As 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-
```
20+
`clearTimeout()` on its way out.
4921

5022
If we wanted to replicate our `sleep()` functionality with promises, we'd need
5123
to do something like accept an [`AbortSignal`][abort-signal] as a second
@@ -80,22 +52,13 @@ await Promise.all([sleep(10, signal), sleep(1000, signal)]);
8052
controller.abort();
8153
```
8254

83-
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
8456
an abort signal was there without sacrificing any clarity in achieving it.
8557

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
8959

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
9962
use [one of the examples from MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#examples)
10063
that uses promises to make a crude replica of the global [`fetch()`][fetch]
10164
function. It manually creates an XHR, and hooks up the `load` and `error` events
@@ -114,17 +77,19 @@ async function fetch(url) {
11477
```
11578

11679
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.
11983

12084
```js
12185
function* 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
13398
implementation, it turns out that the example from MDN has a subtle
13499
bug. In fact, it's the same subtle bug that afflicted the "racing
135100
sleep" example in the [introduction to
136-
operations](http://localhost:8000/docs/operations#cleanup). If
101+
operations](../operations#cleanup). If
137102
we 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
139104
is received. In the example below it does not matter which web request
@@ -147,119 +112,26 @@ await Promise.race([
147112
])
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
152117
passes out of scope.
153118

154-
```js {8-12} showLineNumbers
119+
```js {8} showLineNumbers
155120
function* 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 {
165-
xhr.abort();
166-
}
127+
return () => { xhr.abort(); }; // called in all cases
167128
});
168129
}
169130
```
170131

171132
>💡Almost every usage of the [promise concurrency primitives][promise-concurrency]
172133
> will contain bugs born of leaked effects.
173134
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.
263135

264136
[abort-signal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
265137
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/fetch
@@ -269,4 +141,4 @@ effects.
269141
[scope]: ./scope
270142
[spawn-suspend]: ./spawn#suspend
271143
[action]: /api/v3/action
272-
[all]: /api/v3/all
144+
[all]: /api/v3/all

docs/structure.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
],
99
"Learn Effection": [
1010
["operations.mdx", "Operations"],
11-
["actions.mdx", "Actions and Suspensions"],
12-
["resources.mdx", "Resources"],
1311
["spawn.mdx", "Spawn"],
12+
["resources.mdx", "Resources"],
1413
["collections.mdx", "Streams and Subscriptions"],
1514
["events.mdx", "Events"],
1615
["errors.mdx", "Error Handling"],
17-
["context.mdx", "Context"]
16+
["context.mdx", "Context"],
17+
["actions.mdx", "Actions"]
1818
],
1919
"Advanced": [
2020
["scope.mdx", "Scope"],

0 commit comments

Comments
 (0)