Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added context documentation #905

Merged
merged 6 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions www/assets/images/overriding-context.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions www/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 121 additions & 0 deletions www/docs/context.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
Effection contexts store ambient values that are needed by the operations in your application in order to run. Some of the most common
use cases of `Context` are


* **Configuration**: most apps require values that come from the environment such as evironment variables or configuration files. Use context to easily retrieve such values from within any operation.
* **Client APIs**: many APIs provide client libraries to connect with them. With an Effection context, you can create a client instance once and share it between all child operations.
* **Services**: Database connections, Websockets, and many other types of APIs involve a handle to a stateful object that needs to be
destroyed accordingly to a set lifecycle. Combined with [resource api][resources], Effection context allows you to share a service between child operations and also to guarantee that it is disposed of properly when it is no longer needed.

## The problem

Usually, you can pass information from a parent operation to a child operation
via function argument or lexical scope. But passing function arguments can become
verbose and inconvenient when the you have to pass them through many operations in
the middle, or if many operations need the same information. Likewise, passing information
via lexical scope is only possible if you define the child operation in the body of the
parent operation. `Context` lets the parent operation make some information available to any
operation in the tree below it-no matter how deep-without passing it explicitely through function
arguments or lexical scope.

> 💁 If you're familiar with React Context, you already know most of
> what you need to know about Effection Context. The biggest difference
> is the API but general concepts are same.

## What is argument drilling?

Passing argument to operations is a convenient way to make data from parent operation available to the child operation.

But passing arguments can become inconvenient when the child operation is nested deeply in the tree of operations, or
the same arguments need to be passed to many operations. This situation is called "argument drilling".

Wouldn't it be great if you could access information in a deeply nested operation from a parent operation without
modifying operations in the middle? That's exaclty what Effection Context does.

## Context: an alternative to passing arguments

Context makes a value available to any child process within a tree of processes.

``` typescript
import { createContext, main } from 'effection';

// 1. create the context
const GithubTokenContext = createContext("token");

await main(function* () {
// 2. set the context value
yield* TokenContext.set("gha-1234567890");

yield* fetchGithubData();
})

function* fetchGithubData() {
yield* fetchRepositories();
}

function* fetchRepositories() {
// 3. use the context value in a child operation
const token = yield* GithubTokenContext;

console.log(token);
// -> gha-1234567890
}
```

## Context: overriding nested context

Context is attached to the scope of the parent operation. That means that the operation and _all of its children_ will see that same context.

However, if a child operation sets its own value for the context, it will _not_ affect the value of any of its ancestors.

<div class="max-w-md mx-auto md:max-w-2xl">
![Parent sets value to A, Child overrides value to B and all children below get B](/assets/images/overriding-context.svg)
</div>


## Using Context

To use context in your operations, you need to do the following 3 steps:
1. **Create** a context.
2. **Set** the context value.
3. **Yield** the context value.

### Step 1: Create a context.

Effection provides a function for creating a context appropriatelly called `createContext`. This function will return
a reference that you use to identify the context value in the scope.

``` javascript
import { createContext } from 'effection'

const MyValueContext = createContext("my-value");
```

### Step 2: Set the context value.

``` javascript
await main(function* () {
yield* MyValueContext.set("Hello World!");
});
```

### Step 3: Yield the context value.

```javascript

await main(function* () {
yield* MyValueContext.set("Hello World!");

yield* logMyValue();
});

function* logMyValue() {
const value = yield* MyValueContext;

console.log(value);
}
```

[scope]: /docs/guides/scope
[resources]: /docs/guides/resources
[React Context]: https://react.dev/learn/passing-data-deeply-with-context
8 changes: 4 additions & 4 deletions www/docs/operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ultimately make Effection far more robust at handling asynchrony, and in this
section we'll unpack these differences in more detail. In summary: operations
are stateless, and they can be cancelled with grace.

### Stateless
## Stateless

The fundamental unit of abstraction for `async/await` is the `Promise`. They can
be created independently or with an [async function][], but either way,
Expand Down Expand Up @@ -35,7 +35,7 @@ sayHello();
This is because unlike promises, Operations do not do anything by themselves.
Instead, they describe what should be done when the operation is run.

### Running Operations
## Running Operations

