Skip to content

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!

License

Notifications You must be signed in to change notification settings

bajmming/Stl.Fusion

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ‘Ύ Fusion: the "real-time on!" switch that actually exists

Build Coverage NuGet Version MIT License
Discord Server Commit Activity Downloads

Fusion is a .NET library that implements Distributed REActive Memoization (DREAM) – a novel technique that gracefully solves a number of well-known problems:

Problem So you don't need...
πŸ“± Client-side state management Fluxor, Redux, MobX, Recoil, ...
πŸš€ Real-time updates SignalR, WebSockets, gRPC streaming, ...
πŸ“‡ In-memory cache Redis, memcached, ...
🀹 Real-time cache invalidation No good solutions -
it's an infamously hard problem
πŸ“ͺ Automatic & transparent pub/sub A fair amount of code
🀬 Network chattiness A fair amount of code
πŸ’° Single codebase for Blazor WebAssembly, Server, and Hybrid No good alternatives

So what DREAM means?

  • Memoization is a way to speed up function calls by caching their output for a given input. Fusion provides a transparent memoization for any function, so when you call GetUser(id) multiple times, its actual computation happens just once for every id assuming there is enough RAM to cache every result.
  • Reactive part of your Fusion-based code reacts to changes by triggering invalidations. Invalidation is a call to memoizing function inside a special using (Computed.Invalidate()) { ... } block, which marks the cached result for some specific call (e.g. GetUser(3)) as inconsistent with the ground truth, which guarantees it will be recomputed on the next actual (non-invalidating) call. Suprisingly, invalidation is cascading: if GetUserPic("[email protected]") calls GetUser(3), its result will be invalidated as well in this case, and the same will happen with every other computed result that depends on GetUser(3) directly or indirectly. This means Fusion tracks dependencies between cached computation results; the dependency graph is built and updated in the runtime, and this process is completely transparent for the developers.
  • The dependency graph can be Distributed: Fusion allows you to create invalidation-aware caching RPC clients for any of such functions.
    • They eliminate network chattiness by re-using locally cached results while it's known they aren't invalidated on the server side yet. The pub/sub, delivery, and processing of invalidation messages happens automatically and transparently for you.
    • Moreover, such clients register their results in Fusion's dependency graph like any other Fusion functions, so if you client-side code declares GetUserName(id) => server.GetUser(id).Name function, GetUserName(id) result will be invalidated once GetUser(id) gets invalidated on the server side. And that's what powers all real-time UI updates on the client side in Fusion samples.

Lot traceability is probably the best real-world analogy of how this approach works:

  • For every "product" πŸ₯— (computed value), Fusion keeps track of its "recipe" πŸ“ (function and its arguments), but more importantly, all of its "ingredients" πŸ₯¬πŸ₯¦πŸ…, i.e. intermediate or "basic" products used to produce it.
    E.g. πŸ₯—v1 = πŸ“salad("weird_mix") + (πŸ₯¬v1 πŸ₯¦v1 πŸ…v1)
  • While all the "ingredients" used to produce πŸ₯—v1 are "valid", Fusion ensures that calling a recipe πŸ“salad("weird_mix") resolves to the same cached product instance πŸ₯—v1
  • But once one of such ingredients πŸ…v1 gets "contaminated" ("invalidated" in Fusion terms, i.e. marked as changed), Fusion immediately marks everything that uses this product directly or indirectly as "contaminated" as well, including πŸ₯—v1
  • So next time you call πŸ“salad("weird_mix"), it will produce a new πŸ₯—v2 = πŸ“salad("weird_mix") + (πŸ₯¬v1 πŸ₯¦v1 πŸ…v2)

