ranvier
Fine-grained reactive computation for .NET.
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 IsBusy by hand. Glitch-free, owned and inspired by Solid.
A traced build lets people and agents ask why anything ran; a release build compiles the tracing out.
use graph = new Graph ()
use _ = graph.Activate ()
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)
// view: Fallback
price.Settle 4
price.Fail (exn "feed offline")
price.Settle 5using var graph = new Graph();
using var _ = graph.Activate();
var price = AsyncSource<int>();
var total = Memo(() => price.Value * 3);
var view = Boundary(
() => $"Total {total.Value}",
() => "Loading…",
ex => $"Unavailable: {ex.Message}");
// view: Fallback
price.Settle(4);
price.Fail(new Exception("feed offline"));
price.Settle(5);- Fallback
Ready "Loading…"IsWaiting = true - Ready
Ready "Total 12"IsWaiting = false - Recovered
Ready "Unavailable: feed offline"Caught ≠ null - Ready
Ready "Total 15"Caught = null
ad2d84d.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.
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.
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)
]
The same log answers questions a call stack cannot. An untraced build compiles it out, IL for IL.
Trace.why Why did this not run?Trace.whyNot What did each run do?Trace.history What is it waiting on?Trace.waitingOn Where did this node come from?Trace.origin What does the graph look like?Trace.snapshotWhy Ranvier
The parts of reactive state that .NET developers most often rebuild by hand, built into the library.
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 Peek keeps the last value.
A failure is a value that readers see and a boundary recovers from. The next successful run clears it, and every dependency stays live. ErrorOrigin names the node the failure started in.
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.
Glitch-free diamondsA 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.
Built for C# and XAMLReactiveBindings raises PropertyChanged for derived properties with no dependency attributes. ReactiveCommand derives CanExecute and IsRunning from the graph. C# callers need no F# option types.
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 ObservableCollection receives individual adds, removes and moves instead of a reset.
A graph checks that it is called from its own thread, and the contract lists which calls may come from other threads. Serialised mode accepts a Blazor Server circuit's changing threads and raises on genuine concurrency.
ManualDispatcher and Settle let a test decide when each flight lands. Loading, failure and cancellation are tested with no timers, sleeps or polling.
One core for net10.0, net8.0 and netstandard2.1, with no UI-framework dependency. The untraced build is Native AOT and trim clean, and the library compiles to JavaScript with Fable.
Ranvier.Elmish keeps an existing init and update and reads the model through selector memos. Editable values cover forms seeded from upstream data.