Skip to content

[🐞 ✨] the future of server-side resumability(action$(...), server$(...), etc ... #91

Closed
@revintec

Description

@revintec
  1. is my vision and proposal aligned with qwik?
  2. if so, would these feature implementable? (I can contribute code
  3. if still, how long would them land?

Is your feature request related to a problem?

I'm using qwik 0.19.2, qwik-city 0.4.0, latest version as of writing
qwik recently introduced server$(...), along with the already existing action$(...)

Iet's focus on server$(...) first, because I think that is where the future is headed :)
it currently has the following problems:

  1. (easily fixable) any exception in the code block crashes the entire server, instead of sending the exception back to client(of course the user can choose to inhibit the exception detail for security reasons
  2. it would convert let to const when capturing scoped values, so it's actually not really a server-side function, it may be called a server-side class, all it's state is still serialized to and coming back from the client, no server-side state, just a syntactic sugar around XHR

image

  1. a even more dire problem, is that qwik seems been sending captured server values to client, which is extremely unsafe, and it seems been reading code pointer from client and executing them on server? which is even more unsafe -.- with sufficiently large codebase, this may even allow full remote control of the server(ROP like attacks

image

but as for now I didn't fully understand how this new server$(...) thing works internally, so I may be wrong. and I've seen somewhere else forget where, that (manucorporat) mentioned there are still some serialization problems, maybe that can be fixed(though I highly doubt that under the current architecture

  1. (a little far fetched) I think the current implementation of server$(...) is still a endpoint like request/response http thing, if you really think about it, it's just syntactic sugar around jsonp. we didn't really bring server-side resumability to the table, more on what I mean at the end of the article

why I'm not talking about action$(...)?

in my opinion, it has many fatal design flaws, even compared to server$(...)
from the source code, action$(...) seems to be registering itself to globalThis, this immediately raises several red flags for me:

  1. it only adds to the map but never removes them, so... memory leaks? (and under the current architecture that may be unfixable
  2. wow, it was registering itself to a map, so... single backend instance only? even if no LB or we configure the LB to do consistent routing -- which is a bad practice all by itself -- the backend server couldn't restart without losing state
  3. it's api surface was too complex, even seems entirely bound to form. I think a more clean and atomic interface like server$(...) would be more appropriate, I don't know how many people still use form these days

Describe the solution you'd like

I would show my gratitude to qwik first. I'm a system architect, so instead of feature I would also constantly thinking about scaleability, security and that kind of stuff. I've done enough frontend development that these days I normally won't write more frontend code, as that would add little to my knowledge base, but more of repetitive work and reinvention of the same wheel(no offence). but qwik really caught me! in years of my frontend chore, that was something new! that I was excited, that I think can be the future of frontend development

but qwik did more, it seems it can bring resumability to the server
I've used a lot ancient tech(asp.net runat=server, jsonp, php, someother server-side function etc.
so server$(...) is not new, but all those ancient tech -- maybe limited by their time -- never show me the possibility to really bring resumability to the server, they all seems just syntactic sugar around XHR. qwik -- along with it's compiler -- the ability to segregate code blocks and capture, serialize scoped values -- may finally be able to do:

server-side resumability

qwik currently suspends in SSG and resumes in browser, server-side resumability is like doing that in reverse. but it faces more challanges:

  1. browser can somewhat "trust" server data, because browser possesses no more secure data than the server(except for e2e security, but that is not really secure), and it's already running in a sandbox. the server however can't trust browser data or leak any data to them
  2. browser can be thought of a single threaded process(though in fact they may not necessarily be), but the server is inherently multi-process distributed systems

I propose the following solution
image

const capturedScopeValue=...,scopeValue=...
const coroutine=coroutine$(async(initialReqCtx)=>{
  let localValue=thisCodeRunsInServer()
  await yieldToBrowser(config)
  console.log(localValue) // ok, logs in browser
  console.log(capturedScopeValue) // ok, the data is serialized and sent to browser
  // localValue++ // won't work, the value is captured, thus in server-only
  // capturedScopeValue++ // won't work, the value is captured, thus in server-only
  let browserValue;
  await new Promise((resolve)=>button.once("click",()=>{browserValue=1;resolve()}))
  await resumeInServer(config)
  console.log(browserValue) // ok, logs in server
  // browserValue++ // won't work, the value is captured, thus in browser-only
  capturedScopeValue++ // works
  scopeValue++ // works
  localValue++ // works
},defaultConfigForYieldToBrowserAndResumeInServer)

notice a few things in the code:

  1. not single-shot request response, but multi-trip conversational
  2. scopeValue never send to client, browserValue is the only one coming from browser, capturedScopeValue, scopeValue, localValue is captured and send to client, but not read back from client, they resume in server
  3. each context switch(server <-> browser) involves an await and a config(if not provided, defaultConfigForYieldToBrowserAndResumeInServer is used, if that is not provided, a default implementation is used

the so called config customizes three aspect of server-side resumability, highlighted in red in the above diagram

  1. externalizer1:(ctx,...variableInfo) decides how variables(state) are serialized. it can, for example, detect scopedValue is non-local(through variableInfo), and write it to redis, only returning its redis key. the key, instead of data, is then send to browser, thus leaking no information(the key can be mangled and recovered only through internalizer1, thus the browser gains no knowledge). it can also detect that capturedScopeValue is non-local, allowing it's value to be send to the browser, optionally transform the value in the process(filter some fields etc), while also saving it to redis. the default implementation does nothing special, just leave the value in process memory. for localValue, the default implementation sends the mangled identifiers to browser, on resume it uses the ones recovered from the mangled identifiers, not the browser sent one. the mangled identifiers also have internal checks to prevent identifier remangle or reuse attacks
  2. externalizer2 adds extra routing information to the resulting url, thus when browser sends back request, LB can see its routing value and route the request to the original backend instance(or any other instance the LB deems appropriate). the default implementation adds nothing to the url
  3. versioning adds extra versioning information to the resulting url, thus when browser sends back request, a user-controlled(though config.versionCheck:(...)=>boolean) verifier is run, to decide if the code is runnable on the server. the default implementation adds script build time to the url and it must match exactly with the browser send back one to continue
  4. internalizer1 recovers variables(state) from externalizer1, thus complete the server-side resumability story full-circle. it can choose to recover capturedScopeValue and/or scopeValue from process memory(they may have been changed by now) or from redis. the default implementation try to recover them from process memory. it should also consider localValue as if resumed in a different server process, localValue have to be recovered too. the default implementation try to recover them first from injection made by internalizer2, then from process memory, minimizes the security risk, and if server resumed in different process, error. notice the different behavior for local and non-local values
  5. internalizer2 can run in LB or qwik, (re)routes request based on url's externalizer2 route value, optionally injects data to be recovered by internalizer1

Describe alternatives you've considered

implement server-side resumability directly, use qwik only to pass identifier references(like cookies), or entirely without qwik(use XHR or websocket directly

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions