Skip to content
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
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,17 @@ All sub-namespaces and modules have been renamed accordingly:

## Documentation

Documentation is currently being updated.

See [Interoperability with .NET Reactive Extensions](docs/INTEROPERABILITY.md) for existing
observable, observer, and subscription adapters and their asynchronous execution boundaries.

### takeUntil ordering

`Reactive.takeUntil notifier source` stops forwarding source values when it observes a notifier value.
The two subscriptions deliver independently, so source values still in flight may be dropped when the
notifier stops the stream. Posting source values before a notifier value, even with sequential
`do! sourceObserver.OnNextAsync ...` calls, does not guarantee that downstream has received them.
When that ordering matters, arrange downstream acknowledgement before triggering the notifier.
- [Getting started](docs/getting-started.md): subscribe, handle notifications, and dispose
- [Creating streams](docs/creating.md): values, workflows, and subjects
- [Filtering](docs/filtering.md): select, limit, and stop values
- [Transforming](docs/transforming.md): map values and switch or flatten streams
- [Combining](docs/combining.md): concatenate, merge, and pair streams
- [Aggregating](docs/aggregating.md): running state, final results, and groups
- [Error handling](docs/error-handling.md): failures, fallback streams, and retries
- [Time shifting](docs/time-shifting.md): timers, delays, debounce, and sampling
- [Reactive builder](docs/builder.md): write streams with `reactive { ... }`
- [Actor interoperability](docs/actor-interop.md): subscribe actors and build actor-backed streams
- [Interoperability with .NET Reactive Extensions](docs/INTEROPERABILITY.md): adapters and their execution boundaries

## Install

Expand Down
140 changes: 140 additions & 0 deletions docs/actor-interop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Actor interoperability

