SkillAgentSearch skills...

Stl.Fusion

Build real-time apps (Blazor included) with less than 1% of extra code responsible for real-time updates. Host 10-1000x faster APIs relying on transparent and nearly 100% consistent caching. We call it DREAM, or Distributed REActive Memoization, and it's here to turn real-time on!

Install / Use

/learn @servicetitan/Stl.Fusion

README

👾 Fusion: the "real-time on!" switch that actually exists

Build Coverage NuGet Version MIT License <br/> Discord Server Commit Activity Downloads

Fusion is a .NET library that implements 🦄 Distributed REActive Memoization (DREAM) – a novel abstraction somewhat similar to MobX or Flux, but designed to deal with an arbitrary large state spanning across your backend microservices, API servers, and reaching even every client of your app.

Fusion solves a set of infamously hard problems with a single hammer:

| Problem | So you don't need... | |-|-| | 📇 Caching | Redis, memcached, ... | | 🤹 Real-time cache invalidation | No good solutions - <br/>it's an infamously hard problem | | 🚀 Real-time updates | SignalR, WebSockets, gRPC, ... | | 🤬 Network chattiness | A fair amount of code | | 🔌 Offline mode support | A fair amount of code | | 📱 Client-side state management | MobX, Flux/Redux, Recoil, ... | | 💰 Single codebase for Blazor WebAssembly, Server, and Hybrid/MAUI | No good alternatives |

And the best part is: Fusion does all of that transparently for you, so Fusion-based code is almost identical to a code that doesn't involve it. All you need is to:

  • "Implement" IComputeService (a tagging interface) on your Fusion service to ensure call intercepting proxy is generated for it in compile time.
  • Mark methods requiring "Fusion behavior" with [ComputeMethod] + declare them as virtual
  • Register the service via serviceCollection.AddFusion().AddService<MyService>()
  • Resolve and use them usual - i.e., pass them as dependencies, call their methods, etc.

The magic happens when [ComputeMethod]-s are invoked:

  1. When Fusion knows that a value for a given call (think (serviceInstance, method, args...) cache key) is still consistent, Fusion returns it instantly, without letting the method to run.
  2. And when the value isn't cached or tagged as inconsistent, Fusion lets the method run, but captures new value's dependencies in process. "Dependency" is one [ComputeMethod] call triggered during the evaluation of another [ComputeMethod] call.

The second step allows Fusion to track which values are expected to change when one of them changes. It's quite similar to lot traceability, but implemented for arbitrary functions rather than manufacturing processes.

The last piece of a puzzle is Computed.Invalidate() block allowing to tag cached results as "inconsistent with the ground truth". Here is how you use it:

var avatars = await GetUserAvatars(userId);
using (Computed.Invalidate()) {
    // Any [ComputeMethod] invoked inside this block doesn't run normally,
    // but invalidates the result of the identical call instead.
    // Such calls complete synchronously and return completed Task<TResult>, 
    // so you don't need to await them.

    _ = userService.GetUser(userId);
    foreach (var avatar in avatars)
        _ = userAvatarService.GetAvatar(userId, avatar.Id);
}

The invalidation is always transitive: if GetUserProfile(3) calls GetUserAvatar("3:ava1"), and GetUserAvatar("3:ava1") gets invalidated, GetUserProfile(3) gets invalidated as well.

To make it work, Fusion maintains a dictionary-like structure that tracks recent and "observed" call results:

  • Key: (serviceInstance, method, call arguments...)
  • Value: [Computed<T>], which stores the result, consistency state (Computing, Consistent, Invalidated) and dependent-dependency links. Computed<T> instances are nearly immutable: once constructed, they can only transition to Inconsistent state.

You can "pull" the Computed<T> instance "backing" certain call like this:

var computed1 = await Computed.Capture(() => GetUserProfile(3));
// You can await await for its invalidation:
await computed1.WhenInvalidated();
Assert.IsFalse(computed1.IsConsistent());
// And recompute it:
var computed2 = await computed1.Recompute();