In the example above, the generator function contains a recipe that says "log
'Hello World' to the console", but it does not actually execute that recipe
Expand Down Expand Up @@ -93,7 +93,7 @@ await main(function*() {
Instead of needing to catch the error like we did before, `main()`
will handle it for us, including printing it to the console.

### Composing Operations
## Composing Operations

Entry points like `run()` and `main()` are used usually once at the very
beginning of an Effection program, but the easiest and most common way to
Expand Down Expand Up @@ -134,7 +134,7 @@ each other, and in fact, composition is so core to how operations work that
_every operation in Effection eventually boils down to a combination of
just three primitive operations_: `action()`, `resource()`, and `suspend()`.

### Cleanup
## Cleanup

Perhaps the most critical difference between promises and operations is that
once started, a promise will always run to completion no matter how long that
Expand Down
18 changes: 9 additions & 9 deletions www/docs/scope.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ teardown as a unit so that automatic cleanup is _guaranteed_, but how are
they able to do this, and how can you implement your own operations that clean
up after themselves?

### Scope of a lifetime
## Scope of a lifetime

Every operation in Effection runs in the context of an associated scope which
places a hard limit on how long it will live. For example, the script below uses
Expand Down Expand Up @@ -63,7 +63,7 @@ variable references. Because of this, once the outcome of an operation becomes
known, or it is no longer needed, that operation and all of the operations it
contains can be safely shut down.

### The Three Outcomes
## The Three Outcomes

There are only three ways an operation may pass out of scope.

Expand All @@ -75,7 +75,7 @@ operation is halted.
No matter which one of these happens, every sub-operation associated with that
operation will be automatically destroyed.

### Suspend (it's not the end)
## Suspend (it's not the end)

In order to understand the lifecycle of an Operation, we must first understand the
concept of halting a Task.
Expand All @@ -95,7 +95,7 @@ await task.halt();
Halting a Task means that its operation is canceled, and it also causes any
operation created by that operation to be halted.

### Immediate return
## Immediate return

If an Operation is expressed as a generator (most are), we call `return()`
on the generator when that operation is halted. This
Expand Down Expand Up @@ -144,7 +144,7 @@ let task = run(function*() {
await task.halt();
```

### Cleaning up
## Cleaning up

We can use this mechanism to run code as an Operation is shutting
down regardless of whether it completes successfully, is halted, or
Expand Down Expand Up @@ -173,7 +173,7 @@ let task = run(function*() {
await task.halt();
```

### Asynchronous halt
## Asynchronous halt

You might be wondering what happens when we `yield*` inside the
_finally_ block. In fact, Effection handles this case for you:
Expand All @@ -199,7 +199,7 @@ it is good practice to keep halting speedy and simple. We recommend avoiding
expensive operations during halt where possible, and avoiding throwing any
errors during halting.

### Ensure
## Ensure

Sometimes you want to avoid the rightward drift of using lots of
`try/finally` blocks. The `ensure` operation that ships with
Expand All @@ -222,7 +222,7 @@ let task = run(function*() {
await task.halt();
```

### Abort Signal
## Abort Signal

While cancellation and teardown are handled automatically for us as long as we
are using Effection operations, what do we do when we want to integrate with a
Expand Down Expand Up @@ -275,7 +275,7 @@ function* request(url) {
Now, no matter what happens, when the `request` operation is completed (or
canceled), the HTTP request is guaranteed to be shut down.

### Embedding API
## Embedding API

The nice thing about scope is that you don't need to worry about it. It's just
there, ensuring that things get cleaned up as soon as they are no longer needed.
Expand Down
12 changes: 6 additions & 6 deletions www/docs/spawn.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ main(function*() {
This works, but it slightly inefficient because we are running the fetches one
after the other. How can we run both `fetch` operations at the same time?

### Using `async/await`
## Using `async/await`

If we were just using `async/await` and not using Effection, we might do
something like this to fetch the dates at the same time:
Expand All @@ -36,7 +36,7 @@ async function() {
}
```

### Dangling Promises
## Dangling Promises

This works fine as long as both fetches complete successfully, but what happens
when one of them fails? Since there is no connection between the two tasks, a
Expand All @@ -52,7 +52,7 @@ We call these situations "dangling promises", and most significantly complex
JavaScript applications suffer from this problem. `async/await` fundamentally does
not handle cancellation very well when running multiple operations concurrently.

### Effection
## With Effection

How does Effection deal with this situation? If we wrote the example using
Effection in the exact same way as the `async/await` example, then we will find
Expand Down Expand Up @@ -96,7 +96,7 @@ main(function*() {
This has the same problem as our `async/await` example: a failure in one fetch
has no effect on the other!

### Introducing `spawn`
## Introducing `spawn`

The `spawn` operation is Effection's solution to this problem!

Expand Down Expand Up @@ -147,7 +147,7 @@ cause the parent to error, which in turn halts any siblings.
This idea is called [structured concurrency], and it has profound effects on
the composability of concurrent code.

### Using combinators
## Using combinators

We previously showed how we can use the `Promise.all` combinator to implement
the concurrent fetch. Effection also ships with some combinators, for example
Expand All @@ -162,7 +162,7 @@ main(function *() {
});
```

### Spawning in a Scope
## Spawning in a Scope

The `spawn()` operation always runs its operation as a child of the current
operation. Sometimes however, you might want to run an operation as a child of a
Expand Down
3 changes: 2 additions & 1 deletion www/docs/structure.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
["spawn.mdx", "Spawn"],
["collections.mdx", "Streams and Subscriptions"],
["events.mdx", "Events"],
["errors.mdx", "Error Handling"]
["errors.mdx", "Error Handling"],
["context.mdx", "Context"]
],
"Advanced": [
["scope.mdx", "Scope"],
Expand Down
Loading
Loading