core: invoke context does not retain locale
Is your feature request related to a problem?
I'm exploring new solutions to make the project qwik-speak more dev friendly, and more efficient server side (client side the translations are inlined).
To achieve this, server side the use of Qwik's getLocale function is essential.
Under the hood, getLocale is a function that tries to read the context via the tryGetInvokeContext function: so I deduce that it should only be used where the context is available (from inside the components).
So let's assume I have a function that accepts a string in the signature, reads the locale, and returns the translated string:
export const myFn = (key: string | TemplateStringsArray): string => {
const locale = getLocale('en'); // in layout is set locale('it')
if (typeof key === 'string') {
return `${key} - ${locale}`;
} else {
return `${key[0]} - ${locale}`;
}
};
I added a tagged template to the signature to show the following inconsistency when it used in props:
export default component$(() => {
return (
<>
<Title name={myFn('Hello')} /> {/* used as function, context is not available and returns the default en */}
<Title name={myFn`Hello`} /> {/* used as tag function, context and the locale is available */}
</>
);
});
Reproduction: https://stackblitz.com/edit/qwik-starter-qlwire?file=src%2Froutes%2Findex.tsx
The function is the same: why the context is available only when called with template syntax?
If I inspect the compiled code, I found:
const s_xYL1qOwPyDI = () => {
return /* @__PURE__ */ _jsxC(Fragment, {
children: [
/* @__PURE__ */ _jsxC(Title, {
get name() {
return myFn("Hello");
},
[_IMMUTABLE]: {
name: _IMMUTABLE
}
}, 3, "H1_1"),
" ",
/* @__PURE__ */ _jsxC(Title, {
name: myFn`Hello`,
[_IMMUTABLE]: {
name: _IMMUTABLE
}
}, 3, "H1_2"),
" "
]
}, 1, "H1_3");
};
The only difference seems to be that the function call turns the prop into a getter.
@wmertens getLocale is used under the hood by both $localize and compiled-18n when used in Qwik. They works as long as I call the functions with the template syntax, otherwise not
Describe the solution you'd like
It would be great if the function always worked the same way, not depending on how it is called.
Describe alternatives you've considered
Alternatives exist, but they are not intuitive: avoiding passing functions as props and other solutions are not very dev friendly.
Thanks for any attention or help.
Greetings
Additional context
No response
The difference is the time of invocation: When the prop is turned into a getter, myFn will be called when the JSX is evaluated, and in the second case with the tagged function, myFn is called immediately.
So it seems that with the getter, the locale is not set when the JSX fn is ran? That's very odd.
So this must be a bug, it should work either way.
What's happening is that onGet() in the root layout gets called and sets the locale. This is stored in the invoke context. Then the index is rendered, and the tagged template gets the set locale, but the function call is called later and then the invoke context no longer holds the given locale.
One tempting solution is to make the "special" locale a regular context , automatically adding it to root. That way it gets serialized automatically as well and we could add other things. Thoughts @mhevery ?
Although that would use a bit of memory in every component that uses it
There's a couple of places where invokeContext() is called without passing locale, and there's also newInvokeContextFromTuple which takes a tuple but uses the container to grab the locale.
So one of these places is the likely culprit. It's not clear to me why sometimes the invokeContext is completely cleared.
<Title name={myFn('Hello')} /> {/* Optimizer can't know this is pure and so it delays execution of the function. */}
<Title name={myFn`Hello`} /> {/* function is considered pure and therefore evaluated eagerly as a constant. */}
The problem with getLocale on the server is that the server does not have a request local variable. The result is that there is no easy way to know what the getLocale should return. Imagine you have two concurrent requests. One for en and one for it. If the server code is fully synchronous, things are OK, but if there is an async operation, it is not clear which request the async operation belongs. So while I agree this is a bug, I have no idea how we could fix it.
@mhevery I'm confused, doesn't getLocale give you the SSR-context locale, which stores it for async invocations?
Depends on what you mean by context. As of right now, there is no way to pass context per "thread" (async chain) in JS. Because of this, when a function is running, there is no way to know (easily) for which request it is. Since the same server can serve multiple locales how exactly do we know which locale should be served?
The standard trick is
let globalContext;
function withContext(context, fn) {
const previusContext = globalContext;
globalContext = context;
try {
return fn();
} finally {
globalContext = previousContext
}
}
The issue is that if fn() spawns promises, then the finally statement will execute before the promises. The result is that any async work loses information about the context.
@mhevery no that's what async localstorage solves, it's storage "per thread". It's available in node, bun and cf workers at least.
we have asynclocalstorage now for requestEvent maybe we need to update tryGetInvokeContext with it too
@PatrickJS
BTW, the tryGetInvokeContext is marked as /** @public */ at https://github.com/QwikDev/qwik/blob/220536fa8d938209d26fa654ad4add5971c3b506/packages/qwik/src/core/use/use-core.ts#L91
but no exported entry for that. Is it a kind of bug?
@genki yes it should be marked internal
@PatrickJS IMHO locale is just one example of container-global data, and we should have a more general approach to it
@wmertens I see.
@mhevery Why the tagged functions are considered pure? I think that is not obvious.
@genki which tagged functions do you mean?
@wmertens I meant the tagged template literal function such as
const tagFunction = (s:string|TemplateStringsArray) => {
const value = typeof s === "string" ? s : s[0];
return value;
}
const foo = tagFunction`some text ${expr} some text`;
I think these tagged functions can have side effects so are not obviously pure function. I have also reported the issue about it. https://github.com/QwikDev/qwik/issues/6585
@wmertens I marked this as P1 (instead of P3 before) in gh project. As I understand it, this is more of a DX bug here that needs to be fixed but currently has a workaround right?
Actually I think the original issue is solved, the general issue (render-global data) should be solved better, and the pure template function problem should be a separate issue
As mentioned in the latest comment, it's solved. I close it. Thanks