# Ranvier

> Fine-grained reactive computation for .NET.

<section class="rv-hero">
<div class="rv-hero__copy">
<span class="rv-hero__status"><span class="rv-badge">Preview</span> APIs may change.</span>
<h1 class="rv-hero__title">ranvier</h1>
<p class="rv-hero__line">Fine-grained reactive computation for .NET.</p>
<p class="rv-hero__sub">Signals, memos and effects where loading and failure are part of the graph. An async value's pending and failed states reach every value derived from it and stop at a boundary, so no view model tracks <code>IsBusy</code> by hand. Glitch-free, owned and inspired by <a href="https://www.solidjs.com/blog/solid-2-0-rc-the-big-reveal">Solid</a>.<br/>A traced build lets people and <b>agents</b> ask why anything ran; a release build compiles the tracing out.</p>
<div class="rv-hero__actions">
<a class="rv-btn rv-btn--primary" href="/Ranvier/guide/getting-started/">Get started <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></a>
<a class="rv-btn rv-btn--secondary" href="/Ranvier/guide/async-and-pending/">Async and pending</a>
</div>
</div>
<figure class="rv-demo" data-rv-demo aria-label="A boundary moving through its states as its source settles, fails and settles again">
<div class="rv-demo__code">
<div class="rv-demo__tabs" role="tablist" aria-label="Language">
<button type="button" role="tab" id="rv-demo-tab-csharp" aria-controls="rv-demo-csharp" aria-selected="true" data-lang="csharp">C#</button>
<button type="button" role="tab" id="rv-demo-tab-fsharp" aria-controls="rv-demo-fsharp" aria-selected="false" tabindex="-1" data-lang="fsharp">F#</button>
</div>
<pre id="rv-demo-fsharp" role="tabpanel" aria-labelledby="rv-demo-tab-fsharp" data-lang="fsharp" hidden><code><span class="k">use</span> graph = <span class="k">new</span> Graph ()
<span class="k">use</span> _ = graph.Activate ()
<span></span>
<span class="k">let</span> price = createAsyncSource&lt;<span class="t">int</span>&gt; ()
<span class="k">let</span> total = createMemo (<span class="k">fun</span> _ -&gt; price.Value * <span class="n">3</span>)
<span class="k">let</span> view =
    createBoundary
        (<span class="k">fun</span> _ -&gt; <span class="s">"Loading…"</span>)
        (<span class="k">fun</span> ex _ -&gt; <span class="s">"Unavailable: "</span> + ex.Message)
        (<span class="k">fun</span> () -&gt; sprintf <span class="s">"Total %d"</span> total.Value)
<span></span>
<span class="rv-demo__step" data-step="0"><span class="c">// view: Fallback</span></span>
<span class="rv-demo__step" data-step="1">price.Settle <span class="n">4</span></span>
<span class="rv-demo__step" data-step="2">price.Fail (exn <span class="s">"feed offline"</span>)</span>
<span class="rv-demo__step" data-step="3">price.Settle <span class="n">5</span></span></code></pre>
<pre id="rv-demo-csharp" role="tabpanel" aria-labelledby="rv-demo-tab-csharp" data-lang="csharp"><code><span class="k">using var</span> graph = <span class="k">new</span> Graph();
<span class="k">using var</span> _ = graph.Activate();
<span></span>
<span class="k">var</span> price = AsyncSource&lt;<span class="t">int</span>&gt;();
<span class="k">var</span> total = Memo(() =&gt; price.Value * <span class="n">3</span>);
<span class="k">var</span> view = Boundary(
    () =&gt; <span class="s">$"Total {total.Value}"</span>,
    () =&gt; <span class="s">"Loading…"</span>,
    ex =&gt; <span class="s">$"Unavailable: {ex.Message}"</span>);
<span></span>
<span class="rv-demo__step" data-step="0"><span class="c">// view: Fallback</span></span>
<span class="rv-demo__step" data-step="1">price.Settle(<span class="n">4</span>);</span>
<span class="rv-demo__step" data-step="2">price.Fail(<span class="k">new</span> Exception(<span class="s">"feed offline"</span>));</span>
<span class="rv-demo__step" data-step="3">price.Settle(<span class="n">5</span>);</span></code></pre>
</div>
<div class="rv-demo__out" aria-live="polite">
<div class="rv-demo__label">view.TryValue</div>
<ol class="rv-demo__frames">
<li data-step="0" class="is-active"><span class="rv-state rv-state--fallback">Fallback</span><code>Ready "Loading…"</code><small>IsWaiting = true</small></li>
<li data-step="1"><span class="rv-state rv-state--ready">Ready</span><code>Ready "Total 12"</code><small>IsWaiting = false</small></li>
<li data-step="2"><span class="rv-state rv-state--recovered">Recovered</span><code>Ready "Unavailable: feed offline"</code><small>Caught ≠ null</small></li>
<li data-step="3"><span class="rv-state rv-state--ready">Ready</span><code>Ready "Total 15"</code><small>Caught = null</small></li>
</ol>
</div>
<figcaption>Output from running this code against Ranvier <code>ad2d84d</code>.</figcaption>
</figure>
</section>