Fable.Reactive works with [Fable.Actor](https://github.com/fable-hub/Fable.Actor). Open
`Fable.Reactive`, `Fable.Actor`, and `Fable.Actor.Types` for the examples below. An `Actor<'Msg>` is
the send-only reference; an actor body's `ActorInbox<'Msg>` receives messages. Share
`inbox.Reference` when another component needs to post to an actor created inside a handler.

```fsharp
open Fable.Reactive
open Fable.Actor
open Fable.Actor.Types
```

## Send stream notifications to an actor

`Reactive.subscribeActor actor source` subscribes an actor that receives `Notification<'T>` values.
It returns `Async<IReactiveDisposable>`. Disposal unsubscribes from the source; the caller remains
responsible for the actor's lifetime.

```fsharp
let sink =
Actor.spawn (fun inbox ->
let rec loop () =
actor {
let! notification = inbox.Receive()

match notification with
| OnNext value -> printfn "Value: %d" value
| OnError error -> printfn "Error: %s" error.Message
| OnCompleted -> printfn "Completed"

return! loop ()
}

loop ())

let subscription =
Reactive.interval 0 1000
|> Reactive.subscribeActor sink
```

Run `subscription` to obtain its disposable. Keep `sink` alive while it may receive notifications;
stop it separately after its work is finished.

## Map through a stateful actor

`mapActor handler initialState` creates an actor for each subscription. Its handler receives the
current state and one source value, then returns `(nextState, result)`. The operator waits for the
actor's reply before forwarding that result, so actor processing applies backpressure to the
source.

```fsharp
let runningTotals =
Reactive.ofSeq [ 10; 20; 30 ]
|> Reactive.mapActor (fun total value ->
let next = total + value
next, next) 0

// Emits 10, 30, 60.
```

The returned subscription owns this actor as well as the source subscription. Handler failure is
reported as `OnError`.

## Emit from an actor

`flatMapActor` creates a receiving actor for each subscription. The handler gets an `emit` callback
and an `ActorInbox` for source values. It can emit zero, one, or many results per value.

```fsharp
let doubled =
Reactive.interval 0 1000
|> Reactive.flatMapActor (fun emit inbox ->
let rec loop () =
actor {
let! value = inbox.Receive()
emit (value * 2)
return! loop ()
}

loop ())
```

`emit` posts output without waiting for downstream processing. Source completion and error bypass
the actor, so a terminal notification can overtake actor output still in flight. Use `mapActor`
when each input needs an acknowledged, one-to-one result. An actor crash in `flatMapActor` becomes
`OnError`.

## Choose crash behavior

`flatMapActorSupervised decide handler` adds a decision for an actor failure:

| Directive | Behavior |
| --- | --- |
| `Escalate` | Send the failure downstream as `OnError` |
| `Stop` | Leave the child stopped; the crashing value and later values sent to it produce no output |
| `Restart` | Replace the child; the crashing value is lost, and later values go to the replacement |

```fsharp
let restartOnFailure =
Reactive.interval 0 1000
|> Reactive.flatMapActorSupervised
(fun _ -> Directive.Restart)
(fun emit inbox ->
let rec loop () =
actor {
let! value = inbox.Receive()
emit (value * 2)
return! loop ()
}

loop ())
```

As with `flatMapActor`, source terminal notifications bypass the child. Dispose the subscription
with `do! subscription.DisposeAsync()` to await its owned source and actor cleanup. If the handler
needs to share its receiving actor, pass `inbox.Reference` to code that expects an `Actor<'Msg>`.

## Create an actor-backed source

`Reactive.ofActor body` starts an actor immediately and returns its send-only reference and a hot
stream of values emitted by the body.

```fsharp
let publisher, events =
Reactive.ofActor (fun emit inbox ->
let rec loop () =
actor {
let! value = inbox.Receive()
emit (value * 2)
return! loop ()
}

loop ())
```

Subscribe to `events` before posting to `publisher`. Values emitted before subscription are not
replayed. The caller owns the actor's lifetime; disposing a stream subscription removes that
subscriber but does not stop `publisher`. See [creating streams](creating.md#publish-values-to-subscribers)
for subjects and [the reactive builder](builder.md) for composition syntax.
60 changes: 60 additions & 0 deletions docs/aggregating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Aggregating

Aggregation keeps state across values in one subscription. Choose a running result when each
intermediate state matters, or a final result when only the end matters.

## Running results

`scanInit initial accumulator` starts with an explicit state. It emits the updated state after
each source value; it does not emit the initial state on its own. `scan` uses the first source value
as its initial state and emits that value. Both have async variants for an accumulator returning
`Async<'State>`.

```fsharp
open Fable.Reactive

let totals =
Reactive.ofSeq [ 1; 2; 3 ]
|> Reactive.scanInit 0 (+)

// Emits 1, 3, 6, then completes.
```

Use `scanInitAsync initial accumulator` or `scanAsync accumulator` when updating the state takes
asynchronous work. As with other stateful operators, each subscription has its own accumulated
state.

## Final results

`reduceAsync accumulator` emits only the final accumulated value when the source completes. It
uses the first source value as its starting state. `max` and `min` are final-result operators built
on the same reduction pattern. An empty source has no final value to emit.

```fsharp
let largest =
Reactive.ofSeq [ 8; 3; 13; 5 ]
|> Reactive.max

let sum =
Reactive.ofSeq [ 1; 2; 3 ]
|> Reactive.reduceAsync (fun total value -> async { return total + value })
```

`takeLast count` in the [filtering guide](filtering.md) is useful when you want the last values
themselves rather than a reduced result. Both kinds of final result require source completion.

## Group values

`groupBy keyMapper` emits one inner stream per distinct key. Subscribe to each inner stream as the
outer stream emits it: its first value waits for that subscription. Groups receive their source
values and terminal notification, and the outer stream completes after the groups complete.

```fsharp
let perParityTotals =
Reactive.ofSeq [ 1; 2; 3; 4 ]
|> Reactive.groupBy (fun value -> value % 2)
|> Reactive.flatMap (Reactive.scanInit 0 (+))
```

The groups contain values, not a public key property. Keep the key in each value if downstream
consumers need to identify which group produced a result.
73 changes: 73 additions & 0 deletions docs/builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# The reactive builder

Open `Fable.Reactive` to use the `reactive { ... }` computation expression. It produces an
`IAsyncObservable<'T>`, which still needs a subscription to deliver values. Use `yield` for one
value and `yield!` to include an existing stream.

```fsharp
open Fable.Reactive

let values =
reactive {
yield 1
yield! Reactive.ofSeq [ 2; 3 ]
}
```

This stream emits `1`, `2`, and `3`, then completes. Consecutive `yield` and `yield!` clauses are
concatenated: the first stream finishes before the next begins. `reactive { () }` makes an empty
stream. The builder supports `yield`, not `return`.

## Bind a stream or async workflow

`let!` binds each source value to the rest of the expression. For a stream source, it uses the same
merge behavior as `Reactive.flatMap`: inner results can interleave.

```fsharp
let sums =
reactive {
let! left = Reactive.ofSeq [ 1; 2 ]
let! right = Reactive.ofSeq [ 10; 20 ]
yield left + right
}
```

`sums` produces the four combinations `11`, `21`, `12`, and `22`; their arrival order can vary.
`let!` also accepts `Async<'T>` and turns its result into a one-value stream:

```fsharp
let loaded =
reactive {
let! value = async { return 42 }
yield value * 2
}
```

`yield!` can include an `Async<'T>` directly when its one result is the value to emit.

## Iterate in order

`for` accepts an F# sequence or an `IAsyncObservable<'T>`. It uses `concatMap`, so each iteration's
stream completes before the next iteration begins.

```fsharp
let ordered =
reactive {
for value in [ 1; 2; 3 ] do
yield value * 10
}
```

Choose `for` for ordered expansion and `let!` when merging inner streams is acceptable. See
[transforming](transforming.md#map-each-value-to-a-stream) for the corresponding operators.

## Manage the subscription outside the builder

The builder creates a stream; it does not subscribe to it or own its lifetime. Use
`SubscribeAsync` and await `DisposeAsync()` when you are done, as shown in
[getting started](getting-started.md#your-first-subscription).

The ordinary `async { ... }` builder also accepts `use!` for an `IReactiveDisposable`, including a
reactive subscription. Its cleanup starts `DisposeAsync()` without awaiting completion. When
cleanup must finish before continuing, bind the disposable with `let!` and explicitly
`do! subscription.DisposeAsync()`.
44 changes: 44 additions & 0 deletions docs/combining.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Combining streams

Use `concat` when one stream must finish before the next begins. Use `merge` when both can run
together and value interleaving is acceptable.

```fsharp
open Fable.Reactive

let first = Reactive.ofSeq [ 1; 2 ]
let second = Reactive.ofSeq [ 3; 4 ]

let ordered = first |> Reactive.concat second
let concurrent = first |> Reactive.merge second
```

`ordered` emits values from `first`, then `second`. `concurrent` forwards values as they arrive;
do not rely on one source winning a race. `concatSeq` and `mergeSeq` take a sequence of streams.
`mergeInner` flattens a stream of streams with unbounded active inner subscriptions. `startWith`
prepends ordinary values before subscribing to the source.

## Pair values

| API | When it emits |
| --- | --- |
| `combineLatest other source` | After both have produced a value, then on each new value from either stream |
| `withLatestFrom other source` | When `source` emits and `other` already has a value |
| `zipSeq items source` | Pair each source value with the next item in a sequence |

The tuple order is `(sourceValue, otherValue)`. `combineLatest` and `withLatestFrom` currently
complete when either input completes; plan for that when combining finite and long-lived sources.

```fsharp
let readings = Reactive.interval 0 100
let settings = Reactive.interval 0 250

let latestPairs =
readings |> Reactive.combineLatest settings

let annotated =
readings |> Reactive.zipSeq [ "first"; "second"; "third" ]
```

`zipSeq` completes when the source completes or when it tries to pair a source value after the
sequence has run out. For derived values, continue with [aggregating](aggregating.md).
Loading
Loading