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

Update docs to cover the basics of Command #298

Merged
merged 2 commits into from
Jan 21, 2025
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
5 changes: 5 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

1. [Hello world](./guide/hello_world.md)
1. [Elm Architecture](./guide/elm_architecture.md)
1. [Managed Effects](./guide/effects.md)
1. [Capabilities](./guide/capabilities.md)
1. [Capability APIs](./guide/capability_apis.md)
1. [Testing](./guide/testing.md)
Expand All @@ -36,3 +37,7 @@
1. [FFI bridge](./internals/bridge.md)
1. [The Effect type](./internals/effect.md)
1. [Type generation](./internals/typegen.md)

# RFCs

1. [Command API](./rfcs/command.md)
Binary file added docs/src/command_overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions docs/src/guide/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ From the perspective of the app, you can think of capabilities as an equivalent
to SDKs. And a lot of them will provide an interface to the actual platform
specific SDKs.

```admonish warning title="Capability API Migration"
The capability API is in the middle of a migration. While all the instructions in
this chapter will work, you should probably read the [Managed Effects](./effects.md)
chapter first to see where Crux is headed.
```

## Intent and execution

The Capabilities are the key to Crux being portable across as many platforms as
Expand Down
99 changes: 99 additions & 0 deletions docs/src/guide/effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Managed Effects

The approach to side-effects Crux uses is sometimes called **managed** side-effects. You app's core
is not allowed to perform side-effects directly. Instead, whenever it wants to interact with the outside
world, it needs to request the interaction from the shell.

Think of your whole app as a robot, where the Core is the brain of the robot and the Shell is the body of the robot.
The brain instructs the body through capabilities and the body passes information about the outside world
back to it with Events.

```admonish warning title="Capability API Migration"
The side-effect API is in the middle of a migration. For the original API to request effects, see the
[next chapter about Capabilities](./capabilities.md).

The migration will happen in three stages:

1. Now: enable the Command API
2. Next: add Command based APIs to published capabilities
3. Later: remove original capability support
```

From `crux_core` version 0.11 there is a new API for side-effects which more closely matches the mental
model for Crux apps, in which the `update` function _returns_ effect requests. It is facilitated by a new type - `Command`.
The `Command` encapsulates the entire process orchestrating a potentially complex flow of side-effects resulting
from this one `update` call.

In this chapter we will explore how commands are created and used, before we dive into capabilities in the next
chapter.

## Migrating from previous versions of Crux

The change to `Command` is a breaking one for all Crux apps, but the fix is quite minimal.

There are two parts to it:

1. declare the `Effect` type on your App
2. return `Command` from `update`

Here's an example:

```rust
impl crux_core::App for App {
type Event = Event;
type Model = Model;
type ViewModel = ViewModel;

type Capabilities = Capabilities;
type Effect = Effect; // 1. add the associated type

fn update(
&self,
event: Event,
model: &mut Model,
caps: &Capabilities,
) -> crux_core::Command<Effect, Event> {
crux_core::Command::done() // 2. return a Command
}
}

```

In a typical app the `Effect` will be derived from `Capabilities`, so the added line should just work.

To begin with, you can simply return a `Command::done()` from the `update` function. `Command::done()`
is a no-op effect.

## What is a Command

The Command is a recipe for a side-effects workflow which may perform several effects and also send events
back to the app.

![Core, updated and command](../command_overview.png)

Crux expects Command to be returned by the `update` function. Command can be asked for the effects and events that
have been emitted so far. Each effect carries a request for an operation (e.g. a HTTP request), which can be inspected
and resolved with an operation output (e.g. a HTTP response). After effect requests are resolved, the command
may have further effect requests or events, depending on the recipe it's executing.

This API can be used both in tests and in Rust based shells, and for some advanced use-cases when composing applications.

## Capabilities

Capabilities are developer-friendly, ergonomic APIs to construct commands, from very basic ones all the way to
complex stateful orchestrations.

For now, the capability API support is limited to directly created commands, and the `Render` capability:
instead of `caps.render.render()` you can call `crux_core::render::render()` and return the command it
builds.

## Working with Commands


```admonish warning "Work in progress"
This documentation is work in progress and will be expanded as we complete the
`Command` migration.

For now, you can read the [original RFC](../rfcs/command.md) explaining the reasoning behind the change,
And the [API docs](https://docs.rs/crux_core/latest/crux_core/command/index.html)
```
2 changes: 1 addition & 1 deletion docs/src/guide/elm_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The Shell thus has two sides: the _driving_ side – the interactions causing ev

