diff --git a/docs/actions.mdx b/docs/actions.mdx index 7dec3b61c..2633995b7 100644 --- a/docs/actions.mdx +++ b/docs/actions.mdx @@ -1,51 +1,22 @@ -In this section, we'll cover the first two fundamental operations in Effection: -`suspend()` and `action()`, and how we can use them in tandem to serve as a safe -alternative to the [Promise constructor][promise-constructor]. +In this section, we'll see how we can use `action` as a safe alternative to +the [Promise constructor][promise-constructor]. -## Suspend +## Example: Sleep -Simple in concept, yet bearing enormous practical weight, the `suspend()` -operation is fundamental to Effection. It pauses the current -operation until it [passes out of scope][scope], at which point it will return -immediately. - -Let's revisit our simplified sleep operation from the [introduction to -operations](./operations): +Let's revisit our sleep operation from the [introduction to +operations](../operations): ```js -export function sleep(duration) { - return action(function* (resolve) { +export function sleep(duration: number): Operation { + return action((resolve) => { let timeoutId = setTimeout(resolve, duration); - try { - yield* suspend(); - } finally { - clearTimeout(timeoutId); - } + return () => clearTimeout(timeoutId); }); } ``` As we saw, no matter how the sleep operation ends, it always executes the -`finally {}` block on its way out; thereby clearing out the `setTimeout` -callback. - -It's worth noting that we say the suspend operation will return immediately, -we really mean it. The operation will proceed to return from the suspension -point via _as direct a path as possible_, as though it were returning a value. - -```js {6} -export function sleep(duration) { - return action(function* (resolve) { - let timeoutId = setTimeout(resolve, duration); - try { - yield* suspend(); - console.log('you will never ever see this printed!'); - } finally { - clearTimeout(timeoutId); - } - }); -} -``` +`clearTimeout()` on its way out. If we wanted to replicate our `sleep()` functionality with promises, we'd need 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)]); controller.abort(); ``` -With a suspended action on the other hand, we get all the benefit as if +With an action on the other hand, we get all the benefit as if an abort signal was there without sacrificing any clarity in achieving it. -> 💡Fun Fact: `suspend()` is the only true 'async' operation in Effection. If an -> operation does not include a call to `suspend()`, either by itself or via a -> sub-operation, then that operation is synchronous. - -Most often, [but not always][spawn-suspend], you encounter `suspend()` in the -context of an action as the pivot between that action's setup and teardown. +## Action Constructor -## Actions - -The second fundamental operation, [`action()`][action], serves two -purposes. The first is to adapt callback-based APIs and make them available as -operations. In this regard, it is very much like the -[promise constructor][promise-constructor]. To see this correspondance, let's +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 +[promise constructor][promise-constructor]. To see this, let's use [one of the examples from MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#examples) that uses promises to make a crude replica of the global [`fetch()`][fetch] function. It manually creates an XHR, and hooks up the `load` and `error` events @@ -114,17 +77,19 @@ async function fetch(url) { ``` Consulting the [Async Rosetta Stone](async-rosetta-stone), we can substitute the async -constructs for their Effection counterparts to arrive at a line for line -translation. +constructs for their Effection counterparts to arrive at an (almost) line for line +translation. The only significant difference is that unlike the promise constructor, an +action constructor _must_ return a "finally" function to exit the action. ```js function* fetch(url) { - return yield* action(function*(resolve, reject) { + return yield* action((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); + return () => {}; // "finally" function place holder. }); } ``` @@ -133,7 +98,7 @@ While this works works every bit as well as the promise based implementation, it turns out that the example from MDN has a subtle bug. In fact, it's the same subtle bug that afflicted the "racing sleep" example in the [introduction to -operations](http://localhost:8000/docs/operations#cleanup). If +operations](../operations#cleanup). If we no longer care about the outcome of our `fetch` operation, we will "leak" its http request which will remain in flight until a response 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. ```js await Promise.race([ fetch("https://openweathermap.org"), - fetch("https://open-meteo.org") -]) + fetch("https://open-meteo.org"), +]); ``` -With Effection, this is easily fixed by suspending the operation, and making -sure that the request is cancelled when it is either resolved, rejected, or +With Effection, this is easily fixed by calling `abort()` in the finally function to +make sure that the request is cancelled when it is either resolved, rejected, or passes out of scope. -```js {8-12} showLineNumbers +```js {8} showLineNumbers function* fetch(url) { - return yield* action(function*(resolve, reject) { + return yield* action((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); - try { - yield* suspend(); - } finally { + return () => { xhr.abort(); - } + }; // called in all cases }); } ``` ->💡Almost every usage of the [promise concurrency primitives][promise-concurrency] +> 💡Almost every usage of the [promise concurrency primitives][promise-concurrency] > will contain bugs born of leaked effects. -As we've seen, actions can do anything that a promise can do (and more safely -at that), but they also have a super power that promises do not. If you recall -from the very beginning of this article, a key difference in Effection is that -operations are values which, unlike promises, do not represent runtime state. -Rather, they are "recipes" of what to do, and in order to do them, they need to -be run either explicitly with `run()` or by including them with `yield*`. - -This means that when every operation runs, it is bound to an explicit -lexical context; which is a fancy way of saying that ___running an -operation can only ever return control to a single location___. A -promise on the other hand, because it accepts an unlimited number of -callbacks via `then()`, `catch()`, and `finally()`, can return control -to an unlimited number of locations. This may seem a small thing, but -it is very powerful. To demonstrate, consider the following set of -nested actions. - -```js -await run(function* () { - yield* action(function* (resolve) { - try { - yield* action(function*() { - try { - yield* action(function*() { resolve() }); - } finally { - console.log('inner') - } - }); - } finally { - console.log('middle'); - } - }); - console.log('outer'); -}); -``` - -When we run it, it outputs the strings `inner`, `middle`, and `outer` in order. -Notice however, that we never actually resolved the inner actions, only the -outer one, and yet every single piece of teardown code is executed as expected -as the call stack unwinds and it proceeds back to line 2. This means you can use -actions to "capture" a specific location in your code as an "escape point" and -return to it an any moment, but still feel confident that you won't leak any -effects when you do. - -Let's consider a slightly more practical example of when this functionality -could come in handy. Let's say we have a bunch of numbers scattered across the -network that we want to fetch and multiply together. We want to write an -to muliply these numbers that will use a list of operations that retreive the -numbers for us. - -In order to be time efficient we want to fetch all the numbers -concurrently so we use the [`all()`][all] operation. However, because -this is multiplication, if any one of the numbers is zero, then the -entire result is zero, so if at any point we discover that there is a -`0` in any of the inputs to the computation, there really is no -further point in continuing because the answer will be zero no matter -how we slice it. It would save us time and money if there were a -mechanism to "short-circuit" the operation and proceed directly to -zero, and in fact there is! - -The answer is with an action. - -```ts -import { action, all } from "effection"; - -export function multiply(...operations) { - return action(function* (resolve) { - let fetchNumbers = operations.map(operation => function* () { - let num = yield* operation; - if (num === 0) { - resolve(0); - } - return num; - }); - - let values = yield* all(fetchNumbers); - - let result = values.reduce((current, value) => current * value, 1); - - resolve(result); - }); -} -``` - -We wrap each operation that retrieves a number into one that _immediately_ -ejects from the entire action with a result of zero the _moment_ that any zero -is detected in _any_ of the results. The action will yield zero, but before -returning control back to its caller, it will ensure that all outstanding -requests are completely shutdown so that we can be guaranteed not to leak any -effects. - [abort-signal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal [fetch]: https://developer.mozilla.org/en-US/docs/Web/API/fetch [promise-constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise diff --git a/docs/installation.mdx b/docs/installation.mdx index efa7d8814..0bca69653 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -3,12 +3,6 @@ If you encounter obstacles integrating with your environment, please create a [G ## Node.js & Browser -

- Bundlephobia badge showing effection minified - Bundlephobia badge showing effection gzipped size - Bundlephobia badge showing it's treeshackable -

- 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. ```bash @@ -24,7 +18,7 @@ yarn add effection 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: ```ts -import { main } from "https://jsr.io/@effection/effection/doc"; +import { main } from "jsr:@effection/effection@3"; ``` > 💡 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]. diff --git a/docs/structure.json b/docs/structure.json index 09b1640a2..cbbeb5f7e 100644 --- a/docs/structure.json +++ b/docs/structure.json @@ -8,13 +8,13 @@ ], "Learn Effection": [ ["operations.mdx", "Operations"], - ["actions.mdx", "Actions and Suspensions"], - ["resources.mdx", "Resources"], ["spawn.mdx", "Spawn"], + ["resources.mdx", "Resources"], ["collections.mdx", "Streams and Subscriptions"], ["events.mdx", "Events"], ["errors.mdx", "Error Handling"], - ["context.mdx", "Context"] + ["context.mdx", "Context"], + ["actions.mdx", "Actions"] ], "Advanced": [ ["scope.mdx", "Scope"],