Lot traceability allows to identify every product that uses certain ingredient, and consequently, even every buyer of a product that has certain ingredient. So if you want every consumer to have the most up-to-date version of every product they bought – the most up-to-date πŸš—, 🀳, or πŸ‘  – lot traceability makes this possible. And assuming every purchase order triggers the whole build chain and uses the most recent ingredients, merely notifying the consumers they can buy a newer version of their πŸ“± is enough. It's up to them to decide when to update - they can do this immediately or postpone this till the next πŸ’°, but the important piece is: they are aware the product they have is obsolete now.

We know all of this sounds weird. 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!

If you prefer slides and πŸ•΅ detective stories, check out "Jump the F5 ship straight into the real-time hyperspace with Blazor and Fusion" talk - it explains how all these problems are connected and describes how you can code a simplified version of Fusion's key abstraction in C#.

And if you prefer text - just continue reading!

"What is your evidence?"*

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

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 piece of shared state there, including sign-in state:

If you're looking for more complex example, check out Board Games - it's the newest Fusion sample that runs on 3-node GKS cluster and implements 2 games, chat, online presence, OAuth sign-in, user session tracking and a number of other 100% real-time features. All of this is powered by Fusion + just 35 lines of code related to real-time updates!

A small benchmark in Fusion test suite compares "raw" Entity Framework Core-based Data Access Layer (DAL) against its version relying on Fusion:

The speedup you see is:

Fusion's transparent caching ensures every computation your code runs re-uses as many of cached dependencies as possible & caches its own output. As you can see, this feature allows to speed up even a very basic logic (fetching a single random user) using in-memory EF Core provider by 1000x, and the more complex logic you have, the larger performance gain is.

How Fusion works?

There are 4 components:

  1. Compute Services are services exposing methods "backed" by Fusion's version of "computed observables". When such methods run, they produce Computed Values (instances of IComputed<T>) under the hood, even though the results they return are usual (i.e. are of their return type). But IComputed<T> instances are cached and reused on future calls to the same method with the same arguments; moreover, they form dependency graphs, so once some "deep" IComputed<T> gets invalidated, all of its dependencies are invalidated too.
  2. Replica Services are remote proxies of Compute Services. They substitute Compute Services they "replicate" on the client side exposing their interface, but more importantly, they also "connect" IComputed<T> instances they create on the client with their server-side counterparts. Any Replica Service is also a Compute Service, so any other client-side Compute Service method that calls it becomes dependent on its output too. And since any Compute Service never runs the same computation twice (unless it is invalidated), they kill any network chattiness.
  3. State - more specifically, IComputedState<T> and IMutableState<T>. States are quite similar to observables in Knockout or MobX, but designed to follow Fusion game rules. And yes, you mostly use them in UI and almost never - on the server-side.
  4. And finally, there is IComputed<T> – an observable Computed Value that's in some ways similar to the one you can find in Knockout, MobX, or Vue.js, but very different, if you look at its fundamental properties.

IComputed<T> is:

  • Thread-safe
  • Asynchronous – any Computed Value is computed asynchronously; Fusion APIs dependent on this feature are also asynchronous.
  • Almost immutable – once created, the only change that may happen to it is transition to IsConsistent() == false state
  • GC-friendly – if you know about Pure Computed Observables from Knockout, you understand the problem. IComputed<T> solves it even better – dependent-dependency relationships are explicit there, and the reference pointing from dependency to dependent is weak, so any dependent Computed Value is available for GC unless it's referenced by something else (i.e. used).

All of this makes it possible to use IComputed<T> on the server side – you don't have to synchronize access to it, you can use it everywhere, including async functions, and you don't need to worry about GC.

Check out how Fusion differs from SignalR – this post takes a real app example (Slack-like chat) and describes what has to be done in both these cases to implement it.

Does Fusion scale?

Yes. Fusion does something similar to what any MMORPG game engine does: even though the complete game state is huge, it's still possible to run the game in real time for 1M+ players, because every player observes a tiny fraction of a complete game state, and thus all you need is to ensure the observed part of the state fits in RAM.

