Even though both libraries are targeting similar problems (they help to write real-time apps faster), they do this quite differently:
- SignalR offers an API helping to deliver server-side notifications to the clients (and vice versa). It’s ~ a layer above WebSockets,that helps your server-side code to track clients, group them, and send messages to either individual clients, a group of them, or all the clients connected to hub (~ SignalR server). Besides that, SignalR has clients written in several languages, and I guess they implement connection resilience and some of other must-have features any production app needs.
- And Fusion, even though it also uses WebSockets, solves a very different problem: it lets you find out when a piece of data you requested earlier gets changed. Fusion’s main selling points are:
a) it largely automates this complex task by tracking dependencies between such pieces of data
b) it is capable of doing this even if the data you consume is remote — and that’s exactly what enables Fusion-based real-time UI to work. Fusion offers a “replica service” abstraction, which looks identical to a similar server-side service, but instead of computing the output of any of its methods locally, it invokes a remote counterpart that does this and wires up everything needed to invalidate the replica as soon as matching remote value gets invalidated.
This was the main part of the answer. If you want to learn the details — e.g. what Fusion really changes in comparison to SignalR— read further.
SignalR in *Real* Real-Time Chat App
Imagine you’re building a real-time chat app, and SignalR is the library you want to use to deliver real-time updates to every client. Here is how a typical “change-to-update” sequence involving SignalR looks like:
- One of the clients makes an API call (or uses SignalR to message server) to post a new message to channel C.
- The server persists the message into the DB and responds ~ “200 OK” to the original client. If it wouldn’t be a real-time app, that’s where we would stop.
- But in our case we have to do more — now the server has to notify other clients about this change. This looks like a simple problem at glance — just broadcast the message to everyone connected! That’s the approach shown in almost every SignalR sample. Why? Keep reading.
- Ideally, you want to notify just the clients that are interested in this update — the ones keeping channel C open (so they see the chat itself) or “pinned” (so they see the number of unread messages), or maybe looking at the settings page of this channel, which also displays the number of unread messages there.
- SignalR’s “groups” feature is designed to address almost exactly this problem. All you need is to dynamically add/remove users to a group you may internally call as “seeingChannel(C)”. Once you have this, you can broadcast a message to this group.
- But if you think what’s required to implement this “dynamic add/remove” behavior, you may quickly conclude it’s totally not as easy as it seems, because you have to do this based on the client-side UI state — in other words, now you have to notify server each time client opens or closes settings page of channel C to let it decide whether the client still has to be a part of “seeingChannel(C)” or not, and so on. And it’s complex — e.g. the fact client closed channel C settings alone obviously doesn’t mean you can remove the client from “seeingChannel(C)” group.
- Worse, typically you want to retain the client-side UI state on reconnection / server restarts. And SignalR state (groups, etc.) doesn’t “survive” neither restarts nor connections to another server. So once this happens, you’ll need something allowing server and client to negotiate the groups to subscribe to.
- Oh, and what about the messages that could be posted while you were reconnecting? One way to address this is to re-request every piece of data you display (but you don’t want to lose what’s typed by user, right — i.e. you can’t simply refresh everything), another — to store the messages you send to every client & implement reliable delivery. In short, this is also something to think about.
- And that’s not the end: once you received the message on the client, you have to apply it to the UI state there, which typically means to dispatch through a set of reducers (if you use Redux) so that some of them will apply it to the corresponding part of the UI state, which in turn will make related UI components to re-render.
The gist: contrary to toy apps, the real-life SignalR application requires a fair amount of complex logic. If you want your chat app to be scalable like Slack, you need nearly what I wrote just to handle a single action — “MessagePosted”. And there are 10s of other actions — user joining the channel, leaving the channel, channel permissions changed, user presence messages, etc., and each one of them requires very similar handling.
But if you think what you are really achieving by doing all of this, you’ll quickly conclude the only end result of your actions is that the client-side state (of every client) becomes consistent with the server-side state!
In other words, SignalR, messaging, etc. — all of this isn’t essential to the problem you are solving. Your problem is to “replicate” the state changes happening on the server to every client that’s interested in this part of the server’s state.
And Fusion is designed to solve exactly this problem. Instead of making you to dynamically subscribe/unsubscribe every client to a fair number of SignalR channels, it does something similar completely automatically for you:
- When you invoke a replica service on the client, it automatically subscribes to the invalidation of the result
- Since server-side Fusion tracking works the same way on the client-side, the client is free to compose its own UI state relying on multiple calls to multiple replica services. Technically, it can even expose its own services to other clients (or to the server) as replica services!
- All of this allows you to precisely know when any piece of data you have becomes obsolete/invalidated (likely changed).
- Finally, every IComputed<T> (the “envelope” for any value Fusion tracks, including remote replicas) is “renewable” — it allows you to request its most up-to-date version at any moment, so you’re free to update any piece of data you have w/o any hassle. In particular, you don’t need to remember the original call arguments, etc. — this information is already there (but hidden, of course).
I highly recommend checking out Fusion Samples to see how this works in action, but overall, none of the issues I mentioned above are relevant there!
Besides that, Fusion provides:
- Connection resilience. Once the connection to Fusion Publisher is dropped, the remote Replicator will be trying to reconnect till it succeeds (FYI it publishes the status of each connection as IComputed too, so you can “observe” even it :) ). And once it succeeds, it will automatically re-subscribe to every replica it tracks, so if changes happened in between, you’ll learn about this, and if the replica was gone on server (e.g. due to timeout or server change), replica service on the client will request a new one — and all of this happens completely transparently for you!
- Protocol abstraction API. Almost everything related to remote replicas is located in Stl.Fusion.Bridge namespace, and you might notice it’s a part of Stl.Fusion.dll, which doesn’t reference neither ASP.NET Core nor WebSockets. Internally Fusion assumes that all the communication channels are Channel<Message>, where Message is a base type for Fusion messages. The WebSocket-based Fusion client (and server as well) relies on Channel<T> “wrapper” over WebSocket — in other words, it’s just one of possible communication protocols, and likely we’ll add more of them in future — harder, better, faster, stronger :)
The gist is: (Fusion, SignalR) ~ (apples, oranges).