framesurge / perseus

A state-driven web development framework for Rust with full support for server-side rendering and static generation.
https://framesurge.sh/perseus/en-US
MIT License
2.17k stars 89 forks source link

State platform v2 (asynchronicity and browser-side state mutation) #150

Closed lukechu10 closed 1 year ago

lukechu10 commented 2 years ago

This issue is requesting an enhancement to Perseus. Details of the scope will be available in issue labels. The user described the problem related to this request as follows:

Integration with sycamore Suspense and async components

The user described the issue as follows:

Sycamore v0.8 has support for async components and Suspense. Suspense allows temporarily pausing rendering while async tasks (such as data-fetching) are executed.

Perseus already has get_build_state and I think that should stay. However, it would be nice to also have a way to integrate with Suspense and async components.

There are quite a few details that would need to be decided upon. The first one would be whether suspense is also awaited on the server side or if it is purely a client side construct. Some information on how this is handled in SolidJS (a similar JS library) is available here: https://github.com/solidjs/solid/tree/main/packages/solid-ssr

Another exciting possibility would be SSR streaming. This page (https://nextjs.org/docs/advanced-features/react-18/streaming) describes how NextJS does it with React.

Lastly, how would this work with the existing get_build_state? Should it be recommended to perform data-fetching with get_build_state or using Suspense?

I'll be willing to work on this but since the scope of this issue is so large, we'll probably need to do this in multiple steps. I can also give guidance on Sycamore's internals/implementations if needed.

Tribble internal data dHJpYmJsZS1yZXBvcnRlZCxDLWVuaGFuY2VtZW50LGF1dGhvci13aWxsaW5nLXRvLWltcGw=
arctic-hen7 commented 2 years ago

Yeah this is a great idea! I think the server side stuff should stay as is, because async is already fully supported there, and then an integration on the client side with async components should be great! Of course, we'd also need to support that when we actually render the components on the server, but that shouldn't be a problem.

As for streaming, this is also definitely something I want to set up. There's already some WS stuff on the client side for hot reloading, so similar code could be reused for this. Then it'd just be a matter of setting this up with all the server integrations.

lukechu10 commented 2 years ago

As for streaming, this is also definitely something I want to set up. There's already some WS stuff on the client side for hot reloading, so similar code could be reused for this. Then it'd just be a matter of setting this up with all the server integration

I believe we're talking about two different kinds of streaming here. What I was thinking of was something like https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2 using HTTP chunked transfer encoding.

The way this would work (and also the way ReactJS works too IIRC) would be once a suspense boundary resolves, a snippet of html and some inline js would be sent over the stream and then sycamore would insert it into the right place.

Here's another post that describes how this works in React: https://github.com/reactwg/react-18/discussions/37

arctic-hen7 commented 2 years ago

I think we are talking about the same thing, but would this use the same HTTP connection stream?

If so, I reckon WS could be very useful for inbuilt realtime operations (maybe a supplementary library).

Either way I'd like to hold off on streaming until the islands system is done, because I think there'll be a lot of integration there. Async components I'm happy to go ahead with after I've finished removing .perseus/.

lukechu10 commented 2 years ago

I think we are talking about the same thing, but would this use the same HTTP connection stream? The streaming I was thinking about would only be for the initial page loading. The WS based streaming, from what I understood, seems to be for realtime communication between client and server.

arctic-hen7 commented 2 years ago

Alright, here are my more formal thoughts on this.

  1. Perseus should not interfere with Sycamore's suspense system at all, allowing users to use it whenever it suits them. That would be evaluated as normal on the browser-side, and would just render the fallback state on the engine-side (as I believe is the current Sycamore default --- EDIT: not quite the case, see #225).
  2. Perseus should not support asynchronous templates, rather adding a browser-side addition to the state platform, whereby the user can register an arbitrary number of asynchronous browser-side functions that receive a reactive copy of the page state. These functions can then modify the state in the browser arbitrarily, while the main template reactively updates from user-set fallback states that can have arbitrary degrees of granularity. (E.g. a user might define a state with three different properties that require browser-side network requests, and each of these could be done in parallel, while a matrix could define a different fallback state for each of the eight possible states (2^3), if they want to).
  3. Perseus should add support for state delaying, whereby, once make_rx is converted to a derive macro (if possible), a derive macro helper #[delay_state] is defined, which can be used to annotate parts of a template's state that are particularly heavy. These can then be generated as usual on the engine-side, but they will be placed into separate JSON files on the engine-side, while placeholders referencing the URLs of those files will be placed in the actual state sent to the browser. Then, the user matches on an Option (or perhaps a custom enum) in their template, so the user receives a fallback state while heavy state is requested by the app shell.
  4. Preloading should receive a breaking change whereby preload directives must specify the state type of the preloaded template with a type parameter, allowing deserialization and reactivation of that state, along with fetching of its delayed state, which is only possible if we can get to the reactive version (since that will be where #[make_rx]'s successor will have put the function .fetch_delayed()). There should still be old-style preload directives that don't take the state type, for the rare cases in which the user does not know what they're preloading (which would mean delayed state would have to be fetched in the moment). This should be a distinct edge case, though.

This should pretty much cover everything this issue focuses on, and, for that reason, I'm going to appropriate this for v2 of Perseus' state platform, which will have a strong focus on browser-side state and asynchronicity. @lukechu10 does this basically cover everything you're talking about? I'm aware I haven't really addressed proper streaming, though I don't think this would convey any real benefit to Perseus (or any Wasm framework) at this stage, since Wasm is still unchunked (something I intend to deal with in 2023). The main network bottleneck would be getting the runtime necessary to handle streaming to the browser in the first place. Delayed state, however, does provide a very satisfactory solution (for me at least) to the problems of very large state.

There is also an argument that the render config should be fetched with a separate request, although this may require intertwining the render context with the Perseus router in a way that could be very technically complex. Hopefully, it will be fairly straightforward, and this will likely reduce the volume of bytes sent on initial page loads substantially (since the render config, which is basically an object defining all the routes in an app) has to be sent with every initial load right now.

arctic-hen7 commented 2 years ago

Note that this is conceptually, but not functionally, blocked by #225. Development on this will proceed even before that's fixed, if necessary.

arctic-hen7 commented 1 year ago

I have made a substantial miscalculation: delayed state is a terrible idea! As it stands, commits relating to this will be reverted and the system will not be deployed to main, let alone an actual release of Perseus. The system could only ever work with pure build state, since request-time delayed state would have to be written separately for every single request, which is not even remotely scalable. Either delayed state would only work with build state, or I implement a special in-memory store for delayed state. The latter option would massively increase the complexity of Perseus' server-side systems, and the former would increase complexity unnecessarily and add a major opportunity for confusion, while also complicating the amalgamation system greatly.

Unless there is an explicit feature request for a build-state-only version of delayed state, it will not be implemented. If anyone was counting on this feature, please let me know by opening a new issue!