And that's exactly what Fusion does:

  • It spawns the observed part of the state on-demand (i.e. when you call a Compute Service method)
  • Ensures the dependency graph of this part of the state stays in memory
  • Destroys every part of the dependency graph that isn't "used" by one of "observed" components.

Check out "Scaling Fusion Services" part of the Tutorial to see a much more robust description of how Fusion scales.

Enough talk. Show me the code!

Most of Fusion-based code lives in Compute Services. Such services are resolved via DI containers to their Fusion-generated proxies producing Computed Values while they run. Proxies cache and reuse these IComputed<T> instances on future calls to the same method with the same arguments.

A typical Compute Service looks as follows:

public class ExampleService
{
    [ComputeMethod]
    public virtual async Task<string> GetValue(string key)
    { 
        // This method reads the data from non-Fusion "sources",
        // so it requires invalidation on write (see SetValue)
        return await File.ReadAllTextAsync(_prefix + key);
    }

    [ComputeMethod]
    public virtual async Task<string> GetPair(string key1, string key2)
    { 
        // This method uses only other [ComputeMethod]-s or static data,
        // thus it doesn't require invalidation on write
        var v1 = await GetNonFusionData(key1);
        var v2 = await GetNonFusionData(key2);
        return $"{v1}, {v2}";
    }

    public async Task SetValue(string key, string value)
    { 
        // This method changes the data read by GetValue and GetPair,
        // but since GetPair uses GetValue, it will be invalidated 
        // automatically once we invalidate GetValue.
        await File.WriteAllTextAsync(_prefix + key, value);
        using (Computed.Invalidate()) {
            // This is how you invalidate what's changed by this method.
            // Call arguments matter: you invalidate only a result of a 
            // call with matching arguments rather than every GetValue 
            // call result!
            GetValue(key).Ignore(); // Ignore() suppresses "unused result" warning
        }
    }
}

As you might guess:

  • [ComputeMethod] indicates that every time you call this method, its result is "backed" by Computed Value, and thus it captures dependencies (depends on results of any other compute method it calls) and allows other compute methods to depend on its own results. This attribute works only when you register a service as Compute Service in IoC container and the method it is applied to is async and virtual.
  • Computed.Invalidate() call creates a "scope" (IDisposable) which makes every [ComputeMethod] you call inside it to invalidate the result for this call, which triggers synchronous cascading (i.e. recursive) invalidation of every dependency it has, except remote ones (they are invalidated asynchronously).

Compute services are registered ~ almost like singletons:

var services = new ServiceCollection();
var fusion = services.AddFusion(); // It's ok to call it many times
// ~ Like service.AddSingleton<[TService, ]TImplementation>()
fusion.AddComputeService<ExampleService>();

Check out CounterService from HelloBlazorServer sample to see the actual code of compute service.

Note: Most of Fusion Samples use attribute-based service registration, which is just another way of doing the same. So you might need to look for [ComputeService] attribute there to find out which compute services are registered there.

Now, I guess you're curious how the UI code looks like with Fusion. You'll be surprised, but it's as simple as it could be:

// MomentsAgoBadge.razor
@inherits ComputedStateComponent<string>
@inject IFusionTime _fusionTime

<span>@State.Value</span>

@code {
    [Parameter] 
    public DateTime Value { get; set; }

    protected override Task<string> ComputeState()
        => _fusionTime.GetMomentsAgo(Value) ;
}

MomentsAgoBadge is Blazor component displays "N [seconds/minutes/...] ago" string. It is used in a few samples, including Board Games. The code above is almost identical to its actual code, which is a bit more complex due to null handling.

You see it uses IFusionTime - one of built-in compute services that provides GetUtcNow and GetMomentsAgo methods. As you might guess, the results of these methods are invalidated automatically; check out FusionTime service to see how it works.

But what's important here is that MomentsAgoBadge is inherited from ComputedStateComponent - an abstract type which provides ComputeState method. As you might guess, this method is a [Compute Method] too, so captures its dependencies & its result gets invalidated once cascading invalidation from one of its "ingredients" "hits" it.

ComputedStateComponent<T> exposes State property (of ComputedState<T> type), which allows you to get the most recent output of ComputeState()' via its Value property. "State" is another key Fusion abstraction - it implements a "wait for invalidation and recompute" loop similar to this one:

var computed = await Computed.Capture(_ => service.Method(...));
while (true) {
    await computed.WhenInvalidated();
    computed = await computed.Update();
}

The only difference is that it does this in a more robust way - in particular, it allows you to control the delays between the invalidation and the update, access the most recent non-error value, etc.

Finally, ComputedStateComponent automatically calls StateHasChanged() once its State gets updated to make sure the new value is displayed.

So if you use Fusion, you don't need to code any reactions in the UI. Reactions (i.e. partial updates and re-renders) happen automatically due to dependency chains that connect your UI components with the data providers they use, which in turn are connected to data providers they use, and so on - till the very basic "ingredient providers", i.e. compute methods that are invalidated on changes.

If you want to see a few more examples of similarly simple UI components, check out:

Why Fusion is a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client which state might be impacted by this change. Which means you have to:

  1. Know which clients to notify about a particular event. This alone is a fairly hard problem - in particular, you need to know what every client "sees" now. Sending events for anything that's out of the "viewport" (e.g. a post you may see, but don't see right now) doesn't make sense, because it's a huge waste that severely limits the scalability. Similarly to MMORPG, the "visible" part of the state is tiny in comparison to the "available" one for most of web apps too.
  2. Apply events to the client-side state. Kind of an easy problem too, but note that you should do the same on server side as well, and keeping the logic in two completely different handlers in sync for every event is a source of potential problems in future.
  3. Make UI to properly update its event subscriptions on every client-side state change. This is what client-side code has to do to ensure p.1 properly works on server side. And again, this looks like a solvable problem on paper, but things get much more complex if you want to ensure your UI provides a truly eventually consistent view. Just think in which order you'd run "query the initial data" and "subscribe to the subsequent events" actions to see some issues here.
  4. Throttle down the rate of certain events (e.g. "like" events for every popular post). Easy on paper, but more complex if you want to ensure the user sees eventually consistent view on your system. In particular, this implies that every event you send "summarizes" the changes made by it and every event you discard, so likely, you'll need a dedicated type, producer, and handlers for each of such events.

And Fusion solves all these problems using a single abstraction allowing it to identifying and track data dependencies automatically.

Why Fusion is a game changer for Blazor apps with complex UI?

Fusion allows you to create truly independent UI components. You can embed them in any parts of UI you like without any need to worry of how they'll interact with each other.

This makes Fusion a perfect fit for micro-frontends on Blazor: the ability to create loosely coupled UI components is paramount there.

Besides that, if your invalidation logic is correct, Fusion guarantees that your UI state is eventually consistent.

You might think all of this works only in Blazor Server mode. But no, all these UI components work in Blazor WebAssembly mode as well, which is another unique feature Fusion provides. Any Compute Service can be substituted with Replica Service on the client, which not simply proxies the calls, but also completely kills the chattiness you'd expect from a regular client-side proxy. So if you need to support both modes, Fusion is currently the only library solving this problem gracefully.

Replica Service's RPC protocol is actually an extension to regular Web API, which kicks in only when a client submits a special header, but otherwise the endpoint acts as a regular one. So any of such APIs is callable even without Fusion! Try to open this page in one window in and call ​/api​/Sum​/Accumulate and /api/Sum/GetAccumulator on this Swagger page in another window.

Next Steps

Posts And Other Content

P.S. If you've already spent some time learning about Fusion, please help us to make it better by completing Fusion Feedback Form (1…3 min).

About

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!

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 99.8%
  • Other 0.2%