# Contracts

:::warning
**Preview.** Ranvier is pre-release; its APIs may change.
:::

These contracts define which threads may access a graph, how failures reach readers and how
owners dispose nodes. Each section links to a guide with examples.

## Threading

*Implemented.*

A graph belongs to the thread that constructed it, the **owning thread**. A program that follows the table
below runs every body, cleanup and comparer on the owning thread. The exception is the code of an async body
after its first `await` that suspends: it runs wherever its task resumes, outside the graph's tracking.
Under `ThreadAffinity.Serialised` the graph has no owning thread; see [Serialised hosts](#serialised-hosts).

These thread and inbox rules describe .NET. Under Fable, one graph runs in one JavaScript context:
dispatch runs inline, affinity checks are omitted, `IsOnGraphThread` is true, and `PendingWork` and
`Pump ()` return zero. Promise delivery still uses microtasks. See the [Fable target](../fable/index.md).

### Which threads may call what

| Operation | Thread | Called from another thread under `Guarded` | Under `Serialised` |
| --- | --- | --- | --- |
| `signal.Value <- v` | Owning | Raises `InvalidOperationException` before the equality check. The value stays unchanged, and an equal write raises too. | Runs on any thread on the construction context while no other thread is inside the graph. Raises otherwise, before the equality check. |
| `graph.Pump ()` | Owning | Raises `InvalidOperationException`. | As a write. |
| `graph.Dispatch work` | Any | Queues `work` in the graph's inbox. On the owning thread, `work` runs inline. | Runs `work` inline on the thread inside the graph. Queues it from every other thread, including one on the construction context. |
| `graph.DispatchAsync work` | Any | As `Dispatch`, and the returned task completes once `work` has run. On the owning thread, `work` runs inline and the task is already complete. | As `Dispatch`. |
| `AsyncSource.Settle` and `Fail`, flight completions, stream items | Any | Queued in the inbox, as `Dispatch` is. On the owning thread, they apply inline. | As `Dispatch`. |
| Stale reads, node creation, `batch`, `flush`, `untrack`, `createRoot`, `onCleanup`, `Dispose` | Owning | Raise `InvalidOperationException`. A read of a current value is unchecked. | As a write. |
| `Journal.key`, `Journal.keySource`, `Journal.messages`, `Journal.replay`, `Journal.work` | Owning | Raise `InvalidOperationException` on a graph with a journal. | As a write. |
| `Journal.produce` | Any | `produce` queues its input, as `Dispatch` does. On the owning thread, it runs inline. | As `Dispatch`. |

Under `Guarded`, the affinity check compares the calling thread with the thread that constructed the graph.
The check depends on the thread alone: a second thread that has the owning thread's
`SynchronizationContext` installed still fails the check. The check therefore holds on hosts where a context
can be installed on more than one thread, such as a Blazor Server renderer
([aspnetcore#69323](https://github.com/dotnet/aspnetcore/issues/69323)).

`ThreadAffinity.Unchecked` removes the check. The caller then guarantees that one thread at a time touches
the graph. `ThreadAffinity.Serialised` replaces it with a check that one thread at a time is inside the graph.

### Where work runs

- **Effects** run on the owning thread, in the flush at the end of the write or batch that woke them, or in
  the flush that `Pump` runs after draining the inbox.
- **Projection passes and lookup refreshes** run first in a flush, before every queued effect, so an effect
  reads keys, rows and lookup values from the write that woke it. A projection created in an effect's run
  therefore runs its pass before that effect re-runs, even when the re-run disposes it, and a pass whose
  source an effect in the same flush rewrites runs once more after the write.
- **A write inside an effect body** joins the running flush. The effects it wakes run before the outer write
  returns, and an effect that writes a signal it reads re-runs until the value stops changing.
- **A write inside a memo body** wakes effects that run once the outermost body or read has finished, before
  that read returns.
- **Async settles** from another thread wait in the inbox until the owning thread drains it. The graph's
  dispatcher, chosen at construction, decides when that happens:

| Dispatcher | The inbox drains |
| --- | --- |
| A `SynchronizationContext` was current at construction | On that context, through `Post`. |
| `ManualDispatcher`, the default when `SynchronizationContext.Current` is null at construction | When the owning thread calls `graph.Pump ()`. |
| `ImmediateDispatcher` | On the posting thread. Under `Guarded` that drain raises `Pump ran on thread` there, and the work stays queued. |

[The async graph on .NET](async-graph.md#threads-and-dispatch) covers the dispatcher in detail.

### A write from another thread during a flush

A direct write from another thread raises on that thread, and the running flush completes unaffected.
`Dispatch`, `Settle` and `Fail` return at once, while the owning thread is still inside the flush. Their work
waits in the inbox and applies at the next drain on the owning thread.

### Awaiting dispatched work

`graph.DispatchAsync (work)` returns a `Task<'T>` for result-producing work, for a caller outside the graph that
awaits the outcome. `work` runs where `Dispatch` work runs. A
[sample](https://github.com/ranvier-org/Ranvier/blob/master/samples/dispatch-async.fsx) awaits it from the
thread pool against a graph owned by a dedicated thread.

| Case | Task |
| --- | --- |
| `work` returns | Completes with the result. |
| `work` throws | Faults with that exception object; `await` rethrows it. `graph.Root.Errors` stays unchanged, and the rest of the inbox still runs. |
| Called on the owning thread | `work` runs before the call returns, and the task is already complete or faulted. |
| Called from another thread | `work` joins the inbox behind earlier `Dispatch`, `DispatchAsync`, settle and flight items, and runs in arrival order. Under `ManualDispatcher` the task completes at the owner's next `Pump ()`. |
| Graph disposed at the call | Faults with `ObjectDisposedException`; `work` does not run and nothing is queued. |
| Graph disposed while the item waits | Faults with `ObjectDisposedException` during `Dispose`; a later drain skips `work`. |
| `cancellationToken` cancelled at the call | Cancelled; nothing is queued. |
| `cancellationToken` cancelled while the item waits | Cancelled at once; the drain skips `work`. |
| `cancellationToken` cancelled after `work` starts | Completes with `work`'s outcome. |

- **Effects.** Completion marks the end of `work`, not of what it triggers. Effects woken by its writes run as
  they do for `Dispatch` work: at the write, or at the end of a batch `work` opens, before the task completes.
  An inline call inside an enclosing batch completes its task first, and that batch's flush runs the effects.
  Async memos `work` starts settle later.
- **Continuations.** On .NET a queued item's continuations run on the thread pool, outside the drain and outside a
  `Serialised` graph. An `await` or an `ExecuteSynchronously` continuation on an inline call's task resumes at
  once on the graph thread, inside the graph.
- **Blocking.** Waiting on the task's `Result` from the owning thread deadlocks when that thread is the only one
  that drains the inbox.
- **Fable.** `work` runs inline, and the task is a promise settled before the call returns.

### Internal locks

User code runs outside every internal lock, with one exception: a stream memo's `fold`. This covers memo,
effect and boundary bodies, cleanups, equality comparers, `fallback` and `recover`, a stream's `start`,
dispatched work and `IGraphDispatcher.Post`.

- The inbox is a lock-free queue, so `Dispatch`, `Settle` and `Fail` return at once.
- Each async memo holds one lock, around its published value and its `Previous.Settled` waiters. The tasks
  it completes resume their awaiters asynchronously.
- Each running stream holds one lock around its state. `fold` runs inside it for an item delivered off the
  graph thread, and without it on the graph thread.
- A build with tracing compiled in adds a lock around the trace log's bookkeeping.

### Serialised hosts

A Blazor Server circuit runs its work one item at a time on its synchronisation context, and each item may
run on a different pool thread. A graph for such a host is constructed with
`GraphOptions.Default.WithThreadAffinity Serialised`, on that context:

- **Entry.** Each operation in the table above enters the graph for its duration. The calling thread must
  have the construction context installed, and no other thread may be inside the graph. Either failure raises
  `InvalidOperationException` before the graph changes. Nested entries on the thread inside the graph proceed.
- **Queued work.** `Dispatch`, `Settle`, `Fail` and flight completions run inline only on the thread inside
  the graph. From any other thread they wait in the inbox for a drain on the context, so settles run
  serialised with the host's other work.
- **Held graph.** A drain that finds another thread inside the graph leaves the work queued. The thread
  inside posts a new drain when it leaves.
- **Node creation** outside an enclosing entry checks the caller and does not hold the graph. Inside
  `createRoot`, a body or a batch, the enclosing entry holds it.
- **`IsOnGraphThread`** is true on the thread inside the graph.

The context check catches a call from the wrong context. The concurrent-entry check catches the
[aspnetcore#69323](https://github.com/dotnet/aspnetcore/issues/69323) case, in which two threads run on the
same context at once. Under Fable, `Serialised` behaves as `Unchecked`.
[Blazor Server](../guide/blazor-server.md) shows one graph per circuit.

## Error recovery

*Implemented.*

A failure is a state of a node, and dependencies read during the failed run remain tracked. A subsequent
input change can trigger recovery. System.Reactive's `OnError` terminates a subscription, but operators
such as `Catch` and `Retry` can provide recovery; R3's `OnErrorResume` is nonterminal. See the
[ecosystem comparison](ecosystem.md).

### What a reader sees

A node that has failed reports it on every read:

| Read | Result |
| --- | --- |
| `.Value` | Raises the stored exception. |
| `TryValue` | `Failed ex`. |
| `Status` | Has the `Error` flag. |
| `Peek` | The last value published before the failure. |

A memo that reads a failed node fails with the same exception instance. The instance passes unchanged through
every intermediate memo to the nearest error boundary, which shows `recover ex last` and exposes the
exception as `Caught`. A suspense boundary passes failures through to its readers.

### Recovery

Recovery happens through re-runs alone.

- A re-read of a failed node serves the stored failure and leaves the body un-run.
- A change to a source that the failed run read re-runs the body. A successful run clears the error at every
  level, boundaries included.
- A failed `AsyncSource` recovers at its next `Settle`.
- A failed effect re-runs when a source it read changes, and a successful run clears its `Status` and `Error`.

### Where an exception lands

| Thrown by | Result |
| --- | --- |
| A memo body | The memo fails. |
| An async body, or its task faulting or being cancelled | The async value fails. Under `CancelPrevious` and `KeepLatest`, a superseded flight's outcome is discarded. Under `FinishCurrent`, a failure that settles while a trailing run is owed is discarded. Under `DropWhilePending`, a failure that settles after the flight discarded a change is discarded. |
| A stream's source faulting, its `fold` or `start` | The stream memo fails, and the stream is cancelled. A `createStream` stream that completes without an item fails with `InvalidOperationException`. |
| An effect body | Recorded on the effect's `Status` and `Error`. Every effect queued behind it in the flush still runs. |
| A boundary's `fallback` or `recover` | The boundary fails with that exception. |
| A cleanup | Recorded, and the remaining cleanups and disposals still run. A `createRoot` scope keeps the error in its own `Errors`; a computation's scope records it in `graph.Root.Errors`. |
| Work drained from the inbox | Recorded in `graph.Root.Errors`, and the rest of the inbox still runs. |
| `DispatchAsync` work | Faults the returned task with that exception. `graph.Root.Errors` stays unchanged, and the rest of the inbox still runs. |
| A signal's equality comparer | The write raises to the writer. The value stays unchanged, and readers stay asleep. |
| The equality comparer of a memo, boundary, projection row, fold or lookup cell | The node fails with that exception and keeps its previous value, and its readers wake to see the failure. A boundary's `recover` does not see it. |
| A `createEffectOn` comparer | Recorded on the effect as a failure of `compute`, and `act` does not run. |
| Disposal of a pending async value | The async value fails with `ObjectDisposedException`, and its readers wake once to see it. |
| A flight's task faulting after it was superseded, or after its async value or graph was disposed | Observed under every `FlightPolicy`. `TaskScheduler.UnobservedTaskException` does not receive it. |

`createEffect` returns `unit`, so an effect's failure is visible through tracing or through an `Effect`
constructed directly. See
[Troubleshooting](../guide/troubleshooting.md#an-exception-inside-an-effect-disappears).

### Finding where a failure came from

A failed node reports the node the failure originated in as `ErrorOrigin`, and an error boundary reports the origin
of `Caught` as `CaughtFrom`. Both are null while the node holds no failure. `ErrorOrigin` is an untracked read, like
`Status`; `CaughtFrom` is tracked and brings the boundary current, like `Caught`.

| Node | `ErrorOrigin` |
| --- | --- |
| `Memo`, `Boundary`, `Effect` | The node itself when its body, comparer or purity check raised the exception; otherwise the origin of the failed read it rethrew. |
| `AsyncMemo` | The async memo for a faulted or cancelled flight, a throwing body or disposal while pending; the origin of a failed read rethrown before the first `await` that suspends. A failure read after that `await` can report the async memo instead. |
| `AsyncSource` | The source, after `Fail`. |
| `StreamMemo` | The stream memo for a faulted or cancelled stream, a throwing `fold` or `start`, an empty `createStream` completion or disposal while pending; the origin of a failed read rethrown by `start`. |
| `Projection` | The projection when its source, `keyOf` or a duplicate key failed the pass; otherwise the origin of the failed read the source rethrew. |

A reader that rethrows a failed read keeps its origin, through any number of memos, effects, boundaries and
projections. A boundary's `recover` that rethrows `ex` keeps the origin too. A node that throws a new exception,
including one that wraps the failure as its `InnerException`, is the origin of the new exception.

Rows, lookup cells and fold rows are internal nodes. A failed projection row reports its projection, and a fold over
it reports the row's origin. A lookup is not a node: a failure raised by its `source` or `affected` function reports
the lookup's internal source memo, and one raised by its key function or comparer reports the key's internal cell. In
[traces](../guide/tracing.md) the cells appear as parts of the source memo.

Two limits apply. A body that catches a failed read, reads another failed node and then rethrows the first
exception is the origin of that exception. A failure delivered to a `Queue` flight's turn behind earlier flights
reports the async memo.

`.Value` rethrows the stored exception with its original stack trace. On .NET every reader on the path rethrows the
same capture, so the trace shows the frames of the origin's throw followed by the frames of the last read. Under
Fable the exception is rethrown as it was raised.

Other tools for tracing a failure back:

- a boundary's `Caught` and every failed node on the path hold the same instance
- `PendingSources` on a memo or effect gives the source of its last pending read
- in a build with [tracing](../guide/tracing.md), `Trace.why` gives the writes and settles behind a node's last
  run, and `Trace.origin` gives the site that created the node

## Ownership

*Implemented.*

Every node other than a signal or an async source attaches, when it is constructed, to the **current
owner**:

| Created | Current owner |
| --- | --- |
| At the top level | `graph.Root` |
| Inside `createRoot body` | The new scope |
| Inside `runWithOwner owner body` | `owner` |
| Inside the body of an effect, `createMemoWith`, boundary or `createEffectOn`'s `act` | The current run of that computation |
| Inside the body of `createAsyncWith` or `createOwningAsyncWithPolicy`, before its first `await` that suspends | The current flight |
| Inside a projection's factory | The key's scope |
| Inside a projection's `source` or `keyOf`, or a lookup's `source` | The current pass |

:::info
**A node belongs to the run of the computation whose body created it.** The next run of that body disposes
it, and so does the disposal of the computation. Ownership is independent of the reader that caused the
run: an owning memo first read by an effect keeps its nodes when the effect re-runs.
:::

### What each API returns, and when it is disposed

| API | Owner | Disposed |
| --- | --- | --- |
| `createSignal`, `createAsyncSource` | Unowned | Collected like any object once unreferenced. A reader drops its edge when it is disposed, or re-runs without reading the source. |
| `createMemo`, `createMemoWith`, `createAsync`, `createAsyncWith`, `createAsyncWithPolicy`, `createOwningAsyncWithPolicy`, `createOptionMemo` | Current owner | With the owner, or by `Dispose`. |
| `createEffect`, `createEffectOn` | Current owner | With the owner. Construct `Effect` directly for a handle. |
| `createStream`, `createStreamFold` | Current owner | With the owner, or by `Dispose`, cancelling the stream. |
| `bindSignal`, `bindMemo` | Current owner | With the owner. The source is the only dependency; `write` runs untracked in the flush and must not create reactive nodes. |
| `createSuspense`, `createErrorBoundary`, `createBoundary` | Current owner | With the owner, or by `Dispose`. |
| `createProjection`, `createProjectionWith`, `createProjectionWithOptions`, `createProjectionOnce`, the index forms, and the views built on a projection | Current owner | With the owner, or by `Dispose`. Each key's scope is disposed when the key leaves. |
| `debounce`, `throttleFirst`, `throttleLast`, `throttle`, and their `With` forms | Current owner | With the owner, or by `Dispose`, cancelling timing and detaching dependencies. |
| `createKeyedCollection` | Current owner | With the owner, or by `Dispose`. Its row nodes are retired when their key leaves. |
| `NewKeyReader`, `NewValueReader` | Current owner; disposable reader handle | By owner teardown or `Dispose`; readers also stop following a disposed projection. Value readers release shared row observation when the last reader detaches. |
| `AsObservableCollection ()` | Current owner | The collection stops following when the owner re-runs or is disposed, or when the projection is disposed. |
| `createLookup`, `createSelector` | Current owner | With the owner, or by `Dispose`. |
| `createRoot` | Current owner | With the owner, or by `owner.Dispose ()`. |
| `onCleanup f` | Current owner | `f` runs when the owner's run ends or the owner is disposed. |
| `Journal.key`, `Journal.keySource`, `Journal.messages`, `Mvu.journal` | Current owner | The key is freed when the owner's run ends or the owner is disposed. On a graph with a journal, they raise `InvalidOperationException` in a pure body, as `onCleanup` does. |
| `new Owner ()`, `new Graph ()` | The caller | By `Dispose`. `graph.Dispose ()` disposes `graph.Root`. |

### Nodes created inside a computation

| Created in | Result |
| --- | --- |
| A `createMemo` body | The run fails with `InvalidOperationException`: *A memo created by createMemo created an owned node in its body*. The run fails even when the body catches the exception, and `untrack` blocks are included. Use `createMemoWith`. |
| A `createMemoWith` body | Owned by the run. Disposed before the next run and with the memo. |
| An effect or boundary body | Owned by the run. Disposed before the next run and with the effect or boundary. |
| A `createAsync` or `createAsyncWithPolicy` body | The run fails, and the message names `createAsyncWith`. |
| A `createAsyncWith` or `createOwningAsyncWithPolicy` body, before the first `await` that suspends | Owned by the flight. Disposed before the next flight starts and with the async value. |
| An async body, after that `await` | On the owning thread, owned by `graph.Root`. To keep it in the flight, capture `getOwner ()` before the `await` and create it inside `runWithOwner`. |
| A projection factory | Owned by the key. Disposed when the key leaves or the projection is disposed. |
| A projection row reader, or `createProjection`'s `map` | The row fails with `InvalidOperationException`. Create the node in the factory. |
| A lookup's `f` or `affected` | The key, or every live key, fails with `InvalidOperationException`. |
| A cleanup | Owned by the scope running the cleanup. Disposed before that computation's next run, or at once when the scope itself is being disposed. |

A signal or async source created in any body is unowned, and each run creates a new one that starts from its
initial value. State that has to survive a re-run belongs outside the body that re-runs.

An async value created and read in the same owning body restarts its flight on every settle. See
[Troubleshooting](../guide/troubleshooting.md#a-boundary-shows-its-fallback-forever-and-starts-a-flight-on-every-settle).

### Teardown order

Disposing a scope, or ending a computation's run, tears down its cleanups and children together in reverse
acquisition order, as `use` bindings are disposed. A cleanup runs while every node created before its
registration is still live. Each child scope is torn down the same way. Cleanups run untracked. Disposal is
idempotent.

- A disposed memo keeps its last value and stops recomputing.
- Disposing an async value cancels its flight's token, and disposing a stream memo cancels its stream.
- A pending async value or stream memo that is disposed fails with `ObjectDisposedException`.

### Deterministic lifetimes

Every edge and every ownership link in the graph is a strong reference. A node's lifetime ends at a defined
point: the end of its owner's run, or a `Dispose` call. The graph behaves identically whenever the garbage
collector runs. A build with tracing holds weak references in its trace log, which affects diagnostics
only.

[Roots and owners](../guide/roots.md) and
[Pure and owning memos](../guide/memos.md#pure-and-owning-memos) show these rules in code.
