Skip to content

Commit

Permalink
feat(qrl): wrap resolved funcs for scope (#6575)
Browse files Browse the repository at this point in the history
- now you can call the result of `.resolve()` (and `.resolved`) directly
- avoids wrapping QRL functions that don't use lexical scope
  • Loading branch information
wmertens authored Jun 20, 2024
1 parent 96ae4c6 commit 7b6eab6
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 28 deletions.
78 changes: 50 additions & 28 deletions packages/qwik/src/core/qrl/qrl-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const createQRL = <TYPE>(
chunk: string | null,
symbol: string,
symbolRef: null | ValueOrPromise<TYPE>,
symbolFn: null | (() => Promise<Record<string, unknown>>),
symbolFn: null | (() => Promise<Record<string, TYPE>>),
capture: null | Readonly<string[]>,
captureRef: Readonly<unknown[]> | null,
refSymbol: string | null
Expand Down Expand Up @@ -92,6 +92,35 @@ export const createQRL = <TYPE>(
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<TYPE>) {
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<typeof f>));
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<TYPE> => {
if (symbolRef !== null) {
// Resolving (Promise) or already resolved (value)
Expand All @@ -106,18 +135,20 @@ export const createQRL = <TYPE>(
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<TYPE>).finally(() => emitUsedSymbol(symbol, ctx?.$element$, start));
return symbolRef;
};

const resolveLazy = (containerEl?: Element): ValueOrPromise<TYPE> => {
Expand All @@ -129,28 +160,18 @@ export const createQRL = <TYPE>(
currentCtx?: InvokeContext | InvokeTuple,
beforeFn?: () => void | boolean
) {
return (...args: QrlArgs<TYPE>): QrlReturn<TYPE> => {
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<typeof f>));
// Note that we bind the current `this`
return (...args: QrlArgs<TYPE>): QrlReturn<TYPE> =>
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<typeof f>));
});
};
}

const createOrReuseInvocationContext = (invoke: InvokeContext | InvokeTuple | undefined) => {
Expand Down Expand Up @@ -185,7 +206,8 @@ export const createQRL = <TYPE>(
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);
Expand Down
92 changes: 92 additions & 0 deletions packages/qwik/src/core/qrl/qrl.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>) {
for (const [key, value] of Object.entries(properties)) {
Expand Down Expand Up @@ -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<Function>(
'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']);
});
});

0 comments on commit 7b6eab6

Please sign in to comment.