diff --git a/packages/qwik/src/core/qrl/qrl-class.ts b/packages/qwik/src/core/qrl/qrl-class.ts index 0fc29c5ad28..870cfa12a70 100644 --- a/packages/qwik/src/core/qrl/qrl-class.ts +++ b/packages/qwik/src/core/qrl/qrl-class.ts @@ -64,7 +64,7 @@ export const createQRL = ( chunk: string | null, symbol: string, symbolRef: null | ValueOrPromise, - symbolFn: null | (() => Promise>), + symbolFn: null | (() => Promise>), capture: null | Readonly, captureRef: Readonly | null, refSymbol: string | null @@ -92,6 +92,35 @@ export const createQRL = ( return _containerEl; }; + // Wrap functions to provide their lexical scope + const wrapFn = (fn: TYPE): TYPE => { + if (typeof fn !== 'function' || (!capture?.length && !captureRef?.length)) { + return fn; + } + return function (this: unknown, ...args: QrlArgs) { + let context = tryGetInvokeContext(); + if (context) { + const prevQrl = context.$qrl$; + context.$qrl$ = qrl; + const prevEvent = context.$event$; + if (context.$event$ === undefined) { + context.$event$ = this as Event; + } + // const result = invoke.call(this, context, f, ...(args as Parameters)); + try { + return fn.apply(this, args); + } finally { + context.$qrl$ = prevQrl; + context.$event$ = prevEvent; + } + } + context = newInvokeContext(); + context.$qrl$ = qrl; + context.$event$ = this as Event; + return invoke.call(this, context, fn as any, ...args); + } as TYPE; + }; + const resolve = async (containerEl?: Element): Promise => { if (symbolRef !== null) { // Resolving (Promise) or already resolved (value) @@ -106,18 +135,20 @@ export const createQRL = ( const hash = _containerEl.getAttribute(QInstance)!; const doc = _containerEl.ownerDocument!; const qFuncs = getQFuncs(doc, hash); + // No need to wrap, syncQRLs can't have captured scope return (qrl.resolved = symbolRef = qFuncs[Number(symbol)] as TYPE); } + + const start = now(); + const ctx = tryGetInvokeContext(); if (symbolFn !== null) { - return (symbolRef = symbolFn().then( - (module) => (qrl.resolved = symbolRef = module[symbol] as TYPE) - )); + symbolRef = symbolFn().then((module) => (qrl.resolved = symbolRef = wrapFn(module[symbol]))); } else { const imported = getPlatform().importSymbol(_containerEl, chunk, symbol); - return (symbolRef = maybeThen(imported, (ref) => { - return (qrl.resolved = symbolRef = ref); - })); + symbolRef = maybeThen(imported, (ref) => (qrl.resolved = symbolRef = wrapFn(ref))); } + (symbolRef as Promise).finally(() => emitUsedSymbol(symbol, ctx?.$element$, start)); + return symbolRef; }; const resolveLazy = (containerEl?: Element): ValueOrPromise => { @@ -129,28 +160,18 @@ export const createQRL = ( currentCtx?: InvokeContext | InvokeTuple, beforeFn?: () => void | boolean ) { - return (...args: QrlArgs): QrlReturn => { - const start = now(); - const fn = resolveLazy() as TYPE; - return maybeThen(fn, (f) => { - if (isFunction(f)) { - if (beforeFn && beforeFn() === false) { - return; - } - const baseContext = createOrReuseInvocationContext(currentCtx); - const context: InvokeContext = { - ...baseContext, - $qrl$: qrl as QRLInternal, - }; - if (context.$event$ === undefined) { - context.$event$ = this as Event; - } - emitUsedSymbol(symbol, context.$element$, start); - return invoke.call(this, context, f, ...(args as Parameters)); + // Note that we bind the current `this` + return (...args: QrlArgs): QrlReturn => + maybeThen(resolveLazy(), (f) => { + if (!isFunction(f)) { + throw qError(QError_qrlIsNotFunction); + } + if (beforeFn && beforeFn() === false) { + return; } - throw qError(QError_qrlIsNotFunction); + const context = createOrReuseInvocationContext(currentCtx); + return invoke.call(this, context, f, ...(args as Parameters)); }); - }; } const createOrReuseInvocationContext = (invoke: InvokeContext | InvokeTuple | undefined) => { @@ -185,7 +206,8 @@ export const createQRL = ( resolved: undefined, }); if (symbolRef) { - maybeThen(symbolRef, (resolved) => (qrl.resolved = symbolRef = resolved)); + // Replace symbolRef with (a promise for) the value or wrapped function + symbolRef = maybeThen(symbolRef, (resolved) => (qrl.resolved = symbolRef = wrapFn(resolved))); } if (qDev) { seal(qrl); diff --git a/packages/qwik/src/core/qrl/qrl.unit.ts b/packages/qwik/src/core/qrl/qrl.unit.ts index 6203a8619a3..13a2cc882ae 100644 --- a/packages/qwik/src/core/qrl/qrl.unit.ts +++ b/packages/qwik/src/core/qrl/qrl.unit.ts @@ -3,6 +3,7 @@ import { createQRL } from './qrl-class'; import { qrl } from './qrl'; import { describe, test, assert, assertType, expectTypeOf } from 'vitest'; import { $, type QRL } from './qrl.public'; +import { useLexicalScope } from '../use/use-lexical-scope.public'; function matchProps(obj: any, properties: Record) { for (const [key, value] of Object.entries(properties)) { @@ -136,3 +137,94 @@ describe('serialization', () => { assert.equal(q.resolved, 'hello'); }); }); + +describe('createQRL', () => { + test('should create QRL', () => { + const q = createQRL('chunk', 'symbol', 'resolved', null, null, null, null); + matchProps(q, { + $chunk$: 'chunk', + $symbol$: 'symbol', + resolved: 'resolved', + }); + }); + test('should have .resolved: given scalar', async () => { + const q = createQRL('chunk', 'symbol', 'resolved', null, null, null, null); + assert.equal(q.resolved, 'resolved'); + }); + test('should have .resolved: given promise for scalar', async () => { + const q = createQRL('chunk', 'symbol', Promise.resolve('resolved'), null, null, null, null); + assert.equal(q.resolved, undefined); + assert.equal(await q.resolve(), 'resolved'); + assert.equal(q.resolved, 'resolved'); + }); + test('should have .resolved: promise for scalar', async () => { + const q = createQRL( + 'chunk', + 'symbol', + null, + () => Promise.resolve({ symbol: 'resolved' }), + null, + null, + null + ); + assert.equal(q.resolved, undefined); + assert.equal(await q.resolve(), 'resolved'); + assert.equal(q.resolved, 'resolved'); + }); + + const fn = () => 'hi'; + test('should have .resolved: given function without captures', async () => { + const q = createQRL('chunk', 'symbol', fn, null, null, null, null); + assert.equal(q.resolved, fn); + }); + test('should have .resolved: given promise for function without captures', async () => { + const q = createQRL('chunk', 'symbol', Promise.resolve(fn), null, null, null, null); + assert.equal(q.resolved, undefined); + assert.equal(await q.resolve(), fn); + assert.equal(q.resolved, fn); + }); + test('should have .resolved: promise for function without captures', async () => { + const q = createQRL( + 'chunk', + 'symbol', + null, + () => Promise.resolve({ symbol: fn }), + null, + null, + null + ); + assert.equal(q.resolved, undefined); + assert.equal(await q.resolve(), fn); + assert.equal(q.resolved, fn); + }); + + const capFn = () => useLexicalScope(); + test('should have .resolved: given function with captures', async () => { + const q = createQRL('chunk', 'symbol', capFn, null, null, ['hi'], null); + assert.isDefined(q.resolved); + assert.notEqual(q.resolved, capFn); + assert.deepEqual(q.resolved!(), ['hi']); + }); + test('should have .resolved: given promise for function with captures', async () => { + const q = createQRL('chunk', 'symbol', Promise.resolve(capFn), null, null, ['hi'], null); + assert.equal(q.resolved, undefined); + assert.deepEqual(await q(), ['hi']); + assert.notEqual(q.resolved, capFn); + assert.deepEqual(q.resolved!(), ['hi']); + }); + test('should have .resolved: promise for function with captures', async () => { + const q = createQRL( + 'chunk', + 'symbol', + null, + () => Promise.resolve({ symbol: capFn }), + null, + ['hi'], + null + ); + assert.equal(q.resolved, undefined); + assert.deepEqual(await q(), ['hi']); + assert.notEqual(q.resolved, capFn); + assert.deepEqual(q.resolved!(), ['hi']); + }); +});