diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts index 116acfc959b..92254f548c1 100644 --- a/packages/qwik/src/core/shared/error/error.ts +++ b/packages/qwik/src/core/shared/error/error.ts @@ -7,7 +7,7 @@ export const codeToText = (code: number, ...parts: any[]): string => { const MAP = [ 'Error while serializing class or style attributes', // 0 'Scheduler not found', // 1 - '', // 2 unused + 'track() received object, without prop to track', // 2 'Only primitive and object literals can be serialized. {{0}}', // 3 '', // 4 unused 'You can render over a existing q:container. Skipping render().', // 5 @@ -50,12 +50,11 @@ export const codeToText = (code: number, ...parts: any[]): string => { "Element must have 'q:container' attribute.", // 42 'Unknown vnode type {{0}}.', // 43 'Materialize error: missing element: {{0}} {{1}} {{2}}', // 44 - 'Cannot coerce a Signal, use `.value` instead', // 46 - 'useComputedSignal$ QRL {{0}} {{1}} returned a Promise', // 47 - 'ComputedSignal is read-only', // 48 - 'WrappedSignal is read-only', // 49 - 'SsrError: Promises not expected here.', // 50 - 'Attribute value is unsafe for SSR', // 51 + 'Cannot coerce a Signal, use `.value` instead', // 45 + 'useComputedSignal$ QRL {{0}} {{1}} returned a Promise', // 46 + 'ComputedSignal is read-only', // 47 + 'WrappedSignal is read-only', // 48 + 'Attribute value is unsafe for SSR', // 49 ]; let text = MAP[code] ?? ''; if (parts.length) { @@ -77,7 +76,7 @@ export const codeToText = (code: number, ...parts: any[]): string => { export const enum QError { stringifyClassOrStyle = 0, schedulerNotFound = 1, - UNUSED_2 = 2, + trackObjectWithoutProp = 2, verifySerializable = 3, UNUSED_4 = 4, cannotRenderOverExistingContainer = 5, @@ -124,8 +123,7 @@ export const enum QError { computedNotSync = 46, computedReadOnly = 47, wrappedReadOnly = 48, - promisesNotExpected = 49, - unsafeAttr = 50, + unsafeAttr = 49, } export const qError = (code: number, errorMessageArgs: any[] = []): Error => { diff --git a/packages/qwik/src/core/shared/scheduler.ts b/packages/qwik/src/core/shared/scheduler.ts index 245b1225a71..9537399b08b 100644 --- a/packages/qwik/src/core/shared/scheduler.ts +++ b/packages/qwik/src/core/shared/scheduler.ts @@ -80,15 +80,7 @@ * declaration order within component. */ -import { assertEqual } from './error/assert'; -import type { QRLInternal } from './qrl/qrl-class'; -import type { JSXOutput } from './jsx/types/jsx-node'; -import { Task, TaskFlags, cleanupTask, runTask, type TaskFn } from '../use/use-task'; -import { runResource, type ResourceDescriptor } from '../use/use-resource'; -import { logWarn } from './utils/log'; -import { isPromise, maybeThenPassError, retryOnPromise, safeCall } from './utils/promises'; -import type { ValueOrPromise } from './utils/types'; -import { isDomContainer } from '../client/dom-container'; +import { isDomContainer, type DomContainer } from '../client/dom-container'; import { ElementVNodeProps, VNodeFlags, @@ -97,21 +89,28 @@ import { type ElementVNode, type VirtualVNode, } from '../client/types'; -import { vnode_isVNode, vnode_setAttr, VNodeJournalOpCode } from '../client/vnode'; +import { VNodeJournalOpCode, vnode_isVNode, vnode_setAttr } from '../client/vnode'; import { vnode_diff } from '../client/vnode-diff'; -import { executeComponent } from './component-execution'; -import type { Container, HostElement } from './types'; +import { triggerEffects, type ComputedSignal, type WrappedSignal } from '../signal/signal'; import { isSignal, type Signal } from '../signal/signal.public'; -import { type DomContainer } from '../client/dom-container'; -import { serializeAttribute } from './utils/styles'; +import type { TargetType } from '../signal/store'; +import type { ISsrNode } from '../ssr/ssr-types'; +import { runResource, type ResourceDescriptor } from '../use/use-resource'; +import { Task, TaskFlags, cleanupTask, runTask, type TaskFn } from '../use/use-task'; +import { executeComponent } from './component-execution'; import type { OnRenderFn } from './component.public'; +import { assertEqual } from './error/assert'; import type { Props } from './jsx/jsx-runtime'; +import type { JSXOutput } from './jsx/types/jsx-node'; +import type { QRLInternal } from './qrl/qrl-class'; +import { ssrNodeDocumentPosition, vnode_documentPosition } from './scheduler-document-position'; +import type { Container, HostElement } from './types'; +import { logWarn } from './utils/log'; import { QScopedStyle } from './utils/markers'; +import { isPromise, retryOnPromise, safeCall } from './utils/promises'; import { addComponentStylePrefix } from './utils/scoped-styles'; -import { type WrappedSignal, type ComputedSignal, triggerEffects } from '../signal/signal'; -import type { TargetType } from '../signal/store'; -import { ssrNodeDocumentPosition, vnode_documentPosition } from './scheduler-document-position'; -import type { ISsrNode } from '../ssr/ssr-types'; +import { serializeAttribute } from './utils/styles'; +import type { ValueOrPromise } from './utils/types'; // Turn this on to get debug output of what the scheduler is doing. const DEBUG: boolean = false; @@ -150,7 +149,7 @@ export interface Chore { $target$: ChoreTarget | null; $payload$: unknown; $resolve$: (value: any) => void; - $promise$: Promise; + $promise$?: Promise; $returnValue$: any; $executed$: boolean; } @@ -168,6 +167,10 @@ export type Scheduler = ReturnType; type ChoreTarget = HostElement | QRLInternal<(...args: unknown[]) => unknown> | Signal | TargetType; +const getPromise = (chore: Chore) => { + return (chore.$promise$ ||= new Promise((resolve) => (chore.$resolve$ = resolve))); +}; + export const createScheduler = ( container: Container, scheduleDrain: () => void, @@ -215,13 +218,7 @@ export const createScheduler = ( args: unknown[] ): ValueOrPromise; function schedule( - type: ChoreType.COMPONENT, - host: HostElement, - qrl: QRLInternal>, - props: Props | null - ): ValueOrPromise; - function schedule( - type: ChoreType.COMPONENT_SSR, + type: ChoreType.COMPONENT | ChoreType.COMPONENT_SSR, host: HostElement, qrl: QRLInternal>, props: Props | null @@ -273,7 +270,6 @@ export const createScheduler = ( $returnValue$: null, $executed$: false, }; - chore.$promise$ = new Promise((resolve) => (chore.$resolve$ = resolve)); DEBUG && debugTrace('schedule', chore, currentChore, choreQueue); chore = sortedInsert(choreQueue, chore, (container as DomContainer).rootVNode || null); if (!journalFlushScheduled && runLater) { @@ -283,9 +279,9 @@ export const createScheduler = ( scheduleDrain(); } if (runLater) { - return chore.$promise$; + return getPromise(chore); } else { - return drainUpTo(chore, (container as DomContainer).rootVNode || null); + return drainUpTo(chore); } } @@ -294,29 +290,13 @@ export const createScheduler = ( * * @param runUptoChore */ - function drainUpTo(runUptoChore: Chore, rootVNode: ElementVNode | null): ValueOrPromise { - // If it already ran, it's not in the queue - if (runUptoChore.$executed$) { - return runUptoChore.$returnValue$; - } - if (currentChore) { - // Already running chore, which means we're waiting for async completion - return runUptoChore.$promise$; - } + function drainUpTo(runUptoChore: Chore): ValueOrPromise { while (choreQueue.length) { - const nextChore = choreQueue[0]; - const order = choreComparator(nextChore, runUptoChore, rootVNode); - if (order !== null && order > 0) { - // we have processed all of the chores up to and including the given chore. - break; - } - // We can take the chore out of the queue - choreQueue.shift(); - if (order === null) { - // There was an error with the chore. - DEBUG && debugTrace('skip chore', nextChore, currentChore, choreQueue); - continue; + if (currentChore) { + // Already running chore, which means we're waiting for async completion + return getPromise(currentChore).then(() => drainUpTo(runUptoChore)); } + const nextChore = choreQueue.shift()!; const isDeletedVNode = vNodeAlreadyDeleted(nextChore); if ( isDeletedVNode && @@ -326,13 +306,12 @@ export const createScheduler = ( DEBUG && debugTrace('skip chore', nextChore, currentChore, choreQueue); continue; } - const returnValue = executeChore(nextChore); - if (isPromise(returnValue)) { - const promise = returnValue.then(() => drainUpTo(runUptoChore, rootVNode)); - return promise; + executeChore(nextChore); + if (nextChore === runUptoChore) { + break; } } - return runUptoChore.$returnValue$; + return runUptoChore.$executed$ ? runUptoChore.$returnValue$ : getPromise(runUptoChore); } function executeChore(chore: Chore): ValueOrPromise { @@ -376,12 +355,18 @@ export const createScheduler = ( ); break; case ChoreType.RESOURCE: - // Don't await the return value of the resource, because async resources should not be awaited. - // The reason for this is that we should be able to update for example a node with loading - // text. If we await the resource, the loading text will not be displayed until the resource - // is loaded. - const result = runResource(chore.$payload$ as ResourceDescriptor, container, host); - returnValue = isDomContainer(container) ? null : result; + { + // Don't await the return value of the resource, because async resources should not be awaited. + // The reason for this is that we should be able to update for example a node with loading + // text. If we await the resource, the loading text will not be displayed until the resource + // is loaded. + const result = runResource( + chore.$payload$ as ResourceDescriptor, + container, + host + ); + returnValue = isDomContainer(container) ? null : result; + } break; case ChoreType.RUN_QRL: { @@ -390,72 +375,88 @@ export const createScheduler = ( } break; case ChoreType.TASK: - returnValue = runTask(chore.$payload$ as Task, container, host); - break; case ChoreType.VISIBLE: returnValue = runTask(chore.$payload$ as Task, container, host); break; case ChoreType.CLEANUP_VISIBLE: - const task = chore.$payload$ as Task; - cleanupTask(task); + { + const task = chore.$payload$ as Task; + cleanupTask(task); + } break; case ChoreType.NODE_DIFF: - const parentVirtualNode = chore.$target$ as VirtualVNode; - let jsx = chore.$payload$ as JSXOutput; - if (isSignal(jsx)) { - jsx = jsx.value as any; + { + const parentVirtualNode = chore.$target$ as VirtualVNode; + let jsx = chore.$payload$ as JSXOutput; + if (isSignal(jsx)) { + jsx = jsx.value as any; + } + returnValue = retryOnPromise(() => + vnode_diff(container as DomContainer, jsx, parentVirtualNode, null) + ); } - returnValue = retryOnPromise(() => - vnode_diff(container as DomContainer, jsx, parentVirtualNode, null) - ); break; case ChoreType.NODE_PROP: - const virtualNode = chore.$host$ as unknown as ElementVNode; - const payload = chore.$payload$ as NodePropPayload; - let value: Signal | string = payload.$value$; - if (isSignal(value)) { - value = value.value as any; - } - const isConst = payload.$isConst$; - const journal = (container as DomContainer).$journal$; - const property = chore.$idx$ as string; - const serializedValue = serializeAttribute(property, value, payload.$scopedStyleIdPrefix$); - if (isConst) { - const element = virtualNode[ElementVNodeProps.element] as Element; - journal.push(VNodeJournalOpCode.SetAttribute, element, property, serializedValue); - } else { - vnode_setAttr(journal, virtualNode, property, serializedValue); + { + const virtualNode = chore.$host$ as unknown as ElementVNode; + const payload = chore.$payload$ as NodePropPayload; + let value: Signal | string = payload.$value$; + if (isSignal(value)) { + value = value.value as any; + } + const isConst = payload.$isConst$; + const journal = (container as DomContainer).$journal$; + const property = chore.$idx$ as string; + const serializedValue = serializeAttribute( + property, + value, + payload.$scopedStyleIdPrefix$ + ); + if (isConst) { + const element = virtualNode[ElementVNodeProps.element] as Element; + journal.push(VNodeJournalOpCode.SetAttribute, element, property, serializedValue); + } else { + vnode_setAttr(journal, virtualNode, property, serializedValue); + } } break; case ChoreType.QRL_RESOLVE: { - const target = chore.$target$ as QRLInternal; - returnValue = !target.resolved ? target.resolve() : null; + { + const target = chore.$target$ as QRLInternal; + returnValue = !target.resolved ? target.resolve() : null; + } break; } case ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS: { - const target = chore.$target$ as ComputedSignal | WrappedSignal; - const forceRunEffects = target.$forceRunEffects$; - target.$forceRunEffects$ = false; - if (!target.$effects$?.length) { - break; - } - returnValue = retryOnPromise(() => { - if (target.$computeIfNeeded$() || forceRunEffects) { - triggerEffects(container, target, target.$effects$); + { + const target = chore.$target$ as ComputedSignal | WrappedSignal; + const forceRunEffects = target.$forceRunEffects$; + target.$forceRunEffects$ = false; + if (!target.$effects$?.length) { + break; } - }); + returnValue = retryOnPromise(() => { + if (target.$computeIfNeeded$() || forceRunEffects) { + triggerEffects(container, target, target.$effects$); + } + }); + } break; } } - return maybeThenPassError(returnValue, (value) => { - if (currentChore) { - currentChore.$executed$ = true; - currentChore.$resolve$?.(value); - } - DEBUG && debugTrace('execute.DONE', null, currentChore, choreQueue); + const after = (value: any) => { currentChore = null; - return (chore.$returnValue$ = value); - }); + chore.$executed$ = true; + chore.$returnValue$ = value; + DEBUG && debugTrace('execute.DONE', null, chore, choreQueue); + chore.$resolve$?.(value); + return value; + }; + if (isPromise(returnValue)) { + chore.$promise$ = returnValue; + return returnValue.then(after); + } + return after(returnValue); } }; @@ -496,58 +497,51 @@ function choreComparator(a: Chore, b: Chore, rootVNode: ElementVNode | null): nu return macroTypeDiff; } - // JOURNAL_FLUSH does not have a host or $idx$, so we can't compare it. - if (a.$type$ !== ChoreType.JOURNAL_FLUSH) { - const aHost = a.$host$; - const bHost = b.$host$; - - if (aHost !== bHost && aHost !== null && bHost !== null) { - if (vnode_isVNode(aHost) && vnode_isVNode(bHost)) { - // we are running on the client. - const hostDiff = vnode_documentPosition(aHost, bHost, rootVNode); - if (hostDiff !== 0) { - return hostDiff; - } - } else { - // we are running on the server. - // On server we can't schedule task for a different host! - // Server is SSR, and therefore scheduling for anything but the current host - // implies that things need to be re-run nad that is not supported because of streaming. - const errorMessage = `SERVER: during HTML streaming, re-running tasks on a different host is not allowed. + const aHost = a.$host$; + const bHost = b.$host$; + + if (aHost !== bHost && aHost !== null && bHost !== null) { + if (vnode_isVNode(aHost) && vnode_isVNode(bHost)) { + // we are running on the client. + const hostDiff = vnode_documentPosition(aHost, bHost, rootVNode); + if (hostDiff !== 0) { + return hostDiff; + } + } else { + // we are running on the server. + // On server we can't schedule task for a different host! + // Server is SSR, and therefore scheduling for anything but the current host + // implies that things need to be re-run nad that is not supported because of streaming. + const errorMessage = `SERVER: during HTML streaming, re-running tasks on a different host is not allowed. You are attempting to change a state that has already been streamed to the client. This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR). Problematic Node: ${aHost.toString()}`; - logWarn(errorMessage); - const hostDiff = ssrNodeDocumentPosition(aHost as ISsrNode, bHost as ISsrNode); - if (hostDiff !== 0) { - return hostDiff; - } + logWarn(errorMessage); + const hostDiff = ssrNodeDocumentPosition(aHost as ISsrNode, bHost as ISsrNode); + if (hostDiff !== 0) { + return hostDiff; } } + } - const microTypeDiff = (a.$type$ & ChoreType.MICRO) - (b.$type$ & ChoreType.MICRO); - if (microTypeDiff !== 0) { - return microTypeDiff; - } + const microTypeDiff = (a.$type$ & ChoreType.MICRO) - (b.$type$ & ChoreType.MICRO); + if (microTypeDiff !== 0) { + return microTypeDiff; + } + // types are the same - const idxDiff = toNumber(a.$idx$) - toNumber(b.$idx$); - if (idxDiff !== 0) { - return idxDiff; - } + const idxDiff = toNumber(a.$idx$) - toNumber(b.$idx$); + if (idxDiff !== 0) { + return idxDiff; + } - // If the host is the same (or missing), and the type is the same, we need to compare the target. - if ( - a.$target$ !== b.$target$ && - (a.$type$ === ChoreType.QRL_RESOLVE || - a.$type$ === ChoreType.RUN_QRL || - a.$type$ === ChoreType.NODE_PROP || - a.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS) - ) { - // 1 means that we are going to process chores as FIFO - return 1; - } + // If the host is the same (or missing), and the type is the same, we need to compare the target. + if (a.$target$ !== b.$target$ || a.$payload$ !== b.$payload$) { + // 1 means that we are going to process chores as FIFO + return 1; } + // The chorse are the same and will run only once return 0; } diff --git a/packages/qwik/src/core/signal/signal.ts b/packages/qwik/src/core/signal/signal.ts index 9cfdf15d09d..f0d7793d2a0 100644 --- a/packages/qwik/src/core/signal/signal.ts +++ b/packages/qwik/src/core/signal/signal.ts @@ -32,6 +32,7 @@ import type { Props } from '../shared/jsx/jsx-runtime'; import type { OnRenderFn } from '../shared/component.public'; import { NEEDS_COMPUTATION } from './flags'; import { QError, qError } from '../shared/error/error'; +import { isDomContainer } from '../client/dom-container'; const DEBUG = false; @@ -350,7 +351,12 @@ export const triggerEffects = ( const qrl = container.getHostProp>>(host, OnRenderProp); assertDefined(qrl, 'Component must have QRL'); const props = container.getHostProp(host, ELEMENT_PROPS); - container.$scheduler$(ChoreType.COMPONENT, host, qrl, props); + container.$scheduler$( + isDomContainer(container) ? ChoreType.COMPONENT : ChoreType.COMPONENT_SSR, + host, + qrl, + props + ); } else if (property === EffectProperty.VNODE) { const host: HostElement = effect as any; const target = host; diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index 2d2351ae45a..6a99f3f3e88 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -34,9 +34,9 @@ import { applyInlineComponent, applyQwikComponentBody } from './ssr-render-compo import type { ISsrComponentFrame, ISsrNode, SSRContainer, SsrAttrs } from './ssr-types'; import { qInspector } from '../shared/utils/qdev'; import { serializeAttribute } from '../shared/utils/styles'; -import { QError, qError } from '../shared/error/error'; import { getFileLocationFromJsx } from '../shared/utils/jsx-filename'; import { queueQRL } from '../client/queue-qrl'; +import { ChoreType } from '../shared/scheduler'; class ParentComponentData { constructor( @@ -50,50 +50,17 @@ type StackValue = ValueOrPromise< >; /** @internal */ -export function _walkJSX( +export async function _walkJSX( ssr: SSRContainer, value: JSXOutput, options: { - allowPromises: true; currentStyleScoped: string | null; parentComponentFrame: ISsrComponentFrame | null; } -): ValueOrPromise; -/** @internal */ -export function _walkJSX( - ssr: SSRContainer, - value: JSXOutput, - options: { - allowPromises: false; - currentStyleScoped: string | null; - parentComponentFrame: ISsrComponentFrame | null; - } -): false; -/** @internal */ -export function _walkJSX( - ssr: SSRContainer, - value: JSXOutput, - options: { - allowPromises: boolean; - currentStyleScoped: string | null; - parentComponentFrame: ISsrComponentFrame | null; - } -): ValueOrPromise | false { +): Promise { const stack: StackValue[] = [value]; - let resolveDrain: () => void; - let rejectDrain: (reason: any) => void; - const drained = - options.allowPromises && - new Promise((res, rej) => { - resolveDrain = res; - rejectDrain = rej; - }); const enqueue = (value: StackValue) => stack.push(value); - const resolveValue = (value: JSXOutput) => { - stack.push(value); - drain(); - }; - const drain = (): void => { + const drain = async (): Promise => { while (stack.length) { const value = stack.pop(); if (value instanceof ParentComponentData) { @@ -102,33 +69,23 @@ export function _walkJSX( continue; } else if (typeof value === 'function') { if (value === Promise) { - if (!options.allowPromises) { - throw qError(QError.promisesNotExpected); - } - (stack.pop() as Promise).then(resolveValue, rejectDrain); - return; - } - const waitOn = (value as StackFn).apply(ssr); - if (waitOn) { - if (!options.allowPromises) { - throw qError(QError.promisesNotExpected); - } - waitOn.then(drain, rejectDrain); - return; + stack.push(await (stack.pop() as Promise)); + continue; } + await (value as StackFn).apply(ssr); continue; } processJSXNode(ssr, enqueue, value as JSXOutput, { styleScoped: options.currentStyleScoped, parentComponentFrame: options.parentComponentFrame, }); - } - if (stack.length === 0 && options.allowPromises) { - resolveDrain(); + if (ssr.$hasChores$) { + ssr.$hasChores$ = false; + await ssr.$scheduler$(ChoreType.WAIT_FOR_ALL); + } } }; - drain(); - return drained; + await drain(); } function processJSXNode( @@ -169,7 +126,6 @@ function processJSXNode( enqueue(async () => { for await (const chunk of value) { await _walkJSX(ssr, chunk as JSXOutput, { - allowPromises: true, currentStyleScoped: options.styleScoped, parentComponentFrame: options.parentComponentFrame, }); @@ -276,7 +232,6 @@ function processJSXNode( value = generator({ async write(chunk) { await _walkJSX(ssr, chunk as JSXOutput, { - allowPromises: true, currentStyleScoped: options.styleScoped, parentComponentFrame: options.parentComponentFrame, }); diff --git a/packages/qwik/src/core/ssr/ssr-types.ts b/packages/qwik/src/core/ssr/ssr-types.ts index 8bc7d51bbc9..c4b2c05552b 100644 --- a/packages/qwik/src/core/ssr/ssr-types.ts +++ b/packages/qwik/src/core/ssr/ssr-types.ts @@ -55,6 +55,7 @@ export interface ISsrComponentFrame { export type SymbolToChunkResolver = (symbol: string) => string; export interface SSRContainer extends Container { + $hasChores$: boolean; readonly tag: string; readonly writer: StreamWriter; readonly prefetchResources: PrefetchResource[]; diff --git a/packages/qwik/src/core/tests/use-task.spec.tsx b/packages/qwik/src/core/tests/use-task.spec.tsx index 891f06f8f93..a7b1cb62e43 100644 --- a/packages/qwik/src/core/tests/use-task.spec.tsx +++ b/packages/qwik/src/core/tests/use-task.spec.tsx @@ -55,7 +55,7 @@ describe.each([ }); const { vNode } = await render(, { debug }); - expect(log).toEqual(['Counter', 'task', 'resolved', 'Counter', 'render']); + expect(log).toEqual(['Counter', 'render', 'task', 'resolved']); expect(vNode).toMatchVDOM( @@ -98,17 +98,17 @@ describe.each([ return OK; }); - try { + if (render === ssrRenderToDom) { + await expect(() => render(, { debug })).rejects.toBe(error); + } else { await render( , { debug } ); - expect(ErrorProvider.error).toBe(render === domRender ? error : null); - } catch (e) { - expect(render).toBe(ssrRenderToDom); - expect(e).toBe(error); + // dom render does not throw errors + expect(ErrorProvider.error).toBe(error); } }); it('should not run next task until previous async task is finished', async () => { @@ -133,16 +133,7 @@ describe.each([ }); const { vNode } = await render(, { debug }); - expect(log).toEqual([ - 'Counter', - '1:task', - '1:resolved', - 'Counter', - '2:task', - '2:resolved', - 'Counter', - 'render', - ]); + expect(log).toEqual(['Counter', 'render', '1:task', '1:resolved', '2:task', '2:resolved']); expect(vNode).toMatchVDOM( @@ -312,6 +303,7 @@ describe.each([ (globalThis as any).log = [] as string[]; const Counter = component$(() => { const store = useStore({ count: 1, double: 0, quadruple: 0 }); + // Quadruple runs first, but will run again after double is updated useTask$(({ track }) => { (globalThis as any).log.push('quadruple'); const trackingValue = track(store, 'double') * 2; @@ -330,7 +322,7 @@ describe.each([ }); const { vNode, document } = await render(, { debug }); - expect((globalThis as any).log).toEqual(['quadruple', 'double', 'Counter', 'quadruple']); + expect((globalThis as any).log).toEqual(['Counter', 'quadruple', 'double', 'quadruple']); expect(vNode).toMatchVDOM(