Effects encode potentially quite complex, but common interactions, so they are the perfect candidate for some improved ergonomics in the APIs. This is where Crux capabilities come in. They provide a nicer API for creating effects, and in the future, they will likely provide implementations of the effect execution for the various supported platforms. Capabilities can also implement more complex interactions with the outside world, such as chained network API calls or processing results of effects, like parsing JSON API responses.

We will look at how capabilities work, and will build our own in the next chapter.
We will look at how effects are created and passed to the shell in the next chapter. In the one following that we'll explore how capabilities work, and will build our own.

---

Expand Down
28 changes: 8 additions & 20 deletions crux_core/src/command/RFC.md → docs/src/rfcs/command.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@


### Note for reviewers

Have no fear! About 1500 of the total lines of this PR is int tests. They do demonstrate the APIs quite well, but hopefully don't require very thorough checking. Another 250 lines is a copy of this PR description (so it stays around).

I've left a number of comments in places starting `RFC:` - these are places where I have specific questions about the details of this proposal and would love your feedback. Thank you!!

## What is this RFC?
# RFC: New side effect API - Command

This is a proposed implementation of a new API for creating (requesting) side-effects in crux apps. It is quite a significant part of the Crux API surface, so I'd like feedback on the direction this is taking, rather than just hitting merge.

Expand Down Expand Up @@ -110,13 +103,13 @@ Instead, to create a request followed by another request you can use the builder

```rust
let command = Command::request_from_shell(a_request)
.then(|response| Command::request_from_shell(make_another_request_from(response)))
.then_request(|response| Command::request_from_shell(make_another_request_from(response)))
.then_send(Event::Done);
```

This works just the same with streams or combinations of requests and streams.

`.then` and `Command::all` are nice, but on occasion, you will need the full power of async. The equivalent of the above with async works like this:
`.then_*` and `Command::all` are nice, but on occasion, you will need the full power of async. The equivalent of the above with async works like this:

```rust
let command = Command::new(|ctx| async {
Expand Down Expand Up @@ -222,23 +215,18 @@ A grab bag of other things:
* Tasks can be aborted by calling `.abort()` on a `JoinHandle`
* Whole commands can be aborted using an `AbortHandle` returned by `.abort_handle()`. The handle can be stored in the model and used later.
* Commands can be "hosted" on a pair of channel senders returning a future which should be compatible with the existing executor enabling a reasonably smooth migration path
* I havent looked at implementing any, but this API should in theory enable declarative effect middlewares like caching, retries, throttling, timeouts, etc...
* This API should in theory enable declarative effect middlewares like caching, retries, throttling, timeouts, etc...

### Limitations and drawbacks

I'm sure we'll find some. :)

For one, the return type signature for capabilities is not great, for example: `RequestBuilder<Effect, Event, impl Future<Output = AnOperationOutput>>`. The command builder chaining is using dirty trait tricks, and as a result requires the `CommandBuilder` trait to be in scope - not completely ideal (but the same is true for `FutureExt` and `StreamExt`).
For one, the return type signature for capabilities is not great, for example: `RequestBuilder<Effect, Event, impl Future<Output = AnOperationOutput>>`.

One major perceived limitation which still remains is that `model` is not accessible from the effect code. This is by design, to avoid data races from concurrent access to the model. It should hopefully be a bit more obvious now that the effect code is _returned_ from the `update` function wrapped in a Command.

I suspect there are ways around this by storing some `Arc<Mutex<T>>`s in the model and cloning them into the effect code, etc., but do so at your own peril - tracking causality will become a lot more complex in that world.

## Open questions and other considerations

* I have not fully formed a migration plan yet - it should be possible for the two APIs to coexist for a few versions while people move over
* The command API expects the `Effect` type to implement `From<Request<Op>>` for any capability Operations it is used with.
* I have not fully thought about back-pressure in the Commands (for events, effects and spawned tasks) even to the level of "is any needed?"
* I am not super sure about my implementation of task cancellation using atomics, because they break my head. Help.

I thank and salute you for reading all the way down here. If you have, I 100% owe you a beer - tell me "I've read your entire RFC treatise, and I'm here for my free beer" next time you see me in or near a pub :).
* The command API expects the `Effect` type to implement `From<Request<Op>>` for any capability Operations it is used with. This is derived by the `Effect` macro, and is expected to be supported by a derive macro even in the future state.
* We have not fully thought about back-pressure in the Commands (for events, effects and spawned tasks) even to the level of "is any needed?"
* We will explore ways to make the code that interleaves effects and state updates more "linear" - require fewer intermediate events - separately at a later stage
Loading