Skip to content

Commit c6a57bc

Browse files
authored
Merge pull request #1020 from thefrontside/tm/cleanup-actions-page
Clean up actions page in v3
2 parents fe97560 + 1d0e1ed commit c6a57bc

File tree

3 files changed

+33
-166
lines changed

3 files changed

+33
-166
lines changed

docs/actions.mdx

Lines changed: 29 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,22 @@
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

2818
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-
```
19+
`clearTimeout()` on its way out.
4920

5021
If we wanted to replicate our `sleep()` functionality with promises, we'd need
5122
to do something like accept an [`AbortSignal`][abort-signal] as a second
@@ -80,22 +51,14 @@ await Promise.all([sleep(10, signal), sleep(1000, signal)]);
8051
controller.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
8455
an 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
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
@@ -143,124 +108,32 @@ until _both_ requests are have received a response.
143108
```js
144109
await 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
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 {
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

docs/installation.mdx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ If you encounter obstacles integrating with your environment, please create a [G
33

44
## Node.js & Browser
55

6-
<p class="inline-flex my-0 flex-wrap gap-1">
7-
<img class="my-1" src="https://badgen.net/bundlephobia/min/effection" alt="Bundlephobia badge showing effection minified" />
8-
<img class="my-1" src="https://badgen.net/bundlephobia/minzip/effection" alt="Bundlephobia badge showing effection gzipped size" />
9-
<img class="my-1" src="https://badgen.net/bundlephobia/tree-shaking/effection" alt="Bundlephobia badge showing it's treeshackable" />
10-
</p>
11-
126
Effection is available on [NPM][npm], as well as derived registries such as [Yarn][yarn] and [UNPKG][unpkg]. It comes with TypeScript types and can be consumed as both ESM and CommonJS.
137

148
```bash
@@ -24,7 +18,7 @@ yarn add effection
2418
Effection has first class support for Deno because it is developed with [Deno](https://deno.land). Releases are published to [https://deno.land/x/effection](https://deno.land/x/effection). For example, to import the `main()` function:
2519

2620
```ts
27-
import { main } from "https://jsr.io/@effection/effection/doc";
21+
import { main } from "jsr:@effection/effection@3";
2822
```
2923

3024
> 💡 If you're curious how we keep NPM/YARN and Deno packages in-sync, you can [checkout the blog post on how publish Deno packages to NPM.][deno-npm-publish].

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)