leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
15.09k stars 587 forks source link

Isomorphic Signals (server <-> client) #1662

Open smessmer opened 9 months ago

smessmer commented 9 months ago

Is your feature request related to a problem? Please describe. For web apps that are used by multiple users at the same time, it is useful to have shared state on the server that is immediately synced with all clients. The reactive system seems like a good fit for this, but it currently doesn't run on the server, making things complicated. This feature would be very useful for a variety of web apps, e.g. live chats, but also simple CRUD apps that want to allow concurrent or collaborative editing.

Describe the solution you'd like

Server -> Client only

If we restrict this feature to signals created on the server and streamed to the client, then this shouldn't require too many changes to leptos. During SSR, or later in an even handler for a user interaction, a component could initialize a server side stream that gets synced to a client side signal. The semantics of this would be very similar to create_resource, just that it keeps updating the resource whenever the server side stream has a new value available instead of re-running the resource fetcher based on a client side trigger. Something like the following:

#[server]
fn server_side_signal() -> impl Stream {
  // ... return Stream ...
}

#[component]
fn MyComponent(cx: Scope) -> impl IntoView {
  let server_stream_signal = create_server_signal(server_side_signal);
  view!{cx,
    <span>{"Value "}{server_stream_signal()}</span>
  }
}
Client -> Server

It could also be useful to have signals that are written to by the client and automatically streamed to the server. Doing this would likely require much deeper changes to leptos though since afaik, the server currently doesn't run a reactive system. But if we do run a reactive system on the server, collaborative editing becomes very easy to implement. All it needs is for the server to pass a shared RwSignal with the data to all the clients, and they can concurrently modify it, and its state is automatically synced to the server and from there to other clients.

Describe alternatives you've considered

smessmer commented 9 months ago

Another use case: Say the server has a list of data items and either the whole list can change (by adding/removing items) or individual items can change. The reactive system on the client solves this (somewhat) nicely if you do

let list : Signal<Vec<Signal<DataItem>>> = ...;

If a single data item changes, only that signal updates, if new items get added or removed, the whole list changes. That means a single item changing won't require the client to re-render other items. Unfortunately, adding or removing items will likely cause a full re-render, but it's still better than the alternative of re-rendering for every individual item change.

The same scenario would be useful for server sent events, so that the server only needs to send changes to the client and doesn't have to resent the whole list each time. But this might mean that the stream approach I proposed above falls short. I'm not sure how well sending streams nested in streams would work. So we might actually benefit from a more "reactive" approach, i.e. using a shared Signal instance between the server and the client, even for just the Server -> Client use case.

smessmer commented 9 months ago

Yet another improvement this would have over leptos_sse or leptos_server_events is that those crates can only start the stream once the client is fully loaded and has done another roundtrip to the server to establish the SSE or WebSocket connection. This means this uses "sync rendering" and loses the optimization of "out of order streaming" as defined here where the server starts generating the value already on the first page load.

gbj commented 9 months ago

See also #1284, as there's a lot of overlap here.

To be clear: the reactive system does run on the server. create_effect does not, but create_isomorphic_effect does. (This isn't the case in SolidJS, but it is in Leptos.)

Overall, I'm pretty wary of baking an idea like this into the framework because I think it makes it much too easy for users to create really bad architectures for sharing that data.

Signals have the ability to be mutated in place. This is especially useful in situations like the ones you mention--chat or document editing--where cloning and replacing the whole object is quite expensive.

But mutating an object in place over a network connection/websocket isn't possible. So you can either

  1. Send the whole value of the shared signal over the connection on every update (yikes)
  2. Send patches over the network connection to update the signal

Doing 2 correctly using either operational transformation or CRDTs is genuinely hard, and an entire field of computing with many possibilities and opinions. Naively you can just implement an update function and a Msg type and send Msgs over the network, but doing this for multiple users will immediately become a train wreck of race conditions.

This is a situation where it shouldn't really be very hard to integrate some OT/CRDT library with the reactive system, but I'm hesitant to pull an opinionated solution like that into the core of the framework.

Your point re: HTML streaming is a good one; the solution here would be for a library to use a resource under the hood, at least for its initial value.

initial-algebra commented 3 months ago

Signals that are read-only on the client would be fine, no? They can then be a building block for CRDTs, efficiently creating the sockets and communicating changes back to the clients, while the framework leaves it up to the developer to implement the client->server communication and any synchronization. Well, maybe that's not necessarily ideal, but at least they can improve on leptos_sse with better integration into the framework.

EDIT: Maybe this isn't so necessary with the new server functions, at least when using WebSockets over SSE.