## States you can name

Every reader observes one of six states. The docs, the marks and the API use the same names. Hover a state to hold the map in it.







































































































































































































































<div class="partas-solid partas-solid--inline" data-partas-page="p4e66840c50" data-partas-cell="c2"></div>


## Watch the graph think

A traced build records every write, mark, run and flight, with the source line that caused it. This is the example from the top of the page, running on the real engine compiled to JavaScript with tracing on. Press a button and follow the event along the edges; hover a node for its state, click it for why it last ran. [How to read a map](guide/signal-maps.md#reading-a-map).

<div class="partas-solid-card partas-solid-card--map">

```fsharp
let price = createAsyncSource<int> ()
let total = createMemo (fun _ -> price.Value * 3)

let view =
    createBoundary
        (fun _ -> "Loading…")
        (fun ex _ -> "Unavailable: " + ex.Message)
        (fun () -> sprintf "Total %d" total.Value)

createEffect (fun () -> printfn "%s" view.Value)

let offline = exn "feed offline"

controls [
    button "Settle 4" (fun () -> price.Settle 4)
    button "Fail" (fun () -> price.Fail offline)
    button "Settle 5" (fun () -> price.Settle 5)
]
```

<div class="partas-solid" data-partas-page="p4e66840c50" data-partas-cell="c3"></div>

</div>

<p class="rv-map-edit"><a href="/Ranvier/guide/signal-maps/#edit-a-map">Edit this map in your browser</a></p>

<div class="rv-trace">
<p class="rv-trace__lead">The same log answers questions a call stack cannot. An untraced build compiles it out, IL for IL.</p>
<div class="rv-trace__grid">
<a class="rv-trace__q" href="/Ranvier/guide/tracing/#why-did-it-run"><span>Why did this run?</span><code>Trace.why</code></a>
<a class="rv-trace__q" href="/Ranvier/guide/tracing/#why-did-it-not-run"><span>Why did this not run?</span><code>Trace.whyNot</code></a>
<a class="rv-trace__q" href="/Ranvier/guide/tracing/#what-did-each-run-do"><span>What did each run do?</span><code>Trace.history</code></a>
<a class="rv-trace__q" href="/Ranvier/guide/tracing/#what-is-it-waiting-on"><span>What is it waiting on?</span><code>Trace.waitingOn</code></a>
<a class="rv-trace__q" href="/Ranvier/guide/tracing/#where-did-it-come-from"><span>Where did this node come from?</span><code>Trace.origin</code></a>
<a class="rv-trace__q" href="/Ranvier/guide/tracing/#what-the-graph-looks-like"><span>What does the graph look like?</span><code>Trace.snapshot</code></a>
</div>
<a class="rv-trace__more" href="/Ranvier/guide/tracing/">Read the tracing guide <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></a>
</div>

## Why Ranvier

The parts of reactive state that .NET developers most often rebuild by hand, built into the library.

<div class="rv-cards">
<a class="rv-card" href="/Ranvier/guide/pending/"><strong class="rv-card__title">Loading and errors, derived</strong><p>Pending and failure travel on their own channel. They pass from an async source through every memo that reads it to the nearest boundary, which shows a fallback or a recovered value. A flight policy decides what happens to superseded work: cancel it, keep only the latest, queue it, or finish it and run once more. During a refresh <code>Peek</code> keeps the last value.</p></a>
<a class="rv-card" href="/Ranvier/concepts/contracts/#error-recovery"><strong class="rv-card__title">Errors are state, not the end of the stream</strong><p>A failure is a value that readers see and a boundary recovers from. The next successful run clears it, and every dependency stays live. <code>ErrorOrigin</code> names the node the failure started in.</p></a>
<a class="rv-card" href="/Ranvier/concepts/contracts/#ownership"><strong class="rv-card__title">Owners instead of leaks</strong><p>Every memo, effect and projection row belongs to an owner. Disposing the owner disposes them, in a fixed order. Lifetimes are deterministic and never depend on the garbage collector or on a forgotten unsubscribe.</p></a>
<a class="rv-card" href="/Ranvier/concepts/ecosystem/#diamonds-without-glitches"><strong class="rv-card__title">Glitch-free diamonds</strong><p>A value that reads two paths from one source runs once per write, and both paths it reads come from that write. It never sees one path updated and the other stale.</p></a>
<a class="rv-card" href="/Ranvier/guide/csharp/#binding-to-xaml"><strong class="rv-card__title">Built for C# and XAML</strong><p><code>ReactiveBindings</code> raises <code>PropertyChanged</code> for derived properties with no dependency attributes. <code>ReactiveCommand</code> derives <code>CanExecute</code> and <code>IsRunning</code> from the graph. C# callers need no F# option types.</p></a>
<a class="rv-card" href="/Ranvier/guide/projections/#reading-changes"><strong class="rv-card__title">Incremental collections</strong><p>Keyed projections give each row its own reactive value. A key reader reports only the keys added, removed or replaced since its last read. A bound <code>ObservableCollection</code> receives individual adds, removes and moves instead of a reset.</p></a>
<a class="rv-card" href="/Ranvier/concepts/contracts/#threading"><strong class="rv-card__title">A written threading contract</strong><p>A graph checks that it is called from its own thread, and the contract lists which calls may come from other threads. <code>Serialised</code> mode accepts a Blazor Server circuit's changing threads and raises on genuine concurrency.</p></a>
<a class="rv-card" href="/Ranvier/guide/testing/"><strong class="rv-card__title">Deterministic async tests</strong><p><code>ManualDispatcher</code> and <code>Settle</code> let a test decide when each flight lands. Loading, failure and cancellation are tested with no timers, sleeps or polling.</p></a>
<a class="rv-card" href="/Ranvier/guide/installation/#native-aot-and-trimming"><strong class="rv-card__title">Portable, with no platform package</strong><p>One core for <code>net10.0</code>, <code>net8.0</code> and <code>netstandard2.1</code>, with no UI-framework dependency. The untraced build is Native AOT and trim clean, and the library compiles to JavaScript with Fable.</p></a>
<a class="rv-card" href="/Ranvier/guide/elmish/"><strong class="rv-card__title">Adopt it one view at a time</strong><p><code>Ranvier.Elmish</code> keeps an existing <code>init</code> and <code>update</code> and reads the model through selector memos. Editable values cover forms seeded from upstream data.</p></a>
</div>

<script>
(() => {
  const demo = document.querySelector("[data-rv-demo]");
  if (!demo) return;
  const frames = demo.querySelectorAll(".rv-demo__frames > li");
  const steps = demo.querySelectorAll(".rv-demo__step");
  const tabs = [...demo.querySelectorAll("[role=tab]")];
  const panels = demo.querySelectorAll("[role=tabpanel]");
  let step = 0, lang = 0, timer = 0;
  const show = (n) => {
    step = n;
    for (const f of frames) f.classList.toggle("is-active", +f.dataset.step === n);
    for (const s of steps) s.classList.toggle("is-active", +s.dataset.step === n);
  };
  const pick = (n, focus) => {
    lang = n;
    tabs.forEach((t, i) => {
      t.setAttribute("aria-selected", String(i === n));
      t.tabIndex = i === n ? 0 : -1;
      if (i === n && focus) t.focus();
    });
    for (const p of panels) p.hidden = p.dataset.lang !== tabs[n].dataset.lang;
  };
  // Each wrap back to the first step also moves to the next language.
  const tick = () => {
    const next = (step + 1) % frames.length;
    if (next === 0) pick((lang + 1) % tabs.length);
    show(next);
  };
  const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
  const start = () => { if (!reduce && !timer) timer = setInterval(tick, 2400); };
  const stop = () => { clearInterval(timer); timer = 0; };
  for (const el of [...frames, ...steps]) {
    el.addEventListener("mouseenter", () => show(+el.dataset.step));
  }
  demo.addEventListener("mouseenter", stop);
  demo.addEventListener("focusin", stop);
  demo.addEventListener("mouseleave", () => { if (!demo.contains(document.activeElement)) start(); });
  demo.addEventListener("focusout", (e) => { if (!demo.contains(e.relatedTarget) && !demo.matches(":hover")) start(); });
  tabs.forEach((t, i) => {
    t.addEventListener("click", () => pick(i));
    t.addEventListener("keydown", (e) => {
      const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
      if (d) pick((i + d + tabs.length) % tabs.length, true);
    });
  });
  pick(0);
  show(0);
  start();
})();
</script>
