qwik icon indicating copy to clipboard operation
qwik copied to clipboard

[🐞] sharedMap not available in server$ function

Open juanmarin-co opened this issue 2 years ago • 17 comments

Which component is affected?

Qwik City (routing)

Describe the bug

I have a layout middleware that loads the current session to sharedMap, like this:

export const onRequest: RequestHandler = async ({ sharedMap }) => {
  sharedMap.set("session", session);
};

Then I have a server$ function in a nested route that tries to access shared map but it returns an empty object, the function is like this:

export const getOffers = server$(async function () {
  const { sharedMap } = this;
  console.log("SHARED MAP", sharedMap);
});

Reproduction

https://stackblitz.com/edit/qwik-starter-gta4fs?file=src/routes/index.tsx

Steps to reproduce

As server functions don't work in stackblitz, you'll have to download the code and execute it locally

System Info

System:
    OS: Linux 6.2 Fedora Linux 38 (Workstation Edition)
    CPU: (12) x64 Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
    Memory: 18.60 GB / 31.19 GB
    Container: Yes
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.16.0 - ~/.volta/tools/image/node/18.16.0/bin/node
    npm: 9.5.1 - ~/.volta/tools/image/node/18.16.0/bin/npm
  Browsers:
    Chrome: 112.0.5615.165
    Firefox: 112.0.1
  npmPackages:
    @builder.io/qwik: ^0.100.0 => 0.100.0 
    @builder.io/qwik-city: ^0.100.0 => 0.100.0 
    undici: 5.21.0 => 5.21.0 
    vite: 4.2.1 => 4.2.1

Additional Information

No response

juanmarin-co avatar Apr 22 '23 23:04 juanmarin-co

Just in case I would try upgrading to 103 in case this fixes it: https://github.com/BuilderIO/qwik/pull/3813

cmbartschat avatar Apr 23 '23 04:04 cmbartschat

Oh, looks like sharedMap is available, it's just that the onRequest function in layout.tsx just isn't called for server$ it seems like.

cmbartschat avatar Apr 23 '23 04:04 cmbartschat

@cmbartschat I was able to reproduce the bug with 103

juanmarin-co avatar Apr 23 '23 04:04 juanmarin-co

where are you setting the sharedMap value?

manucorporat avatar Apr 24 '23 08:04 manucorporat

@manucorporat He's setting it in the onRequest in the layout and trying to call the server$ on the client from useVisibleTask$, don't think this would work anyway since any calls to server$ don't invoke any onRequest for layout middleware. (Also, sharedMap is undefined for example even when onRequest is present in the same context, like for example during SSR with useTask$ calling server$)

Is this something you think should stay as is or do you think server$ should also sit behind layout middlewares?

jordanw66 avatar Apr 24 '23 09:04 jordanw66

there is a way to make this work, create a plugin.ts in src/routes and add the onRequest there. plugin.ts files are always called, before any request!

manucorporat avatar Apr 24 '23 15:04 manucorporat

Hi @manucorporat, is the plugin.ts file considered part of the stable Api?

juanmarin-co avatar Apr 24 '23 16:04 juanmarin-co

@manucorporat Nice, didn't know about that! Works half-way, errors with TypeError: Cannot read properties of undefined (reading 'sharedMap') in SSR context, like calling server$ inside a useTask$:

export const getSession = server$(async function() {
  console.log("Calling from `server$`...");
  return this.sharedMap.get("session") as string;
});

export default component$(() => {
  const sessionKey = useSignal<string>();

  useTask$(async () => {
    // This will error during SSR as `sharedMap` is `undefined`.
    sessionKey.value = await getSession();
  });

  return (
    <>
      <div>Session key: {sessionKey.value}</div>
      <button onClick$={async () => sessionKey.value = await getSession()}>Get Key!</button>
    </>
  );
});

jordanw66 avatar Apr 24 '23 16:04 jordanw66

which version of qwik are you using?

manucorporat avatar Apr 24 '23 17:04 manucorporat

@manucorporat

  "devDependencies": {
    "@builder.io/qwik": "0.103.0",
    "@builder.io/qwik-city": "~0.103.0",
    "@types/eslint": "8.37.0",
    "@types/node": "^18.15.9",
    "@typescript-eslint/eslint-plugin": "5.59.0",
    "@typescript-eslint/parser": "5.59.0",
    "eslint": "8.38.0",
    "eslint-plugin-qwik": "0.103.0",
    "prettier": "2.8.7",
    "typescript": "5.0.4",
    "undici": "5.21.2",
    "vite": "4.2.2",
    "vite-tsconfig-paths": "4.2.0"
  }

jordanw66 avatar Apr 24 '23 17:04 jordanw66

I notice that plugin.ts only applies once at the top-level directly inside src/routes and cannot be nested so you can't differentiate or guard routes anyway with it. Useful for some things but I think back to my original question on whether server$ should sit behind middlewares or stay as is? Thoughts?

since any calls to server$ don't invoke any onRequest for layout middleware. (Also, sharedMap is undefined for example even when onRequest is present in the same context, like for example during SSR with useTask$ calling server$)

Is this something you think should stay as is or do you think server$ should also sit behind layout middlewares?

jordanw66 avatar Apr 24 '23 17:04 jordanw66

Yeah! this is intrinsec of server$ they can be called from any route, only routeAction, and routeLoader can be protected with a route guard.

manucorporat avatar Apr 24 '23 17:04 manucorporat

I figured as much, I think that makes sense too, just making sure and I think that is the heart of this issue. If server$ is not to be behind route guards with layout then it doesn't make sense to set sharedMap in onRequest somewhere else which will never get run and access it inside server$.

So this issue is not an issue, I think it is working as intended.

jordanw66 avatar Apr 24 '23 17:04 jordanw66

@jordanw66 but server$ functions offer a very simple API and semantics, routeAction$ could do but those semantics are weird for cases where you want to query some info. For example, I'm polling for newly generated data periodically

juanmarin-co avatar Apr 24 '23 21:04 juanmarin-co

@juanpmarin Yes I understand, server$ has a simpler API and is more flexible, whereas ...Action$ is more concrete and designed around forms/actions. I don't think there is currently a pleasing and simple way to achieve the following:

  1. Populate some piece of data on the page during SSR.
  2. Call back to the server to re-fetch data based on some criteria. (periodic, etc)
  3. Have this be protected by a route guard. (layouts)
  4. Do this in a performant and precise manner. (without re-fetching all other loaders, etc.)

I feel like there should be a streamlined way to achieve this like with a single routeLoader$ for example. It is achievable using a combination of features but with the following issues:

  1. ...Loader$s only load once on their own, no way to programmatically refresh only a specific loader without re-running or re-loading data for all the others.
  2. ...Action$s can return data but cause all ...Loader$s to reload.
  3. ...Action$s returning data cannot be called during SSR. (inside useTask$ for example)
  4. server$ works but is not route guarded and appears not intended to be.

With that said, this issue may be relevant to the above problem: https://github.com/BuilderIO/qwik/issues/2984

jordanw66 avatar Apr 25 '23 02:04 jordanw66

Yeah i agree, i think we will be able to solve this in an upcoming version of qwik

manucorporat avatar Apr 25 '23 12:04 manucorporat

Any progress or ideas on this @manucorporat?

Mo0nbase avatar Jul 17 '23 01:07 Mo0nbase

this should be fixed now but if you need middleware before server$ then you have to use qwik plugin.ts with plugin@.ts

PatrickJS avatar May 07 '24 04:05 PatrickJS