Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

5,448 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

👾 Fusion: The missing layer for real-time apps

BuildNuGet VersionCommit ActivityDownloads
DocumentationSamplesChat @ VoxtChangelog

Overview

ActualLab.Fusion acts as method-call middleware, transparently enriching every call to Fusion-enhanced services with caching, dependency tracking, invalidation, RPC, and more — with almost no changes to application code.

You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:

  • When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
  • Recomputation only happens when you actually request the result

Fusion solves a set of infamously hard problems with a 🦄 single abstraction:

ProblemFusion's AnswerSo you don't need...
⚡ CachingIn-memory memoization by call argumentsRedis, memcached, ...
🔄 Cache invalidationAutomatic dependency tracking + cascading invalidationManual tracking – an infamously hard problem
🪄 Real-time updatesFusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server sideSignalR, WebSockets, custom pub/sub, ...
✈️ Offline operationClient-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free!Service workers, sync logic, conflict resolution, ...
🤬 Network chattinessPersistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frameRequest batching, debouncing, manual optimization, ...
📡 Network trafficActualLab.Rpc is 2-7x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responsesProtocol tuning, custom serialization, ...
🧩 Client-side state managementSame abstractions everywhere: Computed<T>, MutableState<T>, and compute methodsMobX, Flux/Redux, Recoil, ...
💰 Single codebaseSingle codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more)Platform-specific code, multiple implementations, ...

The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.

Documentation

Documentation is the best place to start from.

If you prefer video, check out:

ActualLab.Fusion Video
ActualLab.Rpc Video

Samples

  1. Clone Fusion Samples repository: git clone git@github.com:ActualLab/Fusion.Samples.git
  2. Follow the instructions from README.md to build and run everything.

Blazor Sample · TodoApp Sample · TownHall — a live audience Q&A app ("Slido-lite") in its own repository · Board Games — a standalone Fusion app (real-time multiplayer board games) in its own repository.

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

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 Voxt.ai:

IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!

The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.

Benchmark Highlights

Our benchmarks show Fusion delivering over 500 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:

ScenarioWithout FusionWith FusionSpeedup
Local DAL, almost no writes (peak performance)38.61K calls/s533.85M calls/s>13,000x
Local repo-like service, non-stop writes171.05K calls/s344.98M calls/s~2,017x
Remote repo-like service, non-stop writes102.82K calls/s (REST)230.16M calls/s~2,239x

These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.

Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.

ActualLab.Rpc: The Fastest RPC Protocol on .NET

ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:

FrameworkCalls/sStreaming
ActualLab.Rpc9.91M99.96M items/s
SignalR4.85M17.97M items/s
gRPC1.28M43.78M items/s

So it's significantly faster than gRPC and SignalR, both for calls and for streaming.

What makes Fusion fast:

  • The concept itself is all about eliminating any unnecessary computation. Think msbuild, but for your method call results: what's computed and consistent is never recomputed.
  • Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
  • Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
  • Fusion uses its own ActualLab.Interception library for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET (our microbenchmarks show them ~5-7x faster per call than Castle DynamicProxy).
  • ActualLab.Rpc uses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.
  • All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.

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 backing this part of the state stays in memory while someone uses it
  • Destroys what's unobserved.

Enough talk. Show me the code!

To use Fusion, you need to:

  1. Reference the ActualLab.Fusion NuGet package
  2. "Implement" IComputeService (a tagging interface) on your service
  3. Mark methods requiring caching/invalidation with [ComputeMethod] and declare them as virtual
  4. Register the service via services.AddFusion().AddService<MyService>()

A typical Compute Service looks as follows:

publicclassExampleService:IComputeService{[ComputeMethod]publicvirtualasyncTask<string>GetValue(stringkey){// This method reads the data from non-Fusion "sources",// so it requires invalidation on write (see SetValue)returnawaitFile.ReadAllTextAsync(_prefix+key);}[ComputeMethod]publicvirtualasyncTask<string>GetPair(stringkey1,stringkey2){// This method uses only other [ComputeMethod]-s or static data,// thus it doesn't require invalidation on writevarv1=awaitGetValue(key1);varv2=awaitGetValue(key2);return$"{v1}, {v2}";}publicasyncTaskSetValue(stringkey,stringvalue){// This method changes the data read by GetValue and GetPair,// but since GetPair uses GetValue, it will be invalidated// automatically once we invalidate GetValue.awaitFile.WriteAllTextAsync(_prefix+key,value);using(Invalidation.Begin()){// 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);}}}

[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.

Compute services are registered similarly to singletons:

varservices=newServiceCollection();varfusion=services.AddFusion();// It's ok to call it many times// ~ Like service.AddSingleton<[TService, ]TImplementation>()fusion.AddService<ExampleService>();

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

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

// MomentsAgoBadge.razor@inheritsComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {[Parameter]
public DateTime Value {get;set;}protectedoverrideTask<string>ComputeState()=>_fusionTime.GetMomentsAgo(Value);}

MomentsAgoBadge is a Blazor component that displays an "N [seconds/minutes/...] ago" string. 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 the 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 behaves like a Compute Method.

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:

varcomputed=awaitComputed.Capture(_ =>service.Method(...));while(true){awaitcomputed.WhenInvalidated();computed=awaitcomputed.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 is Fusion a game changer for real-time apps?

Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so 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. This seems easy too, but note that you should do the same on the 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 the future.
  3. Make the UI 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 the 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 that allows it to identify and track data dependencies automatically.

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

Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about 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 a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.

Next Steps

About

Build real-time Blazor and MAUI apps while writing just 0.1% of the usual real-time update code. Handle 10× more API requests with the ActualLab.Rpc protocol—or 1000× more with Fusion’s transparent and perfectly coherent caching.

Topics

Resources

Security policy

Stars

177 stars

Watchers

5 watching

Forks

Used by

Contributors

Languages