Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(qrl): wrap resolved funcs for scope #6575

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']);
});
});
Loading