So any Computed<T> is observable. Moreover, it can be a "replica" of a remote Computed<T> instance that mirrors its state in your local process, so the dependency graph can be distributed. To make it work, Fusion uses its own WebSocket-based RPC protocol, which is quite similar to any other RPC protocol:

  1. To "send" the call to a remote peer, client sends "call" message
  2. The peer responds to it with "call result" message. So far there is no difference with any other RPC protocol.
  3. And here is the unique step: the peer may later send a message telling that the call result it sent earlier was invalidated.

Step 3 doesn't change much in terms of network traffic: it's either zero or one extra message per call (i.e. 3 messages instead of 2 in the worst case). But this small addition allows [Compute Service Clients] to know precisely when a given cached call result becomes inconsistent.

The presence of step 3 makes a huge difference: any cached & still consistent result is as good as the data you'll get from the remote server, right? So it's totally fine to resolve a call that "hits" such a result locally, incurring no network round-trip!

Finally, any [Compute Service Client] behaves as a similar local [Compute Service]. Look at this code:

string GetUserName(id)
    => (await userService.GetUser(id)).Name;

You can't tell whether userService here is a local compute service or a compute service client, right?

  • Both options are of the same base type (e.g. IUserService). The implementations are different though: Fusion service client is registered via fusion.AddClient<TInterface>() vs fusion.AddServer<TInterface, TService>() for the server.
  • And behave identically:
    • Every call you make to userService terminates instantly if its previous result is still consistent
    • And if GetUserName is a method of another computed service (a local one), [computed value] backing GetUser(id) call that it makes would automatically extend Fusion's dependency graph for GetUserName(id) call!

So Fusion abstracts away the "placement" of a service, and does it much better than conventional RPC proxies: Fusion proxies aren't "chatty" by default!

Documentation

<img align="right" width="150" src="./docs/img/FusionSlides.jpg"/> If you prefer slides, check out "Why real-time web apps need Blazor and Fusion?" talk - it explains how many problems we tackle are connected, how Fusion addresses the root cause, and how to code a simplified version of Fusion's key abstraction in C#.

The slides are slightly outdated - e.g. now Fusion clients use Stl.Rpc rather than HTTP to communicate with the server, but all the concepts they cover are still intact.

[Quick Start], [Cheat Sheet], and the [Tutorial] are the best places to start from.

Check out [Samples]; some of them are covered further in this document.

"What is your evidence?"<sup><a href="https://www.youtube.com/watch?v=7O-aNYTtx44<">*</a></sup>

All of this sounds way too good to be true, right? That's why there are lots of visual proofs in the remaining part of this document. But if you'll find anything concerning in Fusion's source code or [samples], please feel free to grill us with questions on [Discord]!

Let's start with some big guns:

Check out [Actual Chat] – a very new chat app built by the minds behind Fusion.

Actual Chat fuses real-time audio, live transcription, and AI assistance to let you communicate with utmost efficiency. With clients for WebAssembly, iOS, Android, and Windows, it boasts nearly 100% code sharing across these platforms. Beyond real-time updates, several of its features, like offline mode, are powered by Fusion.

We're posting some code examples from Actual Chat codebase here, so join this chat to learn how we use it in a real app.

Now, the samples:

Below is Fusion+Blazor Sample delivering real-time updates to 3 browser windows:

<img src="https://img.shields.io/badge/-Live!-red" valign="middle"> Play with live version of this sample right now!

The sample supports both Blazor Server and Blazor WebAssembly hosting modes. And even if you use different modes in different windows, Fusion still keeps in sync literally every bit of a shared state there, including the sign-in state:

Is Fusion fast?

Yes, it's incredibly fast. Here is an RPC call duration distribution for one of the most frequent calls on [Actual Chat]:

IChats.GetTile reads a small "chat tile" - typically 5 entries pinned to a specific ID rang

View on GitHub
GitHub Stars1.9k
CategoryDevelopment
Updated2d ago
Forks107

Languages

C#

Security Score

100/100

Audited on Mar 28, 2026

No findings