|
@@ -1482,7 +1480,7 @@ element
|
-Element \| VNode
+Element
|
diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx
index 4c4a72bcfad..063865c7534 100644
--- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx
+++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx
@@ -765,7 +765,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
}
};
_waitNextPage().then(() => {
- const container = _getQContainerElement(elm as _ElementVNode)!;
+ const container = _getQContainerElement(elm as Element)!;
container.setAttribute(Q_ROUTE, routeName);
const scrollState = currentScrollState(scroller);
saveScrollHistory(scrollState);
diff --git a/packages/qwik/src/core/client/chore-array.ts b/packages/qwik/src/core/client/chore-array.ts
deleted file mode 100644
index b2841344d88..00000000000
--- a/packages/qwik/src/core/client/chore-array.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl';
-import { StoreHandler } from '../reactive-primitives/impl/store';
-import { assertFalse } from '../shared/error/assert';
-import { PropsProxyHandler } from '../shared/jsx/props-proxy';
-import { isQrl } from '../shared/qrl/qrl-utils';
-import type { Chore } from '../shared/scheduler';
-import {
- ssrNodeDocumentPosition,
- vnode_documentPosition,
-} from '../shared/scheduler-document-position';
-import { ChoreType } from '../shared/util-chore-type';
-import type { ISsrNode } from '../ssr/ssr-types';
-import { vnode_isVNode } from './vnode';
-
-export class ChoreArray extends Array {
- add(value: Chore): number {
- /// We need to ensure that the `queue` is sorted by priority.
- /// 1. Find a place where to insert into.
- const idx = sortedFindIndex(this, value);
-
- if (idx < 0) {
- /// 2. Insert the chore into the queue.
- this.splice(~idx, 0, value);
- return idx;
- }
-
- const existing = this[idx];
- /**
- * When a derived signal is updated we need to run vnode_diff. However the signal can update
- * multiple times during component execution. For this reason it is necessary for us to update
- * the chore with the latest result of the signal.
- */
- if (existing.$payload$ !== value.$payload$) {
- existing.$payload$ = value.$payload$;
- }
- return idx;
- }
-
- delete(value: Chore) {
- const idx = this.indexOf(value);
- if (idx >= 0) {
- this.splice(idx, 1);
- }
- return idx;
- }
-}
-
-export function sortedFindIndex(sortedArray: Chore[], value: Chore): number {
- /// We need to ensure that the `queue` is sorted by priority.
- /// 1. Find a place where to insert into.
- let bottom = 0;
- let top = sortedArray.length;
- while (bottom < top) {
- const middle = bottom + ((top - bottom) >> 1);
- const midChore = sortedArray[middle];
- const comp = choreComparator(value, midChore);
- if (comp < 0) {
- top = middle;
- } else if (comp > 0) {
- bottom = middle + 1;
- } else {
- // We already have the host in the queue.
- return middle;
- }
- }
- return ~bottom;
-}
-
-/**
- * Compares two chores to determine their execution order in the scheduler's queue.
- *
- * @param a - The first chore to compare
- * @param b - The second chore to compare
- * @returns A number indicating the relative order of the chores. A negative number means `a` runs
- * before `b`.
- */
-export function choreComparator(a: Chore, b: Chore): number {
- const macroTypeDiff = (a.$type$ & ChoreType.MACRO) - (b.$type$ & ChoreType.MACRO);
- if (macroTypeDiff !== 0) {
- return macroTypeDiff;
- }
-
- 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);
- if (hostDiff !== 0) {
- return hostDiff;
- }
- } else {
- assertFalse(vnode_isVNode(aHost), 'expected aHost to be SSRNode but it is a VNode');
- assertFalse(vnode_isVNode(bHost), 'expected bHost to be SSRNode but it is a VNode');
- 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;
- }
- // types are the same
-
- 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$) {
- if (isQrl(a.$target$) && isQrl(b.$target$) && a.$target$.$hash$ === b.$target$.$hash$) {
- return 0;
- }
- // 1 means that we are going to process chores as FIFO
- return 1;
- }
-
- // ensure that the effect chores are scheduled for the same target
- // TODO: can we do this better?
- if (
- a.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS &&
- b.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS &&
- ((a.$target$ instanceof StoreHandler && b.$target$ instanceof StoreHandler) ||
- (a.$target$ instanceof PropsProxyHandler && b.$target$ instanceof PropsProxyHandler) ||
- (a.$target$ instanceof AsyncComputedSignalImpl &&
- b.$target$ instanceof AsyncComputedSignalImpl)) &&
- a.$payload$ !== b.$payload$
- ) {
- return 1;
- }
-
- // The chores are the same and will run only once
- return 0;
-}
-
-function toNumber(value: number | string): number {
- return typeof value === 'number' ? value : -1;
-}
diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts
index 1f2d6624542..dbb8df06d12 100644
--- a/packages/qwik/src/core/client/dom-container.ts
+++ b/packages/qwik/src/core/client/dom-container.ts
@@ -27,6 +27,7 @@ import {
QScopedStyle,
QStyle,
QStyleSelector,
+ QStylesAllSelector,
Q_PROPS_SEPARATOR,
USE_ON_LOCAL_SEQ_IDX,
getQFuncs,
@@ -47,23 +48,22 @@ import {
} from './types';
import { mapArray_get, mapArray_has, mapArray_set } from './util-mapArray';
import {
- VNodeJournalOpCode,
- vnode_applyJournal,
vnode_createErrorDiv,
- vnode_getDomParent,
- vnode_getProps,
+ vnode_getProp,
vnode_insertBefore,
vnode_isElementVNode,
- vnode_isVNode,
vnode_isVirtualVNode,
vnode_locate,
vnode_newUnMaterializedElement,
+ vnode_setProp,
type VNodeJournal,
-} from './vnode';
-import type { ElementVNode, VNode, VirtualVNode } from './vnode-impl';
+} from './vnode-utils';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
+import type { VNode } from '../shared/vnode/vnode';
+import type { VirtualVNode } from '../shared/vnode/virtual-vnode';
/** @public */
-export function getDomContainer(element: Element | VNode): IClientContainer {
+export function getDomContainer(element: Element): IClientContainer {
const qContainerElement = _getQContainerElement(element);
if (!qContainerElement) {
throw qError(QError.containerNotFound);
@@ -73,19 +73,12 @@ export function getDomContainer(element: Element | VNode): IClientContainer {
export function getDomContainerFromQContainerElement(qContainerElement: Element): IClientContainer {
const qElement = qContainerElement as ContainerElement;
- let container = qElement.qContainer;
- if (!container) {
- container = new DomContainer(qElement);
- }
- return container;
+ return (qElement.qContainer ||= new DomContainer(qElement));
}
/** @internal */
-export function _getQContainerElement(element: Element | VNode): Element | null {
- const qContainerElement: Element | null = vnode_isVNode(element)
- ? (vnode_getDomParent(element, true) as Element)
- : element;
- return qContainerElement.closest(QContainerSelector);
+export function _getQContainerElement(element: Element): Element | null {
+ return element.closest(QContainerSelector);
}
export const isDomContainer = (container: any): container is DomContainer => {
@@ -99,7 +92,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
public qManifestHash: string;
public rootVNode: ElementVNode;
public document: QDocument;
- public $journal$: VNodeJournal;
public $rawStateData$: unknown[];
public $storeProxyMap$: ObjToProxyMap = new WeakMap();
public $qFuncs$: Array<(...args: unknown[]) => unknown>;
@@ -111,27 +103,11 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
private $styleIds$: Set | null = null;
constructor(element: ContainerElement) {
- super(
- () => {
- this.$flushEpoch$++;
- vnode_applyJournal(this.$journal$);
- },
- {},
- element.getAttribute(QLocaleAttr)!
- );
+ super({}, element.getAttribute(QLocaleAttr)!);
this.qContainer = element.getAttribute(QContainerAttr)!;
if (!this.qContainer) {
throw qError(QError.elementWithoutContainer);
}
- this.$journal$ = [
- // The first time we render we need to hoist the styles.
- // (Meaning we need to move all styles from component inline to )
- // We bulk move all of the styles, because the expensive part is
- // for the browser to recompute the styles, (not the actual DOM manipulation.)
- // By moving all of them at once we can minimize the reflow.
- VNodeJournalOpCode.HoistStyles,
- element.ownerDocument,
- ];
this.document = element.ownerDocument as QDocument;
this.element = element;
this.$buildBase$ = element.getAttribute(QBaseAttr)!;
@@ -155,12 +131,30 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
preprocessState(this.$rawStateData$, this);
this.$stateData$ = wrapDeserializerProxy(this, this.$rawStateData$) as unknown[];
}
+ this.$hoistStyles$();
if (!qTest && element.isConnected) {
element.dispatchEvent(new CustomEvent('qresume', { bubbles: true }));
}
}
- $setRawState$(id: number, vParent: ElementVNode | VirtualVNode): void {
+ /**
+ * The first time we render we need to hoist the styles. (Meaning we need to move all styles from
+ * component inline to )
+ *
+ * We bulk move all of the styles, because the expensive part is for the browser to recompute the
+ * styles, (not the actual DOM manipulation.) By moving all of them at once we can minimize the
+ * reflow.
+ */
+ $hoistStyles$(): void {
+ const document = this.element.ownerDocument;
+ const head = document.head;
+ const styles = document.querySelectorAll(QStylesAllSelector);
+ for (let i = 0; i < styles.length; i++) {
+ head.appendChild(styles[i]);
+ }
+ }
+
+ $setRawState$(id: number, vParent: VNode): void {
this.$stateData$[id] = vParent;
}
@@ -171,17 +165,21 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
handleError(err: any, host: VNode | null): void {
if (qDev && host) {
if (typeof document !== 'undefined') {
- const vHost = host as VirtualVNode;
- const journal: VNodeJournal = [];
+ const vHost = host;
const vHostParent = vHost.parent;
const vHostNextSibling = vHost.nextSibling as VNode | null;
- const vErrorDiv = vnode_createErrorDiv(document, vHost, err, journal);
+ const journal: VNodeJournal = [];
+ const vErrorDiv = vnode_createErrorDiv(journal, document, vHost, err);
// If the host is an element node, we need to insert the error div into its parent.
const insertHost = vnode_isElementVNode(vHost) ? vHostParent || vHost : vHost;
// If the host is different then we need to insert errored-host in the same position as the host.
const insertBefore = insertHost === vHost ? null : vHostNextSibling;
- vnode_insertBefore(journal, insertHost, vErrorDiv, insertBefore);
- vnode_applyJournal(journal);
+ vnode_insertBefore(
+ journal,
+ insertHost as ElementVNode | VirtualVNode,
+ vErrorDiv,
+ insertBefore
+ );
}
if (err && err instanceof Error) {
@@ -223,7 +221,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
let vNode: VNode | null = host.parent;
while (vNode) {
if (vnode_isVirtualVNode(vNode)) {
- if (vNode.getProp(OnRenderProp, null) !== null) {
+ if (vnode_getProp(vNode, OnRenderProp, null) !== null) {
return vNode;
}
vNode =
@@ -239,7 +237,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
setHostProp(host: HostElement, name: string, value: T): void {
const vNode: VirtualVNode = host as any;
- vNode.setProp(name, value);
+ vnode_setProp(vNode, name, value);
}
getHostProp(host: HostElement, name: string): T | null {
@@ -258,20 +256,21 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
getObjectById = parseInt;
break;
}
- return vNode.getProp(name, getObjectById);
+ return vnode_getProp(vNode, name, getObjectById);
}
ensureProjectionResolved(vNode: VirtualVNode): void {
if ((vNode.flags & VNodeFlags.Resolved) === 0) {
vNode.flags |= VNodeFlags.Resolved;
- const props = vnode_getProps(vNode);
- for (let i = 0; i < props.length; i = i + 2) {
- const prop = props[i] as string;
- if (isSlotProp(prop)) {
- const value = props[i + 1];
- if (typeof value == 'string') {
- const projection = this.vNodeLocate(value);
- props[i + 1] = projection;
+ const props = vNode.props;
+ if (props) {
+ for (const prop of Object.keys(props)) {
+ if (isSlotProp(prop)) {
+ const value = props[prop];
+ if (typeof value == 'string') {
+ const projection = this.vNodeLocate(value);
+ props[prop] = projection;
+ }
}
}
}
@@ -307,7 +306,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
const styleElement = this.document.createElement('style');
styleElement.setAttribute(QStyle, styleId);
styleElement.textContent = content;
- this.$journal$.push(VNodeJournalOpCode.Insert, this.document.head, null, styleElement);
+ this.document.head.appendChild(styleElement);
}
}
diff --git a/packages/qwik/src/core/client/dom-render.ts b/packages/qwik/src/core/client/dom-render.ts
index b93a3a38e21..65404350502 100644
--- a/packages/qwik/src/core/client/dom-render.ts
+++ b/packages/qwik/src/core/client/dom-render.ts
@@ -1,6 +1,5 @@
-import type { FunctionComponent, JSXNode, JSXOutput } from '../shared/jsx/types/jsx-node';
+import type { FunctionComponent, JSXOutput } from '../shared/jsx/types/jsx-node';
import { isDocument, isElement } from '../shared/utils/element';
-import { ChoreType } from '../shared/util-chore-type';
import { QContainerValue } from '../shared/types';
import { DomContainer, getDomContainer } from './dom-container';
import { cleanup } from './vnode-diff';
@@ -8,6 +7,10 @@ import { QContainerAttr } from '../shared/utils/markers';
import type { RenderOptions, RenderResult } from './types';
import { qDev } from '../shared/utils/qdev';
import { QError, qError } from '../shared/error/error';
+import { vnode_setProp } from './vnode-utils';
+import { markVNodeDirty } from '../shared/vnode/vnode-dirty';
+import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum';
+import { NODE_DIFF_DATA_KEY } from '../shared/cursor/cursor-props';
/**
* Render JSX.
@@ -42,11 +45,16 @@ export const render = async (
const container = getDomContainer(parent as HTMLElement) as DomContainer;
container.$serverData$ = opts.serverData || {};
const host = container.rootVNode;
- container.$scheduler$(ChoreType.NODE_DIFF, host, host, jsxNode as JSXNode);
- await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
+ vnode_setProp(host, NODE_DIFF_DATA_KEY, jsxNode);
+ markVNodeDirty(container, host, ChoreBits.NODE_DIFF);
+ await container.$renderPromise$;
return {
cleanup: () => {
- cleanup(container, container.rootVNode);
+ /**
+ * This can lead to cleaning up projection vnodes via the journal, but since we're cleaning up
+ * they don't matter so we ignore the journal
+ */
+ cleanup(container, [], container.rootVNode);
},
};
};
diff --git a/packages/qwik/src/core/client/process-vnode-data.ts b/packages/qwik/src/core/client/process-vnode-data.ts
index 2278abaebfa..b5993572fa7 100644
--- a/packages/qwik/src/core/client/process-vnode-data.ts
+++ b/packages/qwik/src/core/client/process-vnode-data.ts
@@ -1,7 +1,7 @@
// NOTE: we want to move this function to qwikloader, and therefore this function should not have any external dependencies
import { VNodeDataChar, VNodeDataSeparator } from '../shared/vnode-data-types';
import type { ContainerElement, QDocument } from './types';
-import type { ElementVNode } from './vnode-impl';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
/**
* Process the VNodeData script tags and store the VNodeData in the VNodeDataMap.
diff --git a/packages/qwik/src/core/client/process-vnode-data.unit.tsx b/packages/qwik/src/core/client/process-vnode-data.unit.tsx
index f33fad6dc39..d2d72a1cf18 100644
--- a/packages/qwik/src/core/client/process-vnode-data.unit.tsx
+++ b/packages/qwik/src/core/client/process-vnode-data.unit.tsx
@@ -7,7 +7,7 @@ import { processVNodeData } from './process-vnode-data';
import type { ClientContainer } from './types';
import { QContainerValue } from '../shared/types';
import { QContainerAttr } from '../shared/utils/markers';
-import { vnode_getFirstChild } from './vnode';
+import { vnode_getFirstChild } from './vnode-utils';
import { Fragment } from '@qwik.dev/core';
describe('processVnodeData', () => {
diff --git a/packages/qwik/src/core/client/run-qrl.ts b/packages/qwik/src/core/client/run-qrl.ts
index e0c717edcd2..96199380d31 100644
--- a/packages/qwik/src/core/client/run-qrl.ts
+++ b/packages/qwik/src/core/client/run-qrl.ts
@@ -1,11 +1,11 @@
-import { QError, qError } from '../shared/error/error';
import type { QRLInternal } from '../shared/qrl/qrl-class';
-import { getChorePromise } from '../shared/scheduler';
-import { ChoreType } from '../shared/util-chore-type';
+import { catchError, retryOnPromise } from '../shared/utils/promises';
import type { ValueOrPromise } from '../shared/utils/types';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
import { getInvokeContext } from '../use/use-core';
import { useLexicalScope } from '../use/use-lexical-scope.public';
import { getDomContainer } from './dom-container';
+import { VNodeFlags } from './types';
/**
* This is called by qwik-loader to run a QRL. It has to be synchronous.
@@ -17,20 +17,23 @@ export const _run = (...args: unknown[]): ValueOrPromise => {
const [runQrl] = useLexicalScope<[QRLInternal<(...args: unknown[]) => unknown>]>();
const context = getInvokeContext();
const hostElement = context.$hostElement$;
-
- if (!hostElement) {
- // silently ignore if there is no host element, the element might have been removed
- return;
+ if (hostElement) {
+ return retryOnPromise(() => {
+ if (!(hostElement.flags & VNodeFlags.Deleted)) {
+ return catchError(
+ () => runQrl(...args),
+ (err) => {
+ const container = (context.$container$ ||= getDomContainer(
+ (hostElement as ElementVNode).node
+ ));
+ if (container) {
+ container.handleError(err, hostElement);
+ } else {
+ throw err;
+ }
+ }
+ );
+ }
+ });
}
-
- const container = getDomContainer(context.$element$!);
-
- const scheduler = container.$scheduler$;
- if (!scheduler) {
- throw qError(QError.schedulerNotFound);
- }
-
- // We don't return anything, the scheduler is in charge now
- const chore = scheduler(ChoreType.RUN_QRL, hostElement, runQrl, args);
- return getChorePromise(chore);
};
diff --git a/packages/qwik/src/core/client/types.ts b/packages/qwik/src/core/client/types.ts
index 4bf341e23c5..6d04971ab42 100644
--- a/packages/qwik/src/core/client/types.ts
+++ b/packages/qwik/src/core/client/types.ts
@@ -2,8 +2,8 @@
import type { QRL } from '../shared/qrl/qrl.public';
import type { Container } from '../shared/types';
-import type { VNodeJournal } from './vnode';
-import type { ElementVNode, VirtualVNode } from './vnode-impl';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
+import type { VirtualVNode } from '../shared/vnode/virtual-vnode';
export type ClientAttrKey = string;
export type ClientAttrValue = string | null;
@@ -17,9 +17,7 @@ export interface ClientContainer extends Container {
$locale$: string;
qManifestHash: string;
rootVNode: ElementVNode;
- $journal$: VNodeJournal;
$forwardRefs$: Array | null;
- $flushEpoch$: number;
parseQRL(qrl: string): QRL;
$setRawState$(id: number, vParent: ElementVNode | VirtualVNode): void;
}
@@ -74,30 +72,32 @@ export interface QDocument extends Document {
* @internal
*/
export const enum VNodeFlags {
- Element /* ****************** */ = 0b00_000001,
- Virtual /* ****************** */ = 0b00_000010,
- ELEMENT_OR_VIRTUAL_MASK /* ** */ = 0b00_000011,
- Text /* ********************* */ = 0b00_000100,
- ELEMENT_OR_TEXT_MASK /* ***** */ = 0b00_000101,
- TYPE_MASK /* **************** */ = 0b00_000111,
- INFLATED_TYPE_MASK /* ******* */ = 0b00_001111,
+ Element /* ****************** */ = 0b00_0000001,
+ Virtual /* ****************** */ = 0b00_0000010,
+ ELEMENT_OR_VIRTUAL_MASK /* ** */ = 0b00_0000011,
+ Text /* ********************* */ = 0b00_0000100,
+ ELEMENT_OR_TEXT_MASK /* ***** */ = 0b00_0000101,
+ TYPE_MASK /* **************** */ = 0b00_0000111,
+ INFLATED_TYPE_MASK /* ******* */ = 0b00_0001111,
/// Extra flag which marks if a node needs to be inflated.
- Inflated /* ***************** */ = 0b00_001000,
+ Inflated /* ***************** */ = 0b00_0001000,
/// Marks if the `ensureProjectionResolved` has been called on the node.
- Resolved /* ***************** */ = 0b00_010000,
+ Resolved /* ***************** */ = 0b00_0010000,
/// Marks if the vnode is deleted.
- Deleted /* ****************** */ = 0b00_100000,
+ Deleted /* ****************** */ = 0b00_0100000,
+ /// Marks if the vnode is a cursor (has priority set).
+ Cursor /* ******************* */ = 0b00_1000000,
/// Flags for Namespace
- NAMESPACE_MASK /* *********** */ = 0b11_000000,
- NEGATED_NAMESPACE_MASK /* ** */ = ~0b11_000000,
- NS_html /* ****************** */ = 0b00_000000, // http://www.w3.org/1999/xhtml
- NS_svg /* ******************* */ = 0b01_000000, // http://www.w3.org/2000/svg
- NS_math /* ****************** */ = 0b10_000000, // http://www.w3.org/1998/Math/MathML
+ NAMESPACE_MASK /* *********** */ = 0b11_0000000,
+ NEGATED_NAMESPACE_MASK /* ** */ = ~0b11_0000000,
+ NS_html /* ****************** */ = 0b00_0000000, // http://www.w3.org/1999/xhtml
+ NS_svg /* ******************* */ = 0b01_0000000, // http://www.w3.org/2000/svg
+ NS_math /* ****************** */ = 0b10_0000000, // http://www.w3.org/1998/Math/MathML
}
export const enum VNodeFlagsIndex {
- mask /* ************** */ = 0b11_111111,
- shift /* ************* */ = 8,
+ mask /* ************** */ = 0b11_1111111,
+ shift /* ************* */ = 9,
}
export const enum VNodeProps {
diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts
index 029b3d0fac4..0f5d3d3cc07 100644
--- a/packages/qwik/src/core/client/vnode-diff.ts
+++ b/packages/qwik/src/core/client/vnode-diff.ts
@@ -1,5 +1,4 @@
import { isDev } from '@qwik.dev/core/build';
-import { _CONST_PROPS, _EFFECT_BACK_REF, _VAR_PROPS } from '../internal';
import { clearAllEffects, clearEffectSubscription } from '../reactive-primitives/cleanup';
import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
import type { Signal } from '../reactive-primitives/signal.public';
@@ -27,13 +26,11 @@ import type { JSXNodeInternal } from '../shared/jsx/types/jsx-node';
import type { JSXChildren } from '../shared/jsx/types/jsx-qwik-attributes';
import { SSRComment, SSRRaw, SkipRender } from '../shared/jsx/utils.public';
import type { QRLInternal } from '../shared/qrl/qrl-class';
-import { isSyncQrl } from '../shared/qrl/qrl-utils';
import type { QRL } from '../shared/qrl/qrl.public';
import type { HostElement, QElement, QwikLoaderEventScope, qWindow } from '../shared/types';
import { DEBUG_TYPE, QContainerValue, VirtualType } from '../shared/types';
-import { ChoreType } from '../shared/util-chore-type';
import { escapeHTML } from '../shared/utils/character-escaping';
-import { _OWNER, _PROPS_HANDLER } from '../shared/utils/constants';
+import { _CONST_PROPS, _OWNER, _PROPS_HANDLER, _VAR_PROPS } from '../shared/utils/constants';
import {
fromCamelToKebabCase,
getEventDataFromHtmlAttribute,
@@ -43,7 +40,6 @@ import {
} from '../shared/utils/event-names';
import { getFileLocationFromJsx } from '../shared/utils/jsx-filename';
import {
- ELEMENT_KEY,
ELEMENT_PROPS,
ELEMENT_SEQ,
OnRenderProp,
@@ -54,24 +50,22 @@ import {
QTemplate,
dangerouslySetInnerHTML,
} from '../shared/utils/markers';
-import { isPromise, retryOnPromise } from '../shared/utils/promises';
+import { isPromise, retryOnPromise, safeCall } from '../shared/utils/promises';
import { isSlotProp } from '../shared/utils/prop';
import { hasClassAttr } from '../shared/utils/scoped-styles';
import { serializeAttribute } from '../shared/utils/styles';
import { isArray, isObject, type ValueOrPromise } from '../shared/utils/types';
import { trackSignalAndAssignHost } from '../use/use-core';
import { TaskFlags, isTask } from '../use/use-task';
-import type { DomContainer } from './dom-container';
-import { VNodeFlags, type ClientAttrs, type ClientContainer } from './types';
-import { mapApp_findIndx, mapArray_set } from './util-mapArray';
+import { VNodeFlags, type ClientContainer } from './types';
+import { mapApp_findIndx } from './util-mapArray';
import {
- VNodeJournalOpCode,
vnode_ensureElementInflated,
vnode_getDomParentVNode,
vnode_getElementName,
vnode_getFirstChild,
vnode_getProjectionParentComponent,
- vnode_getProps,
+ vnode_getProp,
vnode_getText,
vnode_getType,
vnode_insertBefore,
@@ -85,26 +79,33 @@ import {
vnode_newText,
vnode_newVirtual,
vnode_remove,
+ vnode_setAttr,
+ vnode_setProp,
vnode_setText,
vnode_truncate,
vnode_walkVNode,
type VNodeJournal,
-} from './vnode';
-import { ElementVNode, TextVNode, VNode, VirtualVNode } from './vnode-impl';
+} from './vnode-utils';
import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace';
import { cleanupDestroyable } from '../use/utils/destroyable';
import { SignalImpl } from '../reactive-primitives/impl/signal-impl';
import { isStore } from '../reactive-primitives/impl/store';
import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl';
+import type { VNode } from '../shared/vnode/vnode';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
+import type { VirtualVNode } from '../shared/vnode/virtual-vnode';
+import type { TextVNode } from '../shared/vnode/text-vnode';
+import { markVNodeDirty } from '../shared/vnode/vnode-dirty';
+import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum';
+import { _EFFECT_BACK_REF } from '../reactive-primitives/backref';
export const vnode_diff = (
container: ClientContainer,
+ journal: VNodeJournal,
jsxNode: JSXChildren,
vStartNode: VNode,
scopedStyleIdPrefix: string | null
) => {
- let journal = (container as DomContainer).$journal$;
-
/**
* Stack is used to keep track of the state of the traversal.
*
@@ -213,7 +214,15 @@ export const vnode_diff = (
if (typeof type === 'string') {
expectNoMoreTextNodes();
expectElement(jsxValue, type);
- descend(jsxValue.children, true);
+
+ const hasDangerousInnerHTML =
+ (jsxValue.constProps && dangerouslySetInnerHTML in jsxValue.constProps) ||
+ dangerouslySetInnerHTML in jsxValue.varProps;
+ if (hasDangerousInnerHTML) {
+ expectNoChildren(false);
+ } else {
+ descend(jsxValue.children, true);
+ }
} else if (typeof type === 'function') {
if (type === Fragment) {
expectNoMoreTextNodes();
@@ -247,7 +256,6 @@ export const vnode_diff = (
}
} else if (jsxValue === (SkipRender as JSXChildren)) {
// do nothing, we are skipping this node
- journal = [];
} else {
expectText('');
}
@@ -415,14 +423,15 @@ export const vnode_diff = (
const projections: Array = [];
if (host) {
- const props = vnode_getProps(host);
- // we need to create empty projections for all the slots to remove unused slots content
- for (let i = 0; i < props.length; i = i + 2) {
- const prop = props[i] as string;
- if (isSlotProp(prop)) {
- const slotName = prop;
- projections.push(slotName);
- projections.push(createProjectionJSXNode(slotName));
+ const props = host.props;
+ if (props) {
+ // we need to create empty projections for all the slots to remove unused slots content
+ for (const prop of Object.keys(props)) {
+ if (isSlotProp(prop)) {
+ const slotName = prop;
+ projections.push(slotName);
+ projections.push(createProjectionJSXNode(slotName));
+ }
}
}
}
@@ -462,7 +471,7 @@ export const vnode_diff = (
const slotName = jsxNode.key as string;
// console.log('expectProjection', JSON.stringify(slotName));
// The parent is the component and it should have our portal.
- vCurrent = (vParent as VirtualVNode).getProp(slotName, (id) =>
+ vCurrent = vnode_getProp(vParent as VirtualVNode, slotName, (id: string) =>
vnode_locate(container.rootVNode, id)
);
// if projection is marked as deleted then we need to create a new one
@@ -473,11 +482,11 @@ export const vnode_diff = (
// that is wrong. We don't yet know if the projection will be projected, so
// we should leave it unattached.
// vNewNode[VNodeProps.parent] = vParent;
- isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Projection);
- isDev && (vNewNode as VirtualVNode).setProp('q:code', 'expectProjection');
- (vNewNode as VirtualVNode).setProp(QSlot, slotName);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Projection);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, 'q:code', 'expectProjection');
+ vnode_setProp(vNewNode as VirtualVNode, QSlot, slotName);
(vNewNode as VirtualVNode).slotParent = vParent;
- (vParent as VirtualVNode).setProp(slotName, vNewNode);
+ vnode_setProp(vParent as VirtualVNode, slotName, vNewNode);
}
}
@@ -485,17 +494,17 @@ export const vnode_diff = (
const vHost = vnode_getProjectionParentComponent(vParent);
const slotNameKey = getSlotNameKey(vHost);
- // console.log('expectSlot', JSON.stringify(slotNameKey));
const vProjectedNode = vHost
- ? (vHost as VirtualVNode).getProp(
+ ? vnode_getProp(
+ vHost as VirtualVNode,
slotNameKey,
// for slots this id is vnode ref id
null // Projections should have been resolved through container.ensureProjectionResolved
//(id) => vnode_locate(container.rootVNode, id)
)
: null;
- // console.log(' ', String(vHost), String(vProjectedNode));
+
if (vProjectedNode == null) {
// Nothing to project, so render content of the slot.
vnode_insertBefore(
@@ -504,14 +513,13 @@ export const vnode_diff = (
(vNewNode = vnode_newVirtual()),
vCurrent && getInsertBefore()
);
- (vNewNode as VirtualVNode).setProp(QSlot, slotNameKey);
- vHost && (vHost as VirtualVNode).setProp(slotNameKey, vNewNode);
- isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Projection);
- isDev && (vNewNode as VirtualVNode).setProp('q:code', 'expectSlot' + count++);
+ vnode_setProp(vNewNode as VirtualVNode, QSlot, slotNameKey);
+ vHost && vnode_setProp(vHost as VirtualVNode, slotNameKey, vNewNode);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Projection);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, 'q:code', 'expectSlot' + count++);
return false;
} else if (vProjectedNode === vCurrent) {
// All is good.
- // console.log(' NOOP', String(vCurrent));
} else {
// move from q:template to the target node
vnode_insertBefore(
@@ -520,10 +528,10 @@ export const vnode_diff = (
(vNewNode = vProjectedNode),
vCurrent && getInsertBefore()
);
- (vNewNode as VirtualVNode).setProp(QSlot, slotNameKey);
- vHost && (vHost as VirtualVNode).setProp(slotNameKey, vNewNode);
- isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Projection);
- isDev && (vNewNode as VirtualVNode).setProp('q:code', 'expectSlot' + count++);
+ vnode_setProp(vNewNode as VirtualVNode, QSlot, slotNameKey);
+ vHost && vnode_setProp(vHost as VirtualVNode, slotNameKey, vNewNode);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Projection);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, 'q:code', 'expectSlot' + count++);
}
return true;
}
@@ -547,7 +555,7 @@ export const vnode_diff = (
if (vNode.flags & VNodeFlags.Deleted) {
continue;
}
- cleanup(container, vNode);
+ cleanup(container, journal, vNode, vStartNode);
vnode_remove(journal, vParent, vNode, true);
}
vSideBuffer.clear();
@@ -585,15 +593,15 @@ export const vnode_diff = (
}
}
- function expectNoChildren() {
+ function expectNoChildren(removeDOM = true) {
const vFirstChild = vCurrent && vnode_getFirstChild(vCurrent);
if (vFirstChild !== null) {
let vChild: VNode | null = vFirstChild;
while (vChild) {
- cleanup(container, vChild);
+ cleanup(container, journal, vChild, vStartNode);
vChild = vChild.nextSibling as VNode | null;
}
- vnode_truncate(journal, vCurrent as ElementVNode | VirtualVNode, vFirstChild);
+ vnode_truncate(journal, vCurrent as ElementVNode | VirtualVNode, vFirstChild, removeDOM);
}
}
@@ -605,7 +613,7 @@ export const vnode_diff = (
const toRemove = vCurrent;
advanceToNextSibling();
if (vParent === toRemove.parent) {
- cleanup(container, toRemove);
+ cleanup(container, journal, toRemove, vStartNode);
// If we are diffing projection than the parent is not the parent of the node.
// If that is the case we don't want to remove the node from the parent.
vnode_remove(journal, vParent, toRemove, true);
@@ -616,7 +624,7 @@ export const vnode_diff = (
function expectNoMoreTextNodes() {
while (vCurrent !== null && vnode_isTextVNode(vCurrent)) {
- cleanup(container, vCurrent);
+ cleanup(container, journal, vCurrent, vStartNode);
const toRemove = vCurrent;
advanceToNextSibling();
vnode_remove(journal, vParent, toRemove, true);
@@ -667,10 +675,10 @@ export const vnode_diff = (
const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent);
if (eventName) {
- vNewNode!.setProp(HANDLER_PREFIX + ':' + scopedEvent, value);
+ vnode_setProp(vNewNode!, HANDLER_PREFIX + ':' + scopedEvent, value);
if (scope) {
// window and document need attrs so qwik loader can find them
- vNewNode!.setAttr(key, '', journal);
+ vnode_setAttr(journal, vNewNode!, key, '');
}
// register an event for qwik loader (window/document prefixed with '-')
registerQwikLoaderEvent(loaderScopedEvent);
@@ -736,7 +744,7 @@ export const vnode_diff = (
}
const key = jsx.key;
if (key) {
- (vNewNode as ElementVNode).setProp(ELEMENT_KEY, key);
+ (vNewNode as ElementVNode).key = key;
}
// append class attribute if styleScopedId exists and there is no class attribute
@@ -771,7 +779,7 @@ export const vnode_diff = (
vCurrent && vnode_isElementVNode(vCurrent) && elementName === vnode_getElementName(vCurrent);
const jsxKey: string | null = jsx.key;
let needsQDispatchEventPatch = false;
- const currentKey = getKey(vCurrent);
+ const currentKey = getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null);
if (!isSameElementName || jsxKey !== currentKey) {
const sideBufferKey = getSideBufferKey(elementName, jsxKey);
const createNew = () => (needsQDispatchEventPatch = createNewElement(jsx, elementName));
@@ -783,135 +791,83 @@ export const vnode_diff = (
// reconcile attributes
- const jsxAttrs = [] as ClientAttrs;
- const props = jsx.varProps;
- if (jsx.toSort) {
- const keys = Object.keys(props).sort();
- for (const key of keys) {
- const value = props[key];
- if (value != null) {
- jsxAttrs.push(key, value as any);
- }
- }
- } else {
- for (const key in props) {
- const value = props[key];
- if (value != null) {
- jsxAttrs.push(key, value as any);
- }
- }
- }
- if (jsxKey !== null) {
- mapArray_set(jsxAttrs, ELEMENT_KEY, jsxKey, 0);
- }
+ const jsxProps = jsx.varProps;
const vNode = (vNewNode || vCurrent) as ElementVNode;
- const element = vNode.element as QElement;
+ const element = vNode.node as QElement;
if (!element.vNode) {
element.vNode = vNode;
}
- needsQDispatchEventPatch =
- setBulkProps(vNode, jsxAttrs, (isDev && getFileLocationFromJsx(jsx.dev)) || null) ||
- needsQDispatchEventPatch;
+ if (jsxProps) {
+ needsQDispatchEventPatch =
+ diffProps(
+ vNode,
+ jsxProps,
+ (vNode.props ||= {}),
+ (isDev && getFileLocationFromJsx(jsx.dev)) || null
+ ) || needsQDispatchEventPatch;
+ }
if (needsQDispatchEventPatch) {
// Event handler needs to be patched onto the element.
if (!element.qDispatchEvent) {
element.qDispatchEvent = (event: Event, scope: QwikLoaderEventScope) => {
+ if (vNode.flags & VNodeFlags.Deleted) {
+ return;
+ }
const eventName = fromCamelToKebabCase(event.type);
const eventProp = ':' + scope.substring(1) + ':' + eventName;
const qrls = [
- vNode.getProp(eventProp, null),
- vNode.getProp(HANDLER_PREFIX + eventProp, null),
+ vnode_getProp(vNode, eventProp, null),
+ vnode_getProp(vNode, HANDLER_PREFIX + eventProp, null),
];
- let returnValue = false;
- qrls.flat(2).forEach((qrl) => {
+
+ for (const qrl of qrls.flat(2)) {
if (qrl) {
- if (isSyncQrl(qrl)) {
- qrl(event, element);
- } else {
- const value = container.$scheduler$(
- ChoreType.RUN_QRL,
- vNode,
- qrl as QRLInternal<(...args: unknown[]) => unknown>,
- [event, element]
- ) as unknown;
- returnValue = returnValue || value === true;
- }
+ safeCall(
+ () => qrl(event, element),
+ () => {},
+ (e) => {
+ container.handleError(e, vNode);
+ }
+ );
}
- });
- return returnValue;
+ }
};
}
}
}
- /** @returns True if `qDispatchEvent` needs patching */
- function setBulkProps(
+ function diffProps(
vnode: ElementVNode,
- srcAttrs: ClientAttrs,
+ newAttrs: Record,
+ oldAttrs: Record,
currentFile: string | null
): boolean {
vnode_ensureElementInflated(vnode);
- const dstAttrs = vnode_getProps(vnode) as ClientAttrs;
- let srcIdx = 0;
- let dstIdx = 0;
let patchEventDispatch = false;
- /**
- * Optimized setAttribute that bypasses redundant checks when we already know:
- *
- * - The index in dstAttrs (no need for binary search)
- * - The vnode is ElementVNode (no instanceof check)
- * - The value has changed (no comparison needed)
- */
- const setAttributeDirect = (
- vnode: ElementVNode,
- key: string,
- value: any,
- dstIdx: number,
- isNewKey: boolean
- ) => {
+ const setAttribute = (vnode: ElementVNode, key: string, value: any) => {
const serializedValue =
value != null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null;
-
- if (isNewKey) {
- // Adding new key - splice into sorted position
- if (serializedValue != null) {
- (dstAttrs as any).splice(dstIdx, 0, key, serializedValue);
- journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, serializedValue);
- }
- } else {
- // Updating or removing existing key at dstIdx
- if (serializedValue != null) {
- // Update existing value
- (dstAttrs as any)[dstIdx + 1] = serializedValue;
- journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, serializedValue);
- } else {
- // Remove key (value is null)
- dstAttrs.splice(dstIdx, 2);
- journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, null);
- }
- }
+ vnode_setAttr(journal, vnode, key, serializedValue);
};
- const record = (key: string, value: any, dstIdx: number, isNewKey: boolean) => {
+ const record = (key: string, value: any) => {
if (key.startsWith(':')) {
- vnode.setProp(key, value);
+ vnode_setProp(vnode, key, value);
return;
}
if (key === 'ref') {
- const element = vnode.element;
+ const element = vnode.node;
if (isSignal(value)) {
value.value = element;
return;
} else if (typeof value === 'function') {
value(element);
return;
- }
- // handling null value is not needed here, because we are filtering null values earlier
- else {
+ } else {
throw qError(QError.invalidRefValue, [currentFile]);
}
}
@@ -925,8 +881,6 @@ export const vnode_diff = (
return;
}
if (currentEffect) {
- // Clear current effect subscription if it exists
- // Only if we want to track the signal again
clearEffectSubscription(container, currentEffect);
}
@@ -942,29 +896,20 @@ export const vnode_diff = (
);
} else {
if (currentEffect) {
- // Clear current effect subscription if it exists
- // and the value is not a signal
- // It means that the previous value was a signal and we need to clear the effect subscription
clearEffectSubscription(container, currentEffect);
}
}
if (isPromise(value)) {
- // For async values, we can't use the known index since it will be stale by the time
- // the promise resolves. Do a binary search to find the current index.
const vHost = vnode as ElementVNode;
const attributePromise = value.then((resolvedValue) => {
- const idx = mapApp_findIndx(dstAttrs, key, 0);
- const isNewKey = idx < 0;
- const currentDstIdx = isNewKey ? idx ^ -1 : idx;
- setAttributeDirect(vHost, key, resolvedValue, currentDstIdx, isNewKey);
+ setAttribute(vHost, key, resolvedValue);
});
asyncAttributePromises.push(attributePromise);
return;
}
- // Always use optimized direct path - we know the index from the merge algorithm
- setAttributeDirect(vnode, key, value, dstIdx, isNewKey);
+ setAttribute(vnode, key, value);
};
const recordJsxEvent = (key: string, value: any) => {
@@ -973,86 +918,38 @@ export const vnode_diff = (
const [scope, eventName] = data;
const scopedEvent = getScopedEventName(scope, eventName);
const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent);
- // Pass dummy index values since ':' prefixed keys take early return via setProp
- record(':' + scopedEvent, value, 0, false);
- // register an event for qwik loader (window/document prefixed with '-')
+ record(':' + scopedEvent, value);
registerQwikLoaderEvent(loaderScopedEvent);
patchEventDispatch = true;
}
};
- // Two-pointer merge algorithm: both arrays are sorted by key
- // Note: dstAttrs mutates during iteration (setAttr uses splice), so we re-read keys each iteration
- while (srcIdx < srcAttrs.length || dstIdx < dstAttrs.length) {
- const srcKey = srcIdx < srcAttrs.length ? (srcAttrs[srcIdx] as string) : undefined;
- const dstKey = dstIdx < dstAttrs.length ? (dstAttrs[dstIdx] as string) : undefined;
+ // Actual diffing logic
+ // Apply all new attributes
+ for (const key in newAttrs) {
+ const newValue = newAttrs[key];
+ const isEvent = isHtmlAttributeAnEventName(key);
- // Skip special keys in destination HANDLER_PREFIX
- if (dstKey?.startsWith(HANDLER_PREFIX)) {
- dstIdx += 2; // skip key and value
- continue;
+ if (key in oldAttrs) {
+ if (newValue !== oldAttrs[key]) {
+ isEvent ? recordJsxEvent(key, newValue) : record(key, newValue);
+ }
+ } else if (newValue != null) {
+ isEvent ? recordJsxEvent(key, newValue) : record(key, newValue);
}
+ }
- if (srcKey === undefined) {
- // Source exhausted: remove remaining destination keys
- if (isHtmlAttributeAnEventName(dstKey!)) {
- // HTML event attributes are immutable and not removed from DOM
- dstIdx += 2; // skip key and value
- } else {
- record(dstKey!, null, dstIdx, false);
- // After removal, dstAttrs shrinks by 2, so don't advance dstIdx
- }
- } else if (dstKey === undefined) {
- // Destination exhausted: add remaining source keys
- const srcValue = srcAttrs[srcIdx + 1];
- if (isHtmlAttributeAnEventName(srcKey)) {
- recordJsxEvent(srcKey, srcValue);
- } else {
- record(srcKey, srcValue, dstIdx, true);
- }
- srcIdx += 2; // skip key and value
- // After addition, dstAttrs grows by 2 at sorted position, advance dstIdx
- dstIdx += 2;
- } else if (srcKey === dstKey) {
- // Keys match: update if values differ
- const srcValue = srcAttrs[srcIdx + 1];
- const dstValue = dstAttrs[dstIdx + 1];
- const isEventHandler = isHtmlAttributeAnEventName(srcKey);
- if (srcValue !== dstValue) {
- if (isEventHandler) {
- recordJsxEvent(srcKey, srcValue);
- } else {
- record(srcKey, srcValue, dstIdx, false);
- }
- } else if (isEventHandler && !vnode.element.qDispatchEvent) {
- // Special case: add event handlers after resume
- recordJsxEvent(srcKey, srcValue);
- }
- // Update in place doesn't change array length
- srcIdx += 2; // skip key and value
- dstIdx += 2; // skip key and value
- } else if (srcKey < dstKey) {
- // Source has a key not in destination: add it
- const srcValue = srcAttrs[srcIdx + 1];
- if (isHtmlAttributeAnEventName(srcKey)) {
- recordJsxEvent(srcKey, srcValue);
- } else {
- record(srcKey, srcValue, dstIdx, true);
- }
- srcIdx += 2; // skip key and value
- // After addition, dstAttrs grows at sorted position (before dstIdx), advance dstIdx
- dstIdx += 2;
- } else {
- // Destination has a key not in source: remove it (dstKey > srcKey)
- if (isHtmlAttributeAnEventName(dstKey)) {
- // HTML event attributes are immutable and not removed from DOM
- dstIdx += 2; // skip key and value
- } else {
- record(dstKey, null, dstIdx, false);
- // After removal, dstAttrs shrinks at dstIdx, so don't advance dstIdx
- }
+ // Remove attributes that no longer exist in new props
+ for (const key in oldAttrs) {
+ if (
+ !(key in newAttrs) &&
+ !key.startsWith(HANDLER_PREFIX) &&
+ !isHtmlAttributeAnEventName(key)
+ ) {
+ record(key, null);
}
}
+
return patchEventDispatch;
}
@@ -1075,7 +972,9 @@ export const vnode_diff = (
let vNode = vCurrent;
while (vNode) {
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
- const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
+ const vKey =
+ getKey(vNode as VirtualVNode | ElementVNode | TextVNode | null) ||
+ getComponentHash(vNode, container.$getObjectById$);
if (vNodeWithKey === null && vKey == key && name == nodeName) {
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
} else {
@@ -1115,7 +1014,9 @@ export const vnode_diff = (
if (!targetNode) {
if (vCurrent) {
const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null;
- const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$);
+ const vKey =
+ getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null) ||
+ getComponentHash(vCurrent, container.$getObjectById$);
if (vKey != null) {
const sideBufferKey = getSideBufferKey(name, vKey);
vSideBuffer ||= new Map();
@@ -1131,7 +1032,9 @@ export const vnode_diff = (
let vNode = vCurrent;
while (vNode && vNode !== targetNode) {
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
- const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
+ const vKey =
+ getKey(vNode as VirtualVNode | ElementVNode | TextVNode | null) ||
+ getComponentHash(vNode, container.$getObjectById$);
if (vKey != null) {
const sideBufferKey = getSideBufferKey(name, vKey);
@@ -1197,7 +1100,8 @@ export const vnode_diff = (
vSideBuffer!.delete(sideBufferKey);
if (addCurrentToSideBufferOnSideInsert && vCurrent) {
const currentKey =
- getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$);
+ getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null) ||
+ getComponentHash(vCurrent, container.$getObjectById$);
if (currentKey != null) {
const currentName = vnode_isElementVNode(vCurrent)
? vnode_getElementName(vCurrent)
@@ -1209,7 +1113,12 @@ export const vnode_diff = (
}
}
}
- vnode_insertBefore(journal, parentForInsert as any, buffered, vCurrent);
+ vnode_insertBefore(
+ journal,
+ parentForInsert as ElementVNode | VirtualVNode,
+ buffered,
+ vCurrent
+ );
vCurrent = buffered;
vNewNode = null;
return;
@@ -1222,7 +1131,7 @@ export const vnode_diff = (
function expectVirtual(type: VirtualType, jsxKey: string | null) {
const checkKey = type === VirtualType.Fragment;
- const currentKey = getKey(vCurrent);
+ const currentKey = getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null);
const currentIsVirtual = vCurrent && vnode_isVirtualVNode(vCurrent);
const isSameNode = currentIsVirtual && currentKey === jsxKey && (checkKey ? !!jsxKey : true);
@@ -1239,8 +1148,8 @@ export const vnode_diff = (
(vNewNode = vnode_newVirtual()),
vCurrent && getInsertBefore()
);
- (vNewNode as VirtualVNode).setProp(ELEMENT_KEY, jsxKey);
- isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, type);
+ (vNewNode as VirtualVNode).key = jsxKey;
+ isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, type);
};
// For fragments without a key, always create a new virtual node (ensures rerender semantics)
if (jsxKey === null) {
@@ -1293,7 +1202,8 @@ export const vnode_diff = (
}
if (host) {
- const vNodeProps = (host as VirtualVNode).getProp(
+ const vNodeProps = vnode_getProp(
+ host as VirtualVNode,
ELEMENT_PROPS,
container.$getObjectById$
);
@@ -1304,7 +1214,7 @@ export const vnode_diff = (
if (shouldRender) {
// Assign the new QRL instance to the host.
// Unfortunately it is created every time, something to fix in the optimizer.
- (host as VirtualVNode).setProp(OnRenderProp, componentQRL);
+ vnode_setProp(host as VirtualVNode, OnRenderProp, componentQRL);
/**
* Mark host as not deleted. The host could have been marked as deleted if it there was a
@@ -1312,7 +1222,7 @@ export const vnode_diff = (
* deleted.
*/
(host as VirtualVNode).flags &= ~VNodeFlags.Deleted;
- container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, vNodeProps);
+ markVNodeDirty(container, host as VirtualVNode, ChoreBits.COMPONENT, vStartNode);
}
}
descendContentToProject(jsxNode.children, host);
@@ -1344,7 +1254,8 @@ export const vnode_diff = (
while (
componentHost &&
(vnode_isVirtualVNode(componentHost)
- ? (componentHost as VirtualVNode).getProp | null>(
+ ? vnode_getProp | null>(
+ componentHost as VirtualVNode,
OnRenderProp,
null
) === null
@@ -1381,10 +1292,10 @@ export const vnode_diff = (
vCurrent && getInsertBefore()
);
const jsxNode = jsxValue as JSXNodeInternal;
- isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Component);
- container.setHostProp(vNewNode, OnRenderProp, componentQRL);
- container.setHostProp(vNewNode, ELEMENT_PROPS, jsxProps);
- container.setHostProp(vNewNode, ELEMENT_KEY, jsxNode.key);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Component);
+ vnode_setProp(vNewNode as VirtualVNode, OnRenderProp, componentQRL);
+ vnode_setProp(vNewNode as VirtualVNode, ELEMENT_PROPS, jsxProps);
+ (vNewNode as VirtualVNode).key = jsxNode.key;
}
function insertNewInlineComponent() {
@@ -1395,10 +1306,10 @@ export const vnode_diff = (
vCurrent && getInsertBefore()
);
const jsxNode = jsxValue as JSXNodeInternal;
- isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.InlineComponent);
- (vNewNode as VirtualVNode).setProp(ELEMENT_PROPS, jsxNode.props);
+ isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.InlineComponent);
+ vnode_setProp(vNewNode as VirtualVNode, ELEMENT_PROPS, jsxNode.props);
if (jsxNode.key) {
- (vNewNode as VirtualVNode).setProp(ELEMENT_KEY, jsxNode.key);
+ (vNewNode as VirtualVNode).key = jsxNode.key;
}
}
@@ -1428,11 +1339,11 @@ export const vnode_diff = (
* @param vNode - VNode to retrieve the key from
* @returns Key
*/
-function getKey(vNode: VNode | null): string | null {
- if (vNode == null) {
+function getKey(vNode: VirtualVNode | ElementVNode | TextVNode | null): string | null {
+ if (vNode == null || vnode_isTextVNode(vNode)) {
return null;
}
- return (vNode as VirtualVNode).getProp(ELEMENT_KEY, null);
+ return vNode.key;
}
/**
@@ -1443,10 +1354,10 @@ function getKey(vNode: VNode | null): string | null {
* @returns Hash
*/
function getComponentHash(vNode: VNode | null, getObject: (id: string) => any): string | null {
- if (vNode == null) {
+ if (vNode == null || vnode_isTextVNode(vNode)) {
return null;
}
- const qrl = (vNode as VirtualVNode).getProp(OnRenderProp, getObject);
+ const qrl = vnode_getProp(vNode as VirtualVNode, OnRenderProp, getObject);
return qrl ? qrl.$hash$ : null;
}
@@ -1484,7 +1395,6 @@ function handleProps(
container: ClientContainer
): boolean {
let shouldRender = false;
- let propsAreDifferent = false;
if (vNodeProps) {
const effects = vNodeProps[_PROPS_HANDLER].$effects$;
const constPropsDifferent = handleChangedProps(
@@ -1494,33 +1404,24 @@ function handleProps(
container,
false
);
- propsAreDifferent = constPropsDifferent;
shouldRender ||= constPropsDifferent;
if (effects && effects.size > 0) {
- const varPropsDifferent = handleChangedProps(
+ handleChangedProps(
jsxProps[_VAR_PROPS],
vNodeProps[_VAR_PROPS],
vNodeProps[_PROPS_HANDLER],
- container
+ container,
+ true
);
-
- propsAreDifferent ||= varPropsDifferent;
// don't mark as should render, effects will take care of it
- // shouldRender ||= varPropsDifferent;
- }
- }
-
- if (propsAreDifferent) {
- if (vNodeProps) {
- // Reuse the same props instance, qrls can use the current props instance
- // as a capture ref, so we can't change it.
- vNodeProps[_OWNER] = (jsxProps as PropsProxy)[_OWNER];
- } else if (jsxProps) {
- // If there is no props instance, create a new one.
- // We can do this because we are not using the props instance for anything else.
- (host as VirtualVNode).setProp(ELEMENT_PROPS, jsxProps);
- vNodeProps = jsxProps;
}
+ // Update the owner after all props have been synced
+ vNodeProps[_OWNER] = (jsxProps as PropsProxy)[_OWNER];
+ } else if (jsxProps) {
+ // If there is no props instance, create a new one.
+ // We can do this because we are not using the props instance for anything else.
+ vnode_setProp(host as VirtualVNode, ELEMENT_PROPS, jsxProps);
+ vNodeProps = jsxProps;
}
return shouldRender;
}
@@ -1532,51 +1433,49 @@ function handleChangedProps(
container: ClientContainer,
triggerEffects: boolean = true
): boolean {
- const srcEmpty = isPropsEmpty(src);
- const dstEmpty = isPropsEmpty(dst);
-
- if (srcEmpty && dstEmpty) {
+ if (isPropsEmpty(src) && isPropsEmpty(dst)) {
return false;
}
- if (srcEmpty || dstEmpty) {
- return true;
- }
-
- const srcKeys = Object.keys(src!);
- const dstKeys = Object.keys(dst!);
-
- let srcLen = srcKeys.length;
- let dstLen = dstKeys.length;
- if ('children' in src!) {
- srcLen--;
- }
- if (QBackRefs in src!) {
- srcLen--;
- }
- if ('children' in dst!) {
- dstLen--;
- }
- if (QBackRefs in dst!) {
- dstLen--;
- }
+ propsHandler.$container$ = container;
+ let changed = false;
- if (srcLen !== dstLen) {
- return true;
+ // Update changed/added props from src
+ if (src) {
+ for (const key in src) {
+ if (key === 'children' || key === QBackRefs) {
+ continue;
+ }
+ if (!dst || src[key] !== dst[key]) {
+ changed = true;
+ if (triggerEffects) {
+ if (dst) {
+ // Update the value in dst BEFORE triggering effects
+ // so effects see the new value
+ // Note: Value is not triggering effects, because we are modyfing direct VAR_PROPS object
+ dst[key] = src[key];
+ }
+ triggerPropsProxyEffect(propsHandler, key);
+ } else {
+ // Early return for const props (no effects)
+ return true;
+ }
+ }
+ }
}
- let changed = false;
- propsHandler.$container$ = container;
- for (const key of srcKeys) {
- if (key === 'children' || key === QBackRefs) {
- continue;
- }
- if (!Object.prototype.hasOwnProperty.call(dst, key) || src![key] !== dst![key]) {
- changed = true;
- if (triggerEffects) {
- triggerPropsProxyEffect(propsHandler, key);
- } else {
- return true;
+ // Remove props that are in dst but not in src
+ if (dst) {
+ for (const key in dst) {
+ if (key === 'children' || key === QBackRefs) {
+ continue;
+ }
+ if (!src || !(key in src)) {
+ changed = true;
+ if (triggerEffects) {
+ delete dst[key];
+ triggerPropsProxyEffect(propsHandler, key);
+ }
}
}
}
@@ -1600,8 +1499,15 @@ function isPropsEmpty(props: Record | null | undefined): boolean {
*
* - Projection nodes by not recursing into them.
* - Component nodes by recursing into the component content nodes (which may be projected).
+ *
+ * @param cursorRoot - Optional cursor root (vStartNode) to propagate dirty bits to during diff.
*/
-export function cleanup(container: ClientContainer, vNode: VNode) {
+export function cleanup(
+ container: ClientContainer,
+ journal: VNodeJournal,
+ vNode: VNode,
+ cursorRoot: VNode | null = null
+) {
let vCursor: VNode | null = vNode;
// Depth first traversal
if (vnode_isTextVNode(vNode)) {
@@ -1618,7 +1524,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
const isComponent =
type & VNodeFlags.Virtual &&
- (vCursor as VirtualVNode).getProp | null>(OnRenderProp, null) !== null;
+ vnode_getProp | null>(vCursor as VirtualVNode, OnRenderProp, null) !== null;
if (isComponent) {
// cleanup q:seq content
const seq = container.getHostProp>(vCursor as VirtualVNode, ELEMENT_SEQ);
@@ -1628,7 +1534,9 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
if (isObject(obj)) {
const objIsTask = isTask(obj);
if (objIsTask && obj.$flags$ & TaskFlags.VISIBLE_TASK) {
- container.$scheduler$(ChoreType.CLEANUP_VISIBLE, obj);
+ obj.$flags$ |= TaskFlags.NEEDS_CLEANUP;
+ markVNodeDirty(container, vCursor, ChoreBits.CLEANUP, cursorRoot);
+
// don't call cleanupDestroyable yet, do it by the scheduler
continue;
} else if (obj instanceof SignalImpl || isStore(obj)) {
@@ -1643,24 +1551,25 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
}
// SPECIAL CASE: If we are a component, we need to descend into the projected content and release the content.
- const attrs = vnode_getProps(vCursor as VirtualVNode);
- for (let i = 0; i < attrs.length; i = i + 2) {
- const key = attrs[i] as string;
- if (isSlotProp(key)) {
- const value = attrs[i + 1];
- if (value) {
- attrs[i + 1] = null; // prevent infinite loop
- const projection =
- typeof value === 'string'
- ? vnode_locate(container.rootVNode, value)
- : (value as unknown as VNode);
- let projectionChild = vnode_getFirstChild(projection);
- while (projectionChild) {
- cleanup(container, projectionChild);
- projectionChild = projectionChild.nextSibling as VNode | null;
- }
+ const attrs = (vCursor as VirtualVNode).props;
+ if (attrs) {
+ for (const key of Object.keys(attrs)) {
+ if (isSlotProp(key)) {
+ const value = attrs[key];
+ if (value) {
+ attrs[key] = null; // prevent infinite loop
+ const projection =
+ typeof value === 'string'
+ ? vnode_locate(container.rootVNode, value)
+ : (value as unknown as VNode);
+ let projectionChild = vnode_getFirstChild(projection);
+ while (projectionChild) {
+ cleanup(container, journal, projectionChild, cursorRoot);
+ projectionChild = projectionChild.nextSibling as VNode | null;
+ }
- cleanupStaleUnclaimedProjection(container.$journal$, projection);
+ cleanupStaleUnclaimedProjection(journal, projection);
+ }
}
}
}
@@ -1675,7 +1584,9 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
vCursor = vFirstChild;
continue;
}
- } else if (vCursor === vNode) {
+ }
+ // TODO: probably can be removed
+ else if (vCursor === vNode) {
/**
* If it is a projection and we are at the root, then we should only walk the children to
* materialize the projection content. This is because we could have references in the vnode
diff --git a/packages/qwik/src/core/client/vnode-diff.unit.tsx b/packages/qwik/src/core/client/vnode-diff.unit.tsx
index f4636c4e051..99e54b66e4d 100644
--- a/packages/qwik/src/core/client/vnode-diff.unit.tsx
+++ b/packages/qwik/src/core/client/vnode-diff.unit.tsx
@@ -1,4 +1,5 @@
import {
+ $,
Fragment,
_fnSignal,
_jsxSorted,
@@ -20,72 +21,79 @@ import type { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-sign
import { createSignal } from '../reactive-primitives/signal-api';
import { StoreFlags } from '../reactive-primitives/types';
import { QError, qError } from '../shared/error/error';
-import type { Scheduler } from '../shared/scheduler';
import type { QElement } from '../shared/types';
-import { ChoreType } from '../shared/util-chore-type';
import { VNodeFlags } from './types';
-import { vnode_applyJournal, vnode_getFirstChild, vnode_getNode } from './vnode';
+import {
+ vnode_getFirstChild,
+ vnode_getNode,
+ vnode_setProp,
+ type VNodeJournal,
+} from './vnode-utils';
import { vnode_diff } from './vnode-diff';
-import type { VirtualVNode } from './vnode-impl';
-
-async function waitForDrain(scheduler: Scheduler) {
- await scheduler(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
-}
+import { _flushJournal } from '../shared/cursor/cursor-flush';
+import { markVNodeDirty } from '../shared/vnode/vnode-dirty';
+import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum';
+import { NODE_DIFF_DATA_KEY } from '../shared/cursor/cursor-props';
describe('vNode-diff', () => {
it('should find no difference', () => {
const { vNode, vParent, container } = vnode_fromJSX( Hello );
expect(vNode).toMatchVDOM(Hello );
- expect(vnode_getNode(vNode!)!.ownerDocument!.body.innerHTML).toEqual(
- 'Hello '
- );
- vnode_diff(container, Hello , vParent, null);
- expect(container.$journal$.length).toEqual(0);
+ expect(vnode_getNode(vNode!)!.ownerDocument!.body.innerHTML).toEqual('Hello ');
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, Hello , vParent, null);
+ expect(journal.length).toEqual(0);
});
describe('text', () => {
it('should update text', () => {
const { vNode, vParent, container } = vnode_fromJSX(Hello );
- vnode_diff(container, World , vParent, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, World , vParent, null);
expect(vNode).toMatchVDOM(World );
- expect(container.$journal$).not.toEqual([]);
- expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('Hello ');
- vnode_applyJournal(container.$journal$);
- expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('World ');
+ expect(journal).not.toEqual([]);
+ expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('Hello ');
+ _flushJournal(journal);
+ expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('World ');
});
it('should add missing text node', () => {
const { vNode, vParent, container } = vnode_fromJSX();
- vnode_diff(container, Hello , vParent, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, Hello , vParent, null);
expect(vNode).toMatchVDOM(Hello );
- expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('');
- vnode_applyJournal(container.$journal$);
- expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('Hello ');
+ expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('');
+ _flushJournal(journal);
+ expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('Hello ');
});
it('should update and add missing text node', () => {
const { vNode, vParent, container } = vnode_fromJSX(text );
- vnode_diff(container, Hello {'World'} , vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, Hello {'World'} , vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(Hello {'World'} );
});
it('should remove extra text nodes', () => {
const { vNode, vParent, container } = vnode_fromJSX(text{'removeMe'} );
- vnode_diff(container, Hello , vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, Hello , vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(Hello );
});
it('should remove all text nodes', () => {
const { vNode, vParent, container } = vnode_fromJSX(text{'removeMe'} );
- vnode_diff(container, , vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, , vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM();
});
it('should treat undefined as no children', () => {
const { vNode, vParent, container } = vnode_fromJSX(text{'removeMe'} );
- vnode_diff(container, {undefined} , vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, {undefined} , vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM();
});
});
@@ -103,9 +111,10 @@ describe('vNode-diff', () => {
);
- vnode_diff(container, test, vParent, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
expect(vNode).toMatchVDOM(test);
- expect(container.$journal$.length).toEqual(0);
+ expect(journal.length).toEqual(0);
});
it('should add missing element', () => {
const { vNode, vParent, container } = vnode_fromJSX();
@@ -114,8 +123,9 @@ describe('vNode-diff', () => {
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
it('should remove extra text node', async () => {
@@ -131,8 +141,9 @@ describe('vNode-diff', () => {
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
await expect(container.document.querySelector('test')).toMatchDOM(test);
});
@@ -166,8 +177,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -196,8 +208,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -226,8 +239,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -256,8 +270,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -277,8 +292,9 @@ describe('vNode-diff', () => {
)
);
const test = _jsxSorted('span', {}, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -307,8 +323,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -337,8 +354,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -367,8 +385,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
@@ -388,8 +407,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
});
@@ -397,20 +417,28 @@ describe('vNode-diff', () => {
describe('keys', () => {
it('should not reuse element because old has a key and new one does not', () => {
const { vNode, vParent, container } = vnode_fromJSX(
- _jsxSorted('test', {}, null, [_jsxSorted('b', {}, null, 'old', 0, '1')], 0, 'KA_6')
+ _jsxSorted(
+ 'test',
+ {},
+ null,
+ [_jsxSorted('b', { id: 'b1' }, null, 'old', 0, '1')],
+ 0,
+ 'KA_6'
+ )
);
const test = _jsxSorted(
'test',
{},
null,
- [_jsxSorted('b', {}, null, 'new', 0, null)],
+ [_jsxSorted('b', { id: 'b1' }, null, 'new', 0, null)],
0,
'KA_6'
);
- const bOriginal = container.document.querySelector('b[q\\:key=1]')!;
+ const bOriginal = container.document.querySelector('#b1')!;
expect(bOriginal).toBeDefined();
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
const bSecond = container.document.querySelector('b')!;
expect(bSecond).toBeDefined();
@@ -422,7 +450,10 @@ describe('vNode-diff', () => {
'test',
{},
null,
- [_jsxSorted('b', {}, null, '1', 0, '1'), _jsxSorted('b', {}, null, '2', 0, '2')],
+ [
+ _jsxSorted('b', { id: 'b1' }, null, '1', 0, '1'),
+ _jsxSorted('b', { id: 'b2' }, null, '2', 0, '2'),
+ ],
0,
'KA_6'
)
@@ -433,23 +464,24 @@ describe('vNode-diff', () => {
null,
[
_jsxSorted('b', {}, null, 'before', 0, null),
- _jsxSorted('b', {}, null, '2', 0, '2'),
+ _jsxSorted('b', { id: 'b2' }, null, '2', 0, '2'),
_jsxSorted('b', {}, null, '3', 0, '3'),
_jsxSorted('b', {}, null, 'in', 0, null),
- _jsxSorted('b', {}, null, '1', 0, '1'),
+ _jsxSorted('b', { id: 'b1' }, null, '1', 0, '1'),
_jsxSorted('b', {}, null, 'after', 0, null),
],
0,
'KA_6'
);
- const selectB1 = () => container.document.querySelector('b[q\\:key=1]')!;
- const selectB2 = () => container.document.querySelector('b[q\\:key=2]')!;
+ const selectB1 = () => container.document.querySelector('b#b1')!;
+ const selectB2 = () => container.document.querySelector('b#b2')!;
const b1 = selectB1();
const b2 = selectB2();
expect(b1).toBeDefined();
expect(b2).toBeDefined();
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
expect(b1).toBe(selectB1());
expect(b2).toBe(selectB2());
@@ -474,8 +506,9 @@ describe('vNode-diff', () => {
0,
'KA_6'
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
});
});
@@ -494,11 +527,12 @@ describe('vNode-diff', () => {
null
);
- const signalFragment = vnode_getFirstChild(vNode!) as VirtualVNode;
+ const signalFragment = vnode_getFirstChild(vNode!);
expect(signalFragment).toBeDefined();
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(expected);
expect(signalFragment).toBe(vnode_getFirstChild(vNode!));
});
@@ -517,11 +551,12 @@ describe('vNode-diff', () => {
null
);
- const promiseFragment = vnode_getFirstChild(vNode!) as VirtualVNode;
+ const promiseFragment = vnode_getFirstChild(vNode!);
expect(promiseFragment).toBeDefined();
- await vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ await vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(expected);
expect(promiseFragment).toBe(vnode_getFirstChild(vNode!));
});
@@ -539,11 +574,12 @@ describe('vNode-diff', () => {
null
);
- const fragment = vnode_getFirstChild(vNode!) as VirtualVNode;
+ const fragment = vnode_getFirstChild(vNode!);
expect(fragment).toBeDefined();
- await vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vNode).toMatchVDOM(test);
expect(fragment).not.toBe(vnode_getFirstChild(vNode!));
});
@@ -552,8 +588,9 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX('1');
const test = Promise.resolve('2') as unknown as JSXOutput; //_jsxSorted(Fragment, {}, null, ['1'], 0, null);
- await vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ await vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vParent).toMatchVDOM(
2
@@ -566,8 +603,9 @@ describe('vNode-diff', () => {
it('should set attributes', async () => {
const { vParent, container } = vnode_fromJSX();
const test = _jsxSorted('span', {}, { class: 'abcd', id: 'b' }, null, 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
const firstChildNode = vnode_getNode(firstChild) as Element;
await expect(firstChildNode).toMatchDOM(test);
@@ -590,8 +628,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -610,8 +649,9 @@ describe('vNode-diff', () => {
)
);
const test = _jsxSorted('span', {}, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -640,8 +680,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -660,8 +701,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM();
});
@@ -680,18 +722,20 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM();
});
- it('should add qDispatchEvent for existing html event attribute', () => {
+ it('should not add qDispatchEvent for removed html event attribute', () => {
const { vParent, container } = vnode_fromJSX(
_jsxSorted('span', { id: 'a', 'on:click': 'abcd' }, null, [], 0, null)
);
const test = _jsxSorted('span', { id: 'a' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -702,9 +746,10 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(
_jsxSorted('span', { id: 'a', 'on:click': 'abcd' }, null, [], 0, null)
);
- const test = _jsxSorted('span', { id: 'a', onClick$: 'abcd' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const test = _jsxSorted('span', { id: 'a', onClick$: $(() => {}) }, null, [], 0, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -716,8 +761,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { id: 'a' }, null, [], 0, null)
);
const test = _jsxSorted('span', { id: 'a', onClick$: () => null }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -729,8 +775,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { id: 'a', 'on:click': 'abcd' }, null, [], 0, null)
);
const test = _jsxSorted('span', { id: 'a', onClick$: () => null }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -742,8 +789,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { id: 'aaa' }, null, [], 0, null)
);
const test = _jsxSorted('span', { name: 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
});
@@ -753,8 +801,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { id: 'aaa' }, null, [], 0, null)
);
const test = _jsxSorted('span', { name: 'bbb', title: 'ccc' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
});
@@ -764,8 +813,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { id: 'aaa', title: 'ccc' }, null, [], 0, null)
);
const test = _jsxSorted('span', { name: 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
});
@@ -775,8 +825,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { name: 'aaa' }, null, [], 0, null)
);
const test = _jsxSorted('span', { id: 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
});
@@ -786,8 +837,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { name: 'aaa' }, null, [], 0, null)
);
const test = _jsxSorted('span', { id: 'bbb', title: 'ccc' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
});
@@ -797,8 +849,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { name: 'aaa', title: 'ccc' }, null, [], 0, null)
);
const test = _jsxSorted('span', { id: 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
});
@@ -808,8 +861,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { onDblClick$: 'aaa' }, null, [], 0, null)
);
const test = _jsxSorted('span', { onClick$: 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -821,8 +875,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { onClick$: 'aaa' }, null, [], 0, null)
);
const test = _jsxSorted('span', { onDblClick$: 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -832,8 +887,9 @@ describe('vNode-diff', () => {
it('should add event scope to element add qDispatchEvent', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const test = _jsxSorted('span', { 'window:onClick$': 'bbb' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
const element = vnode_getNode(firstChild) as QElement;
@@ -845,8 +901,9 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const signal = createSignal();
const test = _jsxSorted('span', { ref: signal }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
expect(signal.value).toBe(vnode_getNode(firstChild));
@@ -863,8 +920,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
expect((globalThis as any).node).toBe(vnode_getNode(firstChild));
@@ -874,8 +932,9 @@ describe('vNode-diff', () => {
it('should handle null ref value attribute', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const test = _jsxSorted('span', { ref: null }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
});
@@ -884,7 +943,8 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const test = _jsxSorted('span', { ref: 'abc' }, null, [], 0, null);
expect(() => {
- vnode_diff(container, test, vParent, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
}).toThrowError(qError(QError.invalidRefValue, [null]));
});
});
@@ -894,8 +954,9 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const signal = createSignal('test');
const test = _jsxSorted('span', { class: signal }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
});
@@ -905,8 +966,9 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const signal = createSignal('initial') as SignalImpl;
const test1 = _jsxSorted('span', { class: signal }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -916,9 +978,9 @@ describe('vNode-diff', () => {
// Replace signal with regular string value
const test2 = _jsxSorted('span', { class: 'static' }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify effects have been cleaned up
@@ -929,8 +991,9 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const signal1 = createSignal('first') as SignalImpl;
const test1 = _jsxSorted('span', { class: signal1 }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -941,9 +1004,9 @@ describe('vNode-diff', () => {
// Replace with another signal
const signal2 = createSignal('second') as SignalImpl;
const test2 = _jsxSorted('span', { class: signal2 }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify first signal's effects have been cleaned up
@@ -957,8 +1020,9 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const signal = createSignal('test') as SignalImpl;
const test1 = _jsxSorted('span', { class: signal }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -968,9 +1032,9 @@ describe('vNode-diff', () => {
// Remove the attribute entirely
const test2 = _jsxSorted('span', {}, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify effects have been cleaned up
@@ -988,8 +1052,9 @@ describe('vNode-diff', () => {
'() => inner.value'
) as WrappedSignalImpl;
const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -1002,9 +1067,9 @@ describe('vNode-diff', () => {
// Replace wrapped signal with regular string value
const test2 = _jsxSorted('span', { class: 'static' }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify inner signal's effects have been cleaned up
@@ -1022,8 +1087,9 @@ describe('vNode-diff', () => {
'() => inner1.value'
) as WrappedSignalImpl;
const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -1042,9 +1108,9 @@ describe('vNode-diff', () => {
'() => inner2.value'
) as WrappedSignalImpl;
const test2 = _jsxSorted('span', { class: wrapped2 }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify first inner signal's effects have been cleaned up
@@ -1068,8 +1134,9 @@ describe('vNode-diff', () => {
'() => inner.value'
) as WrappedSignalImpl;
const test1 = _jsxSorted('span', { class: wrapped }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -1082,9 +1149,9 @@ describe('vNode-diff', () => {
// Remove the attribute entirely
const test2 = _jsxSorted('span', {}, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify effects have been cleaned up
@@ -1103,8 +1170,9 @@ describe('vNode-diff', () => {
'() => store.cls'
) as WrappedSignalImpl;
const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -1116,9 +1184,9 @@ describe('vNode-diff', () => {
// Replace wrapped signal with regular string value
const test2 = _jsxSorted('span', { class: 'static' }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify store's effects have been cleaned up
@@ -1136,8 +1204,9 @@ describe('vNode-diff', () => {
'() => store1.cls'
) as WrappedSignalImpl;
const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -1154,9 +1223,9 @@ describe('vNode-diff', () => {
'() => store2.cls'
) as WrappedSignalImpl;
const test2 = _jsxSorted('span', { class: wrapped2 }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify first store/ wrapped effects have been cleaned up
@@ -1177,8 +1246,9 @@ describe('vNode-diff', () => {
'() => store.cls'
) as WrappedSignalImpl;
const test1 = _jsxSorted('span', { class: wrapped }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
@@ -1189,9 +1259,9 @@ describe('vNode-diff', () => {
// Remove the attribute entirely
const test2 = _jsxSorted('span', {}, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
// Verify effects have been cleaned up
@@ -1224,9 +1294,11 @@ describe('vNode-diff', () => {
3,
null
) as any;
- vnode_diff(container, test1, vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_setProp(vParent, NODE_DIFF_DATA_KEY, test1);
+ markVNodeDirty(container, vParent, ChoreBits.NODE_DIFF);
+ _flushJournal(journal);
+ await container.$renderPromise$;
// Ensure one subscription exists for both wrapped and inner
expect(wrapped.$effects$).not.toBeNull();
@@ -1245,10 +1317,11 @@ describe('vNode-diff', () => {
3,
null
) as any;
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_setProp(vParent, NODE_DIFF_DATA_KEY, test2);
+ markVNodeDirty(container, vParent, ChoreBits.NODE_DIFF);
+ _flushJournal(journal2);
+ await container.$renderPromise$;
// The number of effects should not increase (no duplicate subscriptions)
expect(wrapped.$effects$!.size).toBe(wrappedEffectsAfterFirst);
@@ -1268,8 +1341,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1285,8 +1359,9 @@ describe('vNode-diff', () => {
)
);
const test = _jsxSorted('span', {}, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1302,8 +1377,9 @@ describe('vNode-diff', () => {
)
);
const test = _jsxSorted('span', { class: 'test', id: 'b' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1319,8 +1395,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1336,8 +1413,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1346,8 +1424,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { a: '1', b: '2', c: '3', z: '26' }, null, [], 0, null)
);
const test = _jsxSorted('span', { z: '26' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1356,8 +1435,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { z: '26' }, null, [], 0, null)
);
const test = _jsxSorted('span', { a: '1', b: '2', c: '3', z: '26' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1366,8 +1446,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { b: '2', d: '4', f: '6' }, null, [], 0, null)
);
const test = _jsxSorted('span', { a: '1', c: '3', e: '5' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1390,8 +1471,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
const element = vnode_getNode(firstChild) as QElement;
@@ -1410,8 +1492,9 @@ describe('vNode-diff', () => {
)
);
const test = _jsxSorted('span', { id: 'test' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM(test);
const element = vnode_getNode(firstChild) as QElement;
@@ -1430,8 +1513,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
const element = vnode_getNode(firstChild) as QElement;
expect(element.qDispatchEvent).toBeDefined();
@@ -1442,8 +1526,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null)
);
const test = _jsxSorted('span', { a: '10', b: '20', c: '30' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1452,9 +1537,10 @@ describe('vNode-diff', () => {
_jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null)
);
const test = _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
// Journal should be empty since no changes were made
- expect(container.$journal$.length).toEqual(0);
+ expect(journal.length).toEqual(0);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
@@ -1464,8 +1550,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null)
);
const test1 = _jsxSorted('span', { a: 'NEW', b: '2', c: '3' }, null, [], 0, null);
- vnode_diff(container1, test1, vParent1, null);
- vnode_applyJournal(container1.$journal$);
+ const journal1: VNodeJournal = [];
+ vnode_diff(container1, journal1, test1, vParent1, null);
+ _flushJournal(journal1);
expect(vnode_getFirstChild(vParent1)).toMatchVDOM(test1);
// Change middle
@@ -1473,8 +1560,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null)
);
const test2 = _jsxSorted('span', { a: '1', b: 'NEW', c: '3' }, null, [], 0, null);
- vnode_diff(container2, test2, vParent2, null);
- vnode_applyJournal(container2.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container2, journal2, test2, vParent2, null);
+ _flushJournal(journal2);
expect(vnode_getFirstChild(vParent2)).toMatchVDOM(test2);
// Change last
@@ -1482,8 +1570,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null)
);
const test3 = _jsxSorted('span', { a: '1', b: '2', c: 'NEW' }, null, [], 0, null);
- vnode_diff(container3, test3, vParent3, null);
- vnode_applyJournal(container3.$journal$);
+ const journal3: VNodeJournal = [];
+ vnode_diff(container3, journal3, test3, vParent3, null);
+ _flushJournal(journal3);
expect(vnode_getFirstChild(vParent3)).toMatchVDOM(test3);
});
@@ -1497,8 +1586,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
const element = vnode_getNode(firstChild) as QElement;
@@ -1534,8 +1624,9 @@ describe('vNode-diff', () => {
0,
null
);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
const element = vnode_getNode(firstChild) as QElement;
@@ -1546,17 +1637,18 @@ describe('vNode-diff', () => {
const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null));
const signal1 = createSignal('value1');
const test1 = _jsxSorted('span', { class: signal1 }, null, [], 0, null);
- vnode_diff(container, test1, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test1, vParent, null);
+ _flushJournal(journal);
const firstChild = vnode_getFirstChild(vParent);
expect(firstChild).toMatchVDOM();
// Update with different signal
const signal2 = createSignal('value2');
const test2 = _jsxSorted('span', { class: signal2 }, null, [], 0, null);
- container.$journal$ = [];
- vnode_diff(container, test2, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, test2, vParent, null);
+ _flushJournal(journal2);
expect(firstChild).toMatchVDOM();
});
@@ -1573,8 +1665,9 @@ describe('vNode-diff', () => {
_jsxSorted('span', manyAttrs, null, [], 0, null)
);
const test = _jsxSorted('span', manyAttrsUpdated, null, [], 0, null);
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(vnode_getFirstChild(vParent)).toMatchVDOM(test);
});
});
@@ -1587,11 +1680,12 @@ describe('vNode-diff', () => {
vParent.flags |= VNodeFlags.Deleted;
- vnode_diff(container, World , vParent, null);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, World , vParent, null);
- expect(container.$journal$.length).toEqual(0);
+ expect(journal.length).toEqual(0);
- expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('Hello ');
+ expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('Hello ');
});
});
@@ -1601,14 +1695,16 @@ describe('vNode-diff', () => {
const signal = createSignal('test') as SignalImpl;
const test = _jsxSorted('div', { class: signal }, null, [], 0, 'KA_0');
- vnode_diff(container, test, vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_diff(container, journal, test, vParent, null);
+ _flushJournal(journal);
expect(signal.$effects$).toBeDefined();
expect(signal.$effects$!.size).toBeGreaterThan(0);
- vnode_diff(container, _jsxSorted('div', {}, null, [], 0, 'KA_0'), vParent, null);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ vnode_diff(container, journal2, _jsxSorted('div', {}, null, [], 0, 'KA_0'), vParent, null);
+ _flushJournal(journal2);
expect(signal.$effects$!.size).toBe(0);
});
@@ -1623,16 +1719,19 @@ describe('vNode-diff', () => {
});
const test1 = _jsxSorted(Child, null, { value: signal }, null, 3, null) as JSXChildren;
- await vnode_diff(container, test1, vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_setProp(vParent, NODE_DIFF_DATA_KEY, test1);
+ markVNodeDirty(container, vParent, ChoreBits.NODE_DIFF);
+ _flushJournal(journal);
+ await container.$renderPromise$;
expect(signal.$effects$).toBeDefined();
expect(signal.$effects$!.size).toBeGreaterThan(0);
- await vnode_diff(container, , vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ await vnode_diff(container, journal2, , vParent, null);
+ _flushJournal(journal2);
+ await container.$renderPromise$;
expect(signal.$effects$!.size).toBe(0);
});
@@ -1648,16 +1747,20 @@ describe('vNode-diff', () => {
});
const test = _jsxSorted(Child as unknown as any, null, null, null, 3, null) as any;
- vnode_diff(container, test, vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+
+ vnode_setProp(vParent, NODE_DIFF_DATA_KEY, test);
+ markVNodeDirty(container, vParent, ChoreBits.NODE_DIFF);
+ await container.$renderPromise$;
+ _flushJournal(journal);
expect((globalThis as any).innerSignal.$effects$).toBeDefined();
expect((globalThis as any).innerSignal.$effects$!.size).toBeGreaterThan(0);
- vnode_diff(container, , vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal2: VNodeJournal = [];
+ await vnode_diff(container, journal2, , vParent, null);
+ await container.$renderPromise$;
+ _flushJournal(journal2);
expect((globalThis as any).innerSignal.$effects$.size).toBe(0);
});
@@ -1673,19 +1776,20 @@ describe('vNode-diff', () => {
});
const test = _jsxSorted(Child as unknown as any, null, null, null, 3, null) as any;
- vnode_diff(container, test, vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
+ const journal: VNodeJournal = [];
+ vnode_setProp(vParent, NODE_DIFF_DATA_KEY, test);
+ markVNodeDirty(container, vParent, ChoreBits.NODE_DIFF);
+ await container.$renderPromise$;
+ _flushJournal(journal);
const store = getStoreHandler((globalThis as any).store);
expect(store!.$effects$?.size).toBeGreaterThan(0);
- container.$journal$ = [];
- vnode_diff(container, , vParent, null);
- await waitForDrain(container.$scheduler$);
- vnode_applyJournal(container.$journal$);
-
+ const journal2: VNodeJournal = [];
+ await vnode_diff(container, journal2, , vParent, null);
+ _flushJournal(journal2);
+ await container.$renderPromise$;
expect(store!.$effects$?.size).toBe(0);
});
});
diff --git a/packages/qwik/src/core/client/vnode-impl.ts b/packages/qwik/src/core/client/vnode-impl.ts
deleted file mode 100644
index 9d948a39820..00000000000
--- a/packages/qwik/src/core/client/vnode-impl.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { VNodeFlags } from './types';
-import { mapApp_findIndx, mapArray_get } from './util-mapArray';
-import {
- vnode_ensureElementInflated,
- vnode_toString,
- VNodeJournalOpCode,
- type VNodeJournal,
-} from './vnode';
-import type { ChoreArray } from './chore-array';
-import { _EFFECT_BACK_REF } from '../reactive-primitives/types';
-import { BackRef } from '../reactive-primitives/cleanup';
-import { isDev } from '@qwik.dev/core/build';
-import type { QElement } from '../shared/types';
-
-/** @internal */
-export abstract class VNode extends BackRef {
- props: unknown[] | null = null;
- slotParent: VNode | null = null;
- // scheduled chores for this vnode
- chores: ChoreArray | null = null;
- // blocked chores for this vnode
- blockedChores: ChoreArray | null = null;
-
- constructor(
- public flags: VNodeFlags,
- public parent: ElementVNode | VirtualVNode | null,
- public previousSibling: VNode | null | undefined,
- public nextSibling: VNode | null | undefined
- ) {
- super();
- }
-
- getProp(key: string, getObject: ((id: string) => any) | null): T | null {
- const type = this.flags;
- if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) {
- type & VNodeFlags.Element && vnode_ensureElementInflated(this);
- this.props ||= [];
- const idx = mapApp_findIndx(this.props as any, key, 0);
- if (idx >= 0) {
- let value = this.props[idx + 1] as any;
- if (typeof value === 'string' && getObject) {
- this.props[idx + 1] = value = getObject(value);
- }
- return value;
- }
- }
- return null;
- }
-
- setProp(key: string, value: any) {
- this.props ||= [];
- const idx = mapApp_findIndx(this.props, key, 0);
- if (idx >= 0) {
- this.props[idx + 1] = value as any;
- } else if (value != null) {
- this.props.splice(idx ^ -1, 0, key, value as any);
- }
- }
-
- getAttr(key: string): string | null {
- if ((this.flags & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) {
- vnode_ensureElementInflated(this);
- this.props ||= [];
- return mapArray_get(this.props, key, 0) as string | null;
- }
- return null;
- }
-
- setAttr(key: string, value: string | null | boolean, journal: VNodeJournal | null) {
- const type = this.flags;
- if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) {
- vnode_ensureElementInflated(this);
- this.props ||= [];
- const idx = mapApp_findIndx(this.props, key, 0);
-
- if (idx >= 0) {
- if (this.props[idx + 1] != value && this instanceof ElementVNode) {
- // Values are different, update DOM
- journal && journal.push(VNodeJournalOpCode.SetAttribute, this.element, key, value);
- }
- if (value == null) {
- this.props.splice(idx, 2);
- } else {
- this.props[idx + 1] = value;
- }
- } else if (value != null) {
- this.props.splice(idx ^ -1, 0, key, value);
- if (this instanceof ElementVNode) {
- // New value, update DOM
- journal && journal.push(VNodeJournalOpCode.SetAttribute, this.element, key, value);
- }
- }
- }
- }
-
- toString(): string {
- if (isDev) {
- return vnode_toString.call(this);
- }
- return String(this);
- }
-}
-
-/** @internal */
-export class TextVNode extends VNode {
- constructor(
- flags: VNodeFlags,
- parent: ElementVNode | VirtualVNode | null,
- previousSibling: VNode | null | undefined,
- nextSibling: VNode | null | undefined,
- public textNode: Text | null,
- public text: string | undefined
- ) {
- super(flags, parent, previousSibling, nextSibling);
- }
-}
-
-/** @internal */
-export class VirtualVNode extends VNode {
- constructor(
- flags: VNodeFlags,
- parent: ElementVNode | VirtualVNode | null,
- previousSibling: VNode | null | undefined,
- nextSibling: VNode | null | undefined,
- public firstChild: VNode | null | undefined,
- public lastChild: VNode | null | undefined
- ) {
- super(flags, parent, previousSibling, nextSibling);
- }
-}
-
-/** @internal */
-export class ElementVNode extends VNode {
- constructor(
- flags: VNodeFlags,
- parent: ElementVNode | VirtualVNode | null,
- previousSibling: VNode | null | undefined,
- nextSibling: VNode | null | undefined,
- public firstChild: VNode | null | undefined,
- public lastChild: VNode | null | undefined,
- public element: QElement,
- public elementName: string | undefined
- ) {
- super(flags, parent, previousSibling, nextSibling);
- }
-}
diff --git a/packages/qwik/src/core/client/vnode-namespace.ts b/packages/qwik/src/core/client/vnode-namespace.ts
index 8f7970a789a..81933c34406 100644
--- a/packages/qwik/src/core/client/vnode-namespace.ts
+++ b/packages/qwik/src/core/client/vnode-namespace.ts
@@ -20,8 +20,10 @@ import {
vnode_isElementVNode,
vnode_isTextVNode,
type VNodeJournal,
-} from './vnode';
-import type { ElementVNode, VNode } from './vnode-impl';
+} from './vnode-utils';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
+import type { VNode } from '../shared/vnode/vnode';
+import type { TextVNode } from '../shared/vnode/text-vnode';
export const isForeignObjectElement = (elementName: string) => {
return isDev ? elementName.toLowerCase() === 'foreignobject' : elementName === 'foreignObject';
@@ -53,16 +55,16 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert(
journal: VNodeJournal,
domParentVNode: ElementVNode,
newChild: VNode
-) {
+): (ElementVNode | TextVNode)[] {
const { elementNamespace, elementNamespaceFlag } = getNewElementNamespaceData(
domParentVNode,
newChild
);
- let domChildren: (Element | Text)[] = [];
+ let domChildren: (ElementVNode | TextVNode)[] = [];
if (elementNamespace === HTML_NS) {
// parent is in the default namespace, so just get the dom children. This is the fast path.
- domChildren = vnode_getDOMChildNodes(journal, newChild);
+ domChildren = vnode_getDOMChildNodes(journal, newChild, true);
} else {
// parent is in a different namespace, so we need to clone the children with the correct namespace.
// The namespace cannot be changed on nodes, so we need to clone these nodes
@@ -72,7 +74,7 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert(
const childVNode = children[i];
if (vnode_isTextVNode(childVNode)) {
// text nodes are always in the default namespace
- domChildren.push(childVNode.textNode as Text);
+ domChildren.push(childVNode);
continue;
}
if (
@@ -80,7 +82,7 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert(
(domParentVNode.flags & VNodeFlags.NAMESPACE_MASK)
) {
// if the child and parent have the same namespace, we don't need to clone the element
- domChildren.push(childVNode.element as Element);
+ domChildren.push(childVNode);
continue;
}
@@ -93,7 +95,8 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert(
);
if (newChildElement) {
- domChildren.push(newChildElement);
+ childVNode.node = newChildElement;
+ domChildren.push(childVNode);
}
}
}
@@ -154,7 +157,7 @@ function vnode_cloneElementWithNamespace(
let newChildElement: Element | null = null;
if (vnode_isElementVNode(vCursor)) {
// Clone the element
- childElement = vCursor.element as Element;
+ childElement = vCursor.node;
const childElementTag = vnode_getElementName(vCursor);
// We need to check if the parent is a foreignObject element
@@ -197,7 +200,7 @@ function vnode_cloneElementWithNamespace(
// Then we can overwrite the cursor with newly created element.
// This is because we need to materialize the children before we assign new element
- vCursor.element = newChildElement;
+ vCursor.node = newChildElement;
// Set correct namespace flag
vCursor.flags &= VNodeFlags.NEGATED_NAMESPACE_MASK;
vCursor.flags |= namespaceFlag;
diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode-utils.ts
similarity index 81%
rename from packages/qwik/src/core/client/vnode.ts
rename to packages/qwik/src/core/client/vnode-utils.ts
index a1a574fb3a9..c229bdd1fae 100644
--- a/packages/qwik/src/core/client/vnode.ts
+++ b/packages/qwik/src/core/client/vnode-utils.ts
@@ -122,17 +122,17 @@ import { qwikDebugToString } from '../debug';
import { assertDefined, assertEqual, assertFalse, assertTrue } from '../shared/error/assert';
import { QError, qError } from '../shared/error/error';
import {
+ type Container,
DEBUG_TYPE,
QContainerValue,
+ type QElement,
VirtualType,
VirtualTypeName,
- type QElement,
} from '../shared/types';
import { isText } from '../shared/utils/element';
import {
dangerouslySetInnerHTML,
ELEMENT_ID,
- ELEMENT_KEY,
ELEMENT_PROPS,
ELEMENT_SEQ,
ELEMENT_SEQ_IDX,
@@ -148,12 +148,10 @@ import {
QScopedStyle,
QSlot,
QStyle,
- QStylesAllSelector,
} from '../shared/utils/markers';
import { isHtmlElement } from '../shared/utils/types';
import { VNodeDataChar } from '../shared/vnode-data-types';
import { getDomContainer } from './dom-container';
-import { mapArray_set } from './util-mapArray';
import {
type ClientContainer,
type ContainerElement,
@@ -166,50 +164,42 @@ import {
vnode_getElementNamespaceFlags,
} from './vnode-namespace';
import { mergeMaps } from '../shared/utils/maps';
-import { _EFFECT_BACK_REF } from '../reactive-primitives/types';
-import { ElementVNode, TextVNode, VirtualVNode, VNode } from './vnode-impl';
import { EventNameHtmlScope } from '../shared/utils/event-names';
+import { VNode } from '../shared/vnode/vnode';
+import { ElementVNode } from '../shared/vnode/element-vnode';
+import { TextVNode } from '../shared/vnode/text-vnode';
+import { VirtualVNode } from '../shared/vnode/virtual-vnode';
+import { VNodeOperationType } from '../shared/vnode/enums/vnode-operation-type.enum';
+import { addVNodeOperation } from '../shared/vnode/vnode-dirty';
+import { isCursor } from '../shared/cursor/cursor';
+import { _EFFECT_BACK_REF } from '../reactive-primitives/backref';
+import type { VNodeOperation } from '../shared/vnode/types/dom-vnode-operation';
+import { _flushJournal } from '../shared/cursor/cursor-flush';
//////////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Fundamental DOM operations are:
- *
- * - Insert new DOM element/text
- * - Remove DOM element/text
- * - Set DOM element attributes
- * - Set text node value
- */
-export const enum VNodeJournalOpCode {
- SetText = 1, // ------ [SetAttribute, target, text]
- SetAttribute = 2, // - [SetAttribute, target, ...(key, values)]]
- HoistStyles = 3, // -- [HoistStyles, document]
- Remove = 4, // ------- [Remove, target(parent), ...nodes]
- RemoveAll = 5, // ------- [RemoveAll, target(parent)]
- Insert = 6, // ------- [Insert, target(parent), reference, ...nodes]
-}
-
-export type VNodeJournal = Array<
- VNodeJournalOpCode | Document | Element | Text | string | boolean | null
->;
+export type VNodeJournal = Array;
//////////////////////////////////////////////////////////////////////////////////////////////////////
-export const vnode_newElement = (element: Element, elementName: string): ElementVNode => {
+export const vnode_newElement = (
+ element: Element,
+ elementName: string,
+ key: string | null = null
+): ElementVNode => {
assertEqual(fastNodeType(element), 1 /* ELEMENT_NODE */, 'Expecting element node.');
const vnode: ElementVNode = new ElementVNode(
+ key,
VNodeFlags.Element | VNodeFlags.Inflated | (-1 << VNodeFlagsIndex.shift), // Flag
null,
null,
null,
null,
null,
+ null,
element,
elementName
);
- assertTrue(vnode_isElementVNode(vnode), 'Incorrect format of ElementVNode.');
- assertFalse(vnode_isTextVNode(vnode), 'Incorrect format of ElementVNode.');
- assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of ElementVNode.');
(element as QElement).vNode = vnode;
return vnode;
};
@@ -217,18 +207,17 @@ export const vnode_newElement = (element: Element, elementName: string): Element
export const vnode_newUnMaterializedElement = (element: Element): ElementVNode => {
assertEqual(fastNodeType(element), 1 /* ELEMENT_NODE */, 'Expecting element node.');
const vnode: ElementVNode = new ElementVNode(
+ null,
VNodeFlags.Element | (-1 << VNodeFlagsIndex.shift), // Flag
null,
null,
null,
+ null,
undefined,
undefined,
element,
undefined
);
- assertTrue(vnode_isElementVNode(vnode), 'Incorrect format of ElementVNode.');
- assertFalse(vnode_isTextVNode(vnode), 'Incorrect format of ElementVNode.');
- assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of ElementVNode.');
(element as QElement).vNode = vnode;
return vnode;
};
@@ -239,18 +228,16 @@ export const vnode_newSharedText = (
textContent: string
): TextVNode => {
sharedTextNode &&
- assertEqual(fastNodeType(sharedTextNode), 3 /* TEXT_NODE */, 'Expecting element node.');
+ assertEqual(fastNodeType(sharedTextNode), 3 /* TEXT_NODE */, 'Expecting text node.');
const vnode: TextVNode = new TextVNode(
VNodeFlags.Text | (-1 << VNodeFlagsIndex.shift), // Flag
null, // Parent
previousTextNode, // Previous TextNode (usually first child)
null, // Next sibling
- sharedTextNode, // SharedTextNode
- textContent // Text Content
+ null,
+ sharedTextNode,
+ textContent
);
- assertFalse(vnode_isElementVNode(vnode), 'Incorrect format of TextVNode.');
- assertTrue(vnode_isTextVNode(vnode), 'Incorrect format of TextVNode.');
- assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of TextVNode.');
return vnode;
};
@@ -260,10 +247,11 @@ export const vnode_newText = (textNode: Text, textContent: string | undefined):
null, // Parent
null, // No previous sibling
null, // We may have a next sibling.
+ null,
textNode, // TextNode
textContent // Text Content
);
- assertEqual(fastNodeType(textNode), 3 /* TEXT_NODE */, 'Expecting element node.');
+ assertEqual(fastNodeType(textNode), 3 /* TEXT_NODE */, 'Expecting text node.');
assertFalse(vnode_isElementVNode(vnode), 'Incorrect format of TextVNode.');
assertTrue(vnode_isTextVNode(vnode), 'Incorrect format of TextVNode.');
assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of TextVNode.');
@@ -272,11 +260,13 @@ export const vnode_newText = (textNode: Text, textContent: string | undefined):
export const vnode_newVirtual = (): VirtualVNode => {
const vnode: VirtualVNode = new VirtualVNode(
+ null,
VNodeFlags.Virtual | (-1 << VNodeFlagsIndex.shift), // Flags
null,
null,
null,
null,
+ null,
null
);
assertFalse(vnode_isElementVNode(vnode), 'Incorrect format of TextVNode.');
@@ -292,12 +282,10 @@ export const vnode_isVNode = (vNode: any): vNode is VNode => {
};
export const vnode_isElementVNode = (vNode: VNode): vNode is ElementVNode => {
- assertDefined(vNode, 'Missing vNode');
- const flag = vNode.flags;
- return (flag & VNodeFlags.Element) === VNodeFlags.Element;
+ return vNode instanceof ElementVNode;
};
-export const vnode_isElementOrTextVNode = (vNode: VNode): vNode is ElementVNode => {
+export const vnode_isElementOrTextVNode = (vNode: VNode): vNode is ElementVNode | TextVNode => {
assertDefined(vNode, 'Missing vNode');
const flag = vNode.flags;
return (flag & VNodeFlags.ELEMENT_OR_TEXT_MASK) !== 0;
@@ -324,22 +312,20 @@ export const vnode_isMaterialized = (vNode: VNode): boolean => {
/** @internal */
export const vnode_isTextVNode = (vNode: VNode): vNode is TextVNode => {
- assertDefined(vNode, 'Missing vNode');
- const flag = vNode.flags;
- return (flag & VNodeFlags.Text) === VNodeFlags.Text;
+ return vNode instanceof TextVNode;
};
/** @internal */
export const vnode_isVirtualVNode = (vNode: VNode): vNode is VirtualVNode => {
- assertDefined(vNode, 'Missing vNode');
- const flag = vNode.flags;
- return (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual;
+ return vNode instanceof VirtualVNode;
};
export const vnode_isProjection = (vNode: VNode): vNode is VirtualVNode => {
assertDefined(vNode, 'Missing vNode');
const flag = vNode.flags;
- return (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual && vNode.getProp(QSlot, null) !== null;
+ return (
+ (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual && vnode_getProp(vNode, QSlot, null) !== null
+ );
};
const ensureTextVNode = (vNode: VNode): TextVNode => {
@@ -378,13 +364,67 @@ export const vnode_getNodeTypeName = (vNode: VNode): string => {
return '';
};
+export const vnode_getProp = (
+ vNode: VNode,
+ key: string,
+ getObject: ((id: string) => unknown) | null
+): T | null => {
+ if (vnode_isElementVNode(vNode) || vnode_isVirtualVNode(vNode)) {
+ const value = vNode.props?.[key] ?? null;
+ if (typeof value === 'string' && getObject) {
+ const result = getObject(value) as T | null;
+ vNode.props![key] = result;
+ return result;
+ }
+ return value as T | null;
+ }
+ return null;
+};
+
+export const vnode_setProp = (vNode: VNode, key: string, value: unknown) => {
+ if (vnode_isElementVNode(vNode) || vnode_isVirtualVNode(vNode)) {
+ if (value == null && vNode.props) {
+ delete vNode.props[key];
+ } else {
+ vNode.props ||= {};
+ vNode.props[key] = value;
+ }
+ }
+};
+
+export const vnode_setAttr = (
+ journal: VNodeJournal,
+ vNode: VNode,
+ key: string,
+ value: string | null | boolean
+) => {
+ if (vnode_isElementVNode(vNode)) {
+ vnode_setProp(vNode, key, value);
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.SetAttribute,
+ target: vNode.node,
+ attrName: key,
+ attrValue: value,
+ });
+ }
+};
+
+export const vnode_ensureElementKeyInflated = (vnode: ElementVNode) => {
+ if (vnode.key) {
+ return;
+ }
+ const value = vnode.node.getAttribute(Q_PROPS_SEPARATOR);
+ if (value) {
+ vnode.key = value;
+ }
+};
+
/** @internal */
export const vnode_ensureElementInflated = (vnode: VNode) => {
- const flags = vnode.flags;
- if ((flags & VNodeFlags.INFLATED_TYPE_MASK) === VNodeFlags.Element) {
+ if ((vnode.flags & VNodeFlags.INFLATED_TYPE_MASK) === VNodeFlags.Element) {
const elementVNode = vnode as ElementVNode;
elementVNode.flags ^= VNodeFlags.Inflated;
- const element = elementVNode.element;
+ const element = elementVNode.node;
const attributes = element.attributes;
for (let idx = 0; idx < attributes.length; idx++) {
const attr = attributes[idx];
@@ -392,18 +432,22 @@ export const vnode_ensureElementInflated = (vnode: VNode) => {
if (key === Q_PROPS_SEPARATOR || !key) {
// SVG in Domino does not support ':' so it becomes an empty string.
// all attributes after the ':' are considered immutable, and so we ignore them.
+ const value = attr.value;
+ if (value) {
+ // don't assign empty string as a key
+ elementVNode.key = value;
+ }
break;
} else if (key.startsWith(QContainerAttr)) {
- const props = vnode_getProps(elementVNode);
- if (attr.value === QContainerValue.HTML) {
- mapArray_set(props, dangerouslySetInnerHTML, element.innerHTML, 0);
- } else if (attr.value === QContainerValue.TEXT && 'value' in element) {
- mapArray_set(props, 'value', element.value, 0);
+ const value = attr.value;
+ if (value === QContainerValue.HTML) {
+ vnode_setProp(elementVNode, 'dangerouslySetInnerHTML', element.innerHTML);
+ } else if (value === QContainerValue.TEXT && 'value' in element) {
+ vnode_setProp(elementVNode, 'value', element.value);
}
} else if (!key.startsWith(EventNameHtmlScope.on)) {
const value = attr.value;
- const props = vnode_getProps(elementVNode);
- mapArray_set(props, key, value, 0);
+ vnode_setProp(elementVNode, key, value);
}
}
}
@@ -541,17 +585,16 @@ export function vnode_getDOMChildNodes(
* @param descend - If true, than we will descend into the children first.
* @returns
*/
+// TODO: split this function into two, one for next and one for previous.
const vnode_getDomSibling = (
vNode: VNode,
nextDirection: boolean,
descend: boolean
): ElementVNode | TextVNode | null => {
- const childProp = nextDirection ? 'firstChild' : 'lastChild';
- const siblingProp = nextDirection ? 'nextSibling' : 'previousSibling';
let cursor: VNode | null = vNode;
// first make sure we have a DOM node or no children.
while (descend && cursor && vnode_isVirtualVNode(cursor)) {
- const child: VNode | null | undefined = cursor[childProp];
+ const child: VNode | null | undefined = nextDirection ? cursor.firstChild : cursor.lastChild;
if (!child) {
break;
}
@@ -562,7 +605,9 @@ const vnode_getDomSibling = (
}
while (cursor) {
// Look at the previous/next sibling.
- let sibling: VNode | null | undefined = cursor[siblingProp];
+ let sibling: VNode | null | undefined = nextDirection
+ ? cursor.nextSibling
+ : cursor.previousSibling;
if (sibling && sibling.flags & VNodeFlags.ELEMENT_OR_TEXT_MASK) {
// we found a previous/next DOM node, return it.
return sibling as ElementVNode | TextVNode;
@@ -572,7 +617,10 @@ const vnode_getDomSibling = (
if (virtual && !vnode_isVirtualVNode(virtual)) {
return null;
}
- while (virtual && !(sibling = virtual[siblingProp])) {
+ while (
+ virtual &&
+ !(sibling = nextDirection ? virtual.nextSibling : virtual.previousSibling)
+ ) {
virtual = virtual.parent;
if (virtual && !vnode_isVirtualVNode(virtual)) {
@@ -598,7 +646,9 @@ const vnode_getDomSibling = (
// zero length and which does not have a representation in the DOM.
return cursor as ElementVNode | TextVNode;
}
- sibling = (cursor as VirtualVNode)[childProp];
+ sibling = nextDirection
+ ? (cursor as VirtualVNode).firstChild
+ : (cursor as VirtualVNode).lastChild;
}
// If we are here we did not find anything and we need to go up the tree again.
}
@@ -617,48 +667,59 @@ const vnode_ensureTextInflated = (journal: VNodeJournal, vnode: TextVNode) => {
if ((flags & VNodeFlags.Inflated) === 0) {
const parentNode = vnode_getDomParent(vnode);
assertDefined(parentNode, 'Missing parent node.');
- const sharedTextNode = textVNode.textNode as Text;
+ const sharedTextNode = textVNode.node as Text;
const doc = parentNode.ownerDocument;
// Walk the previous siblings and inflate them.
- let cursor = vnode_getDomSibling(vnode, false, true);
+ let vCursor = vnode_getDomSibling(vnode, false, true);
// If text node is 0 length, than there is no text node.
// In that case we use the next node as a reference, in which
// case we know that the next node MUST be either NULL or an Element.
const node = vnode_getDomSibling(vnode, true, true);
const insertBeforeNode: Element | Text | null =
sharedTextNode ||
- (((node instanceof ElementVNode ? node.element : node?.textNode) || null) as
- | Element
- | Text
- | null);
+ (((node instanceof ElementVNode ? node.node : node?.node) || null) as Element | Text | null);
let lastPreviousTextNode = insertBeforeNode;
- while (cursor && vnode_isTextVNode(cursor)) {
- if ((cursor.flags & VNodeFlags.Inflated) === 0) {
- const textNode = doc.createTextNode(cursor.text!);
- journal.push(VNodeJournalOpCode.Insert, parentNode, lastPreviousTextNode, textNode);
+ while (vCursor && vnode_isTextVNode(vCursor)) {
+ if ((vCursor.flags & VNodeFlags.Inflated) === 0) {
+ const textNode = doc.createTextNode(vCursor.text!);
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.InsertOrMove,
+ parent: parentNode,
+ beforeTarget: lastPreviousTextNode,
+ target: textNode,
+ });
lastPreviousTextNode = textNode;
- cursor.textNode = textNode;
- cursor.flags |= VNodeFlags.Inflated;
+ vCursor.node = textNode;
+ vCursor.flags |= VNodeFlags.Inflated;
}
- cursor = vnode_getDomSibling(cursor, false, true);
+ vCursor = vnode_getDomSibling(vCursor, false, true);
}
// Walk the next siblings and inflate them.
- cursor = vnode;
- while (cursor && vnode_isTextVNode(cursor)) {
- const next = vnode_getDomSibling(cursor, true, true);
+ vCursor = vnode;
+ while (vCursor && vnode_isTextVNode(vCursor)) {
+ const next = vnode_getDomSibling(vCursor, true, true);
const isLastNode = next ? !vnode_isTextVNode(next) : true;
- if ((cursor.flags & VNodeFlags.Inflated) === 0) {
+ if ((vCursor.flags & VNodeFlags.Inflated) === 0) {
if (isLastNode && sharedTextNode) {
- journal.push(VNodeJournalOpCode.SetText, sharedTextNode, cursor.text!);
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.SetText,
+ target: sharedTextNode,
+ text: vCursor.text!,
+ });
} else {
- const textNode = doc.createTextNode(cursor.text!);
- journal.push(VNodeJournalOpCode.Insert, parentNode, insertBeforeNode, textNode);
- cursor.textNode = textNode;
+ const textNode = doc.createTextNode(vCursor.text!);
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.InsertOrMove,
+ parent: parentNode,
+ beforeTarget: insertBeforeNode,
+ target: textNode,
+ });
+ vCursor.node = textNode;
}
- cursor.flags |= VNodeFlags.Inflated;
+ vCursor.flags |= VNodeFlags.Inflated;
}
- cursor = next;
+ vCursor = next;
}
}
};
@@ -666,7 +727,7 @@ const vnode_ensureTextInflated = (journal: VNodeJournal, vnode: TextVNode) => {
export const vnode_locate = (rootVNode: ElementVNode, id: string | Element): VNode => {
ensureElementVNode(rootVNode);
let vNode: VNode | Element = rootVNode;
- const containerElement = rootVNode.element as ContainerElement;
+ const containerElement = rootVNode.node as ContainerElement;
const { qVNodeRefs } = containerElement;
let elementOffset: number = -1;
let refElement: Element | VNode;
@@ -751,7 +812,7 @@ export const vnode_getVNodeForChildNode = (
ensureElementVNode(vNode);
let child = vnode_getFirstChild(vNode);
assertDefined(child, 'Missing child.');
- while (child && (child instanceof ElementVNode ? child.element !== childElement : true)) {
+ while (child && (child instanceof ElementVNode ? child.node !== childElement : true)) {
if (vnode_isVirtualVNode(child)) {
const next = child.nextSibling as VNode | null;
const firstChild = vnode_getFirstChild(child);
@@ -775,7 +836,7 @@ export const vnode_getVNodeForChildNode = (
vNodeStack.pop();
}
ensureElementVNode(child);
- assertEqual((child as ElementVNode).element, childElement, 'Child not found.');
+ assertEqual((child as ElementVNode).node, childElement, 'Child not found.');
// console.log('FOUND', child[VNodeProps.node]?.outerHTML);
return child as ElementVNode;
};
@@ -793,10 +854,10 @@ const indexOfAlphanumeric = (id: string, length: number): number => {
};
export const vnode_createErrorDiv = (
+ journal: VNodeJournal,
document: Document,
host: VNode,
- err: Error,
- journal: VNodeJournal
+ err: Error
) => {
const errorDiv = document.createElement('errored-host');
if (err && err instanceof Error) {
@@ -844,43 +905,30 @@ export const vnode_journalToString = (journal: VNodeJournal): string => {
}
while (idx < length) {
- const op = journal[idx++] as VNodeJournalOpCode;
- switch (op) {
- case VNodeJournalOpCode.SetText:
+ const op = journal[idx++];
+ switch (op.operationType) {
+ case VNodeOperationType.SetText:
stringify('SetText');
- stringify(' ', journal[idx++]);
- stringify(' -->', journal[idx++]);
+ stringify(' ', op.text);
+ stringify(' -->', op.target);
break;
- case VNodeJournalOpCode.SetAttribute:
+ case VNodeOperationType.SetAttribute:
stringify('SetAttribute');
- stringify(' ', journal[idx++]);
- stringify(' key', journal[idx++]);
- stringify(' val', journal[idx++]);
+ stringify(' ', op.attrName);
+ stringify(' key', op.attrName);
+ stringify(' val', op.attrValue);
break;
- case VNodeJournalOpCode.HoistStyles:
- stringify('HoistStyles');
- break;
- case VNodeJournalOpCode.Remove: {
- stringify('Remove');
- const parent = journal[idx++];
- stringify(' ', parent);
- let nodeToRemove: any;
- while (idx < length && typeof (nodeToRemove = journal[idx]) !== 'number') {
- stringify(' -->', nodeToRemove);
- idx++;
- }
+ case VNodeOperationType.Delete: {
+ stringify('Delete');
+ stringify(' -->', op.target);
break;
}
- case VNodeJournalOpCode.Insert: {
- stringify('Insert');
- const parent = journal[idx++];
- const insertBefore = journal[idx++];
+ case VNodeOperationType.InsertOrMove: {
+ stringify('InsertOrMove');
+ const parent = op.parent;
+ const insertBefore = op.beforeTarget;
stringify(' ', parent);
- let newChild: any;
- while (idx < length && typeof (newChild = journal[idx]) !== 'number') {
- stringify(' -->', newChild);
- idx++;
- }
+ stringify(' -->', op.target);
if (insertBefore) {
stringify(' ', insertBefore);
}
@@ -891,113 +939,7 @@ export const vnode_journalToString = (journal: VNodeJournal): string => {
lines.push('END JOURNAL');
return lines.join('\n');
};
-
-const parseBoolean = (value: string | boolean | null): boolean => {
- if (value === 'false') {
- return false;
- }
- return Boolean(value);
-};
-
-const isBooleanAttr = (element: Element, key: string): boolean => {
- const isBoolean =
- key == 'allowfullscreen' ||
- key == 'async' ||
- key == 'autofocus' ||
- key == 'autoplay' ||
- key == 'checked' ||
- key == 'controls' ||
- key == 'default' ||
- key == 'defer' ||
- key == 'disabled' ||
- key == 'formnovalidate' ||
- key == 'inert' ||
- key == 'ismap' ||
- key == 'itemscope' ||
- key == 'loop' ||
- key == 'multiple' ||
- key == 'muted' ||
- key == 'nomodule' ||
- key == 'novalidate' ||
- key == 'open' ||
- key == 'playsinline' ||
- key == 'readonly' ||
- key == 'required' ||
- key == 'reversed' ||
- key == 'selected';
- return isBoolean && key in element;
-};
-
-export const vnode_applyJournal = (journal: VNodeJournal) => {
- // console.log('APPLY JOURNAL', vnode_journalToString(journal));
- let idx = 0;
- const length = journal.length;
- while (idx < length) {
- const op = journal[idx++] as VNodeJournalOpCode;
- switch (op) {
- case VNodeJournalOpCode.SetText:
- const text = journal[idx++] as Text;
- text.nodeValue = journal[idx++] as string;
- break;
- case VNodeJournalOpCode.SetAttribute:
- const element = journal[idx++] as Element;
- let key = journal[idx++] as string;
- if (key === 'className') {
- key = 'class';
- }
- const value = journal[idx++] as string | null | boolean;
- const shouldRemove = value == null || value === false;
- if (isBooleanAttr(element, key)) {
- (element as any)[key] = parseBoolean(value);
- } else if (key === dangerouslySetInnerHTML) {
- (element as any).innerHTML = value!;
- element.setAttribute(QContainerAttr, QContainerValue.HTML);
- } else if (shouldRemove) {
- element.removeAttribute(key);
- } else if (key === 'value' && key in element) {
- (element as any).value = String(value);
- } else {
- element.setAttribute(key, String(value));
- }
- break;
- case VNodeJournalOpCode.HoistStyles:
- const document = journal[idx++] as Document;
- const head = document.head;
- const styles = document.querySelectorAll(QStylesAllSelector);
- for (let i = 0; i < styles.length; i++) {
- head.appendChild(styles[i]);
- }
- break;
- case VNodeJournalOpCode.Remove:
- const removeParent = journal[idx++] as Element;
- let nodeToRemove: any;
- while (idx < length && typeof (nodeToRemove = journal[idx]) !== 'number') {
- removeParent.removeChild(nodeToRemove as Element | Text);
- idx++;
- }
- break;
- case VNodeJournalOpCode.RemoveAll:
- const removeAllParent = journal[idx++] as Element;
- if (removeAllParent.replaceChildren) {
- removeAllParent.replaceChildren();
- } else {
- // fallback if replaceChildren is not supported
- removeAllParent.textContent = '';
- }
- break;
- case VNodeJournalOpCode.Insert:
- const insertParent = journal[idx++] as Element;
- const insertBefore = journal[idx++] as Element | Text | null;
- let newChild: any;
- while (idx < length && typeof (newChild = journal[idx]) !== 'number') {
- insertParent.insertBefore(newChild, insertBefore);
- idx++;
- }
- break;
- }
- }
- journal.length = 0;
-};
+export const vnode_applyJournal = _flushJournal;
//////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1011,7 +953,7 @@ export const vnode_insertBefore = (
if (vnode_isElementVNode(parent)) {
ensureMaterialized(parent);
}
- const newChildCurrentParent = newChild.parent;
+ const newChildCurrentParent = newChild.parent as ElementVNode | VirtualVNode | null;
if (newChild === insertBefore) {
// invalid insertBefore. We can't insert before self reference
// prevent infinity loop and putting self reference to next sibling
@@ -1045,8 +987,8 @@ export const vnode_insertBefore = (
* find children first (and inflate them).
*/
const domParentVNode = vnode_getDomParentVNode(parent, false);
- const parentNode = domParentVNode && domParentVNode.element;
- let domChildren: (Element | Text)[] | null = null;
+ const parentNode = domParentVNode && domParentVNode.node;
+ let domChildren: (ElementVNode | TextVNode)[] | null = null;
if (domParentVNode) {
domChildren = vnode_getDomChildrenWithCorrectNamespacesToInsert(
journal,
@@ -1101,9 +1043,9 @@ export const vnode_insertBefore = (
const parentIsDeleted = parent.flags & VNodeFlags.Deleted;
+ let adjustedInsertBefore: VNode | null = null;
// if the parent is deleted, then we don't need to insert the new child
if (!parentIsDeleted) {
- let adjustedInsertBefore: VNode | null = null;
if (insertBefore == null) {
if (vnode_isVirtualVNode(parent)) {
// If `insertBefore` is null, than we need to insert at the end of the list.
@@ -1120,14 +1062,15 @@ export const vnode_insertBefore = (
}
adjustedInsertBefore && vnode_ensureInflatedIfText(journal, adjustedInsertBefore);
- // Here we know the insertBefore node
if (domChildren && domChildren.length) {
- journal.push(
- VNodeJournalOpCode.Insert,
- parentNode,
- vnode_getNode(adjustedInsertBefore),
- ...domChildren
- );
+ for (const child of domChildren) {
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.InsertOrMove,
+ parent: parentNode!,
+ beforeTarget: vnode_getNode(adjustedInsertBefore),
+ target: child.node!,
+ });
+ }
}
}
@@ -1153,12 +1096,9 @@ export const vnode_insertBefore = (
}
};
-export const vnode_getDomParent = (
- vnode: VNode,
- includeProjection = true
-): Element | Text | null => {
+export const vnode_getDomParent = (vnode: VNode, includeProjection = true): Element | null => {
vnode = vnode_getDomParentVNode(vnode, includeProjection) as VNode;
- return (vnode && (vnode as ElementVNode).element) as Element | Text | null;
+ return (vnode && (vnode as ElementVNode).node) as Element | null;
};
export const vnode_getDomParentVNode = (
@@ -1184,13 +1124,21 @@ export const vnode_remove = (
if (removeDOM) {
const domParent = vnode_getDomParent(vParent, false);
- const isInnerHTMLParent = vParent.getAttr(dangerouslySetInnerHTML);
+ const isInnerHTMLParent = vnode_getProp(vParent, dangerouslySetInnerHTML, null) !== null;
if (isInnerHTMLParent) {
// ignore children, as they are inserted via innerHTML
return;
}
- const children = vnode_getDOMChildNodes(journal, vToRemove);
- domParent && children.length && journal.push(VNodeJournalOpCode.Remove, domParent, ...children);
+ const children = vnode_getDOMChildNodes(journal, vToRemove, true);
+ //&& //journal.push(VNodeOperationType.Remove, domParent, ...children);
+ if (domParent && children.length) {
+ for (const child of children) {
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.Delete,
+ target: child.node!,
+ });
+ }
+ }
}
const vPrevious = vToRemove.previousSibling;
@@ -1210,6 +1158,8 @@ export const vnode_remove = (
};
export const vnode_queryDomNodes = (
+ container: Container,
+ journal: VNodeJournal,
vNode: VNode,
selector: string,
cb: (element: Element) => void
@@ -1224,7 +1174,7 @@ export const vnode_queryDomNodes = (
} else {
let child = vnode_getFirstChild(vNode);
while (child) {
- vnode_queryDomNodes(child, selector, cb);
+ vnode_queryDomNodes(container, journal, child, selector, cb);
child = child.nextSibling as VNode | null;
}
}
@@ -1233,16 +1183,27 @@ export const vnode_queryDomNodes = (
export const vnode_truncate = (
journal: VNodeJournal,
vParent: ElementVNode | VirtualVNode,
- vDelete: VNode
+ vDelete: VNode,
+ removeDOM = true
) => {
assertDefined(vDelete, 'Missing vDelete.');
const parent = vnode_getDomParent(vParent);
- if (parent) {
+ if (parent && removeDOM) {
if (vnode_isElementVNode(vParent)) {
- journal.push(VNodeJournalOpCode.RemoveAll, parent);
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.RemoveAllChildren,
+ target: vParent.node!,
+ });
} else {
- const children = vnode_getDOMChildNodes(journal, vParent);
- children.length && journal.push(VNodeJournalOpCode.Remove, parent, ...children);
+ const children = vnode_getDOMChildNodes(journal, vParent, true);
+ if (children.length) {
+ for (const child of children) {
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.Delete,
+ target: child.node!,
+ });
+ }
+ }
}
}
const vPrevious = vDelete.previousSibling;
@@ -1260,7 +1221,7 @@ export const vnode_getElementName = (vnode: ElementVNode): string => {
const elementVNode = ensureElementVNode(vnode);
let elementName = elementVNode.elementName;
if (elementName === undefined) {
- const element = elementVNode.element;
+ const element = elementVNode.node;
const nodeName = fastNodeName(element)!.toLowerCase();
elementName = elementVNode.elementName = nodeName;
elementVNode.flags |= vnode_getElementNamespaceFlags(element);
@@ -1271,15 +1232,19 @@ export const vnode_getElementName = (vnode: ElementVNode): string => {
export const vnode_getText = (textVNode: TextVNode): string => {
let text = textVNode.text;
if (text === undefined) {
- text = textVNode.text = textVNode.textNode!.nodeValue!;
+ text = textVNode.text = textVNode.node!.nodeValue!;
}
return text;
};
export const vnode_setText = (journal: VNodeJournal, textVNode: TextVNode, text: string) => {
vnode_ensureTextInflated(journal, textVNode);
- const textNode = textVNode.textNode!;
- journal.push(VNodeJournalOpCode.SetText, textNode, (textVNode.text = text));
+ textVNode.text = text;
+ addVNodeOperation(journal, {
+ operationType: VNodeOperationType.SetText,
+ target: textVNode.node!,
+ text: text,
+ });
};
/** @internal */
@@ -1295,7 +1260,7 @@ export const vnode_getFirstChild = (vnode: VNode): VNode | null => {
};
const vnode_materialize = (vNode: ElementVNode) => {
- const element = vNode.element;
+ const element = vNode.node;
const firstChild = fastFirstChild(element);
const vNodeData = (element.ownerDocument as QDocument)?.qVNodeData?.get(element);
@@ -1309,6 +1274,7 @@ const materialize = (
firstChild: Node | null,
vNodeData?: string
): VNode | null => {
+ vnode_ensureElementKeyInflated(vNode);
if (vNodeData) {
if (vNodeData.charCodeAt(0) === VNodeDataChar.SEPARATOR) {
/**
@@ -1354,7 +1320,7 @@ export const ensureMaterialized = (vnode: ElementVNode): VNode | null => {
let vFirstChild = vParent.firstChild;
if (vFirstChild === undefined) {
// need to materialize the vNode.
- const element = vParent.element;
+ const element = vParent.node;
if (vParent.parent && shouldIgnoreChildren(element)) {
// We have a container with html value, must ignore the content.
@@ -1555,14 +1521,14 @@ const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null, vDat
processVNodeData(vData, (peek, consumeValue) => {
if (peek() === VNodeDataChar.ID) {
if (!container) {
- container = getDomContainer(vParent.element);
+ container = getDomContainer(vParent.node);
}
const id = consumeValue();
container.$setRawState$(parseInt(id), vParent);
- isDev && vParent.setAttr(ELEMENT_ID, id, null);
+ isDev && vnode_setProp(vParent, ELEMENT_ID, id);
} else if (peek() === VNodeDataChar.BACK_REFS) {
if (!container) {
- container = getDomContainer(vParent.element);
+ container = getDomContainer(vParent.node);
}
setEffectBackRefFromVNodeData(vParent, consumeValue(), container);
} else {
@@ -1671,11 +1637,12 @@ export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[]
if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) {
vnode_ensureElementInflated(vnode);
const keys: string[] = [];
- const props = vnode_getProps(vnode);
- for (let i = 0; i < props.length; i = i + 2) {
- const key = props[i] as string;
- if (!key.startsWith(Q_PROPS_SEPARATOR)) {
- keys.push(key);
+ const props = vnode.props;
+ if (props) {
+ for (const key of Object.keys(props)) {
+ if (!key.startsWith(Q_PROPS_SEPARATOR)) {
+ keys.push(key);
+ }
}
}
return keys;
@@ -1683,12 +1650,6 @@ export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[]
return [];
};
-/** @internal */
-export const vnode_getProps = (vnode: ElementVNode | VirtualVNode): unknown[] => {
- vnode.props ||= [];
- return vnode.props;
-};
-
export const vnode_isDescendantOf = (vnode: VNode, ancestor: VNode): boolean => {
let parent: VNode | null = vnode_getProjectionParentOrParent(vnode);
while (parent) {
@@ -1708,11 +1669,7 @@ export const vnode_getNode = (vnode: VNode | null): Element | Text | null => {
if (vnode === null || vnode_isVirtualVNode(vnode)) {
return null;
}
- if (vnode_isElementVNode(vnode)) {
- return vnode.element;
- }
- assertTrue(vnode_isTextVNode(vnode), 'Expecting Text Node.');
- return (vnode as TextVNode).textNode!;
+ return (vnode as ElementVNode | TextVNode).node;
};
/** @internal */
@@ -1745,13 +1702,13 @@ export function vnode_toString(
const attrs: string[] = ['[' + String(idx) + ']'];
vnode_getAttrKeys(vnode).forEach((key) => {
if (key !== DEBUG_TYPE) {
- const value = vnode!.getAttr(key);
+ const value = vnode_getProp(vnode!, key, null);
attrs.push(' ' + key + '=' + qwikDebugToString(value));
}
});
const name =
(colorize ? NAME_COL_PREFIX : '') +
- (VirtualTypeName[vnode.getAttr(DEBUG_TYPE) || VirtualType.Virtual] ||
+ (VirtualTypeName[vnode_getProp(vnode, DEBUG_TYPE, null) || VirtualType.Virtual] ||
VirtualTypeName[VirtualType.Virtual]) +
(colorize ? NAME_COL_SUFFIX : '');
strings.push('<' + name + attrs.join('') + '>');
@@ -1764,9 +1721,18 @@ export function vnode_toString(
} else if (vnode_isElementVNode(vnode)) {
const tag = vnode_getElementName(vnode);
const attrs: string[] = [];
+ if (isCursor(vnode)) {
+ attrs.push(' cursor');
+ }
+ if (vnode.dirty) {
+ attrs.push(` dirty:${vnode.dirty}`);
+ }
+ if (vnode.dirtyChildren) {
+ attrs.push(` dirtyChildren[${vnode.dirtyChildren.length}]`);
+ }
const keys = vnode_getAttrKeys(vnode);
keys.forEach((key) => {
- const value = vnode!.getAttr(key);
+ const value = vnode_getProp(vnode!, key, null);
attrs.push(' ' + key + '=' + qwikDebugToString(value));
});
const node = vnode_getNode(vnode) as HTMLElement;
@@ -1869,18 +1835,18 @@ function materializeFromVNodeData(
}
// collect the elements;
} else if (peek() === VNodeDataChar.SCOPED_STYLE) {
- vParent.setAttr(QScopedStyle, consumeValue(), null);
+ vnode_setProp(vParent, QScopedStyle, consumeValue());
} else if (peek() === VNodeDataChar.RENDER_FN) {
- vParent.setAttr(OnRenderProp, consumeValue(), null);
+ vnode_setProp(vParent, OnRenderProp, consumeValue());
} else if (peek() === VNodeDataChar.ID) {
if (!container) {
container = getDomContainer(element);
}
const id = consumeValue();
container.$setRawState$(parseInt(id), vParent);
- isDev && vParent.setAttr(ELEMENT_ID, id, null);
+ isDev && vnode_setProp(vParent, ELEMENT_ID, id);
} else if (peek() === VNodeDataChar.PROPS) {
- vParent.setAttr(ELEMENT_PROPS, consumeValue(), null);
+ vnode_setProp(vParent, ELEMENT_PROPS, consumeValue());
} else if (peek() === VNodeDataChar.KEY) {
const isEscapedValue = getChar(nextToConsumeIdx + 1) === VNodeDataChar.SEPARATOR;
let value;
@@ -1891,11 +1857,11 @@ function materializeFromVNodeData(
} else {
value = consumeValue();
}
- vParent.setAttr(ELEMENT_KEY, value, null);
+ vParent.key = value;
} else if (peek() === VNodeDataChar.SEQ) {
- vParent.setAttr(ELEMENT_SEQ, consumeValue(), null);
+ vnode_setProp(vParent, ELEMENT_SEQ, consumeValue());
} else if (peek() === VNodeDataChar.SEQ_IDX) {
- vParent.setAttr(ELEMENT_SEQ_IDX, consumeValue(), null);
+ vnode_setProp(vParent, ELEMENT_SEQ_IDX, consumeValue());
} else if (peek() === VNodeDataChar.BACK_REFS) {
if (!container) {
container = getDomContainer(element);
@@ -1907,7 +1873,7 @@ function materializeFromVNodeData(
}
vParent.slotParent = vnode_locate(container!.rootVNode, consumeValue());
} else if (peek() === VNodeDataChar.CONTEXT) {
- vParent.setAttr(QCtxAttr, consumeValue(), null);
+ vnode_setProp(vParent, QCtxAttr, consumeValue());
} else if (peek() === VNodeDataChar.OPEN) {
consume();
addVNode(vnode_newVirtual());
@@ -1918,7 +1884,7 @@ function materializeFromVNodeData(
} else if (peek() === VNodeDataChar.SEPARATOR) {
const key = consumeValue();
const value = consumeValue();
- vParent.setAttr(key, value, null);
+ vnode_setProp(vParent, key, value);
} else if (peek() === VNodeDataChar.CLOSE) {
consume();
vParent.lastChild = vLast;
@@ -1928,7 +1894,7 @@ function materializeFromVNodeData(
vFirst = stack.pop();
vParent = stack.pop();
} else if (peek() === VNodeDataChar.SLOT) {
- vParent.setAttr(QSlot, consumeValue(), null);
+ vnode_setProp(vParent, QSlot, consumeValue());
} else {
// skip over style or non-qwik elements in front of text nodes, where text node is the first child (except the style node)
while (isElement(child) && shouldSkipElement(child)) {
@@ -2001,7 +1967,7 @@ export const vnode_getProjectionParentComponent = (vHost: VNode): VirtualVNode |
while (projectionDepth--) {
while (
vHost &&
- (vnode_isVirtualVNode(vHost) ? vHost.getProp(OnRenderProp, null) === null : true)
+ (vnode_isVirtualVNode(vHost) ? vnode_getProp(vHost, OnRenderProp, null) === null : true)
) {
const qSlotParent = vHost.slotParent;
const vProjectionParent = vnode_isVirtualVNode(vHost) && qSlotParent;
diff --git a/packages/qwik/src/core/client/vnode.unit.tsx b/packages/qwik/src/core/client/vnode.unit.tsx
index 452a9e6099f..9b0d6dc295d 100644
--- a/packages/qwik/src/core/client/vnode.unit.tsx
+++ b/packages/qwik/src/core/client/vnode.unit.tsx
@@ -7,6 +7,7 @@ import { VNodeFlags, type ContainerElement, type QDocument } from './types';
import {
vnode_applyJournal,
vnode_getFirstChild,
+ vnode_getProp,
vnode_insertBefore,
vnode_locate,
vnode_newElement,
@@ -14,11 +15,16 @@ import {
vnode_newUnMaterializedElement,
vnode_newVirtual,
vnode_remove,
+ vnode_setAttr,
+ vnode_setProp,
vnode_setText,
vnode_walkVNode,
type VNodeJournal,
-} from './vnode';
-import type { ElementVNode, TextVNode, VirtualVNode, VNode } from './vnode-impl';
+} from './vnode-utils';
+import type { ElementVNode } from '../shared/vnode/element-vnode';
+import type { VNode } from '../shared/vnode/vnode';
+import type { TextVNode } from '../shared/vnode/text-vnode';
+import type { VirtualVNode } from '../shared/vnode/virtual-vnode';
describe('vnode', () => {
let parent: ContainerElement;
@@ -450,9 +456,9 @@ describe('vnode', () => {
const fragment1 = vnode_newVirtual();
const fragment2 = vnode_newVirtual();
const fragment3 = vnode_newVirtual();
- (fragment1 as VirtualVNode).setAttr('q:id', '1', null);
- (fragment2 as VirtualVNode).setAttr('q:id', '2', null);
- (fragment3 as VirtualVNode).setAttr('q:id', '3', null);
+ vnode_setProp(fragment1 as VirtualVNode, 'q:id', '1');
+ vnode_setProp(fragment2 as VirtualVNode, 'q:id', '2');
+ vnode_setProp(fragment3 as VirtualVNode, 'q:id', '3');
const textA = vnode_newText(document.createTextNode('1A'), '1A');
const textB = vnode_newText(document.createTextNode('2B'), '2B');
const textC = vnode_newText(document.createTextNode('3C'), '3C');
@@ -2560,8 +2566,8 @@ describe('vnode', () => {
const v2 = v1.nextSibling as VirtualVNode;
expect(v1).toMatchVDOM(<>A>);
expect(v2).toMatchVDOM(<>B>);
- expect(v1.getProp('', getVNode)).toBe(v2);
- expect(v2.getProp(':', getVNode)).toBe(v1);
+ expect(vnode_getProp(v1, '', getVNode)).toBe(v2);
+ expect(vnode_getProp(v2, ':', getVNode)).toBe(v1);
});
});
describe('attributes', () => {
@@ -2578,7 +2584,7 @@ describe('vnode', () => {
it('should update innerHTML', () => {
parent.innerHTML = 'content ';
const div = vnode_getFirstChild(vParent) as ElementVNode;
- (div as VirtualVNode).setAttr('dangerouslySetInnerHTML', 'new content', journal);
+ vnode_setAttr(journal, div, 'dangerouslySetInnerHTML', 'new content');
vnode_applyJournal(journal);
expect(parent.innerHTML).toBe('new content ');
expect(vParent).toMatchVDOM(
@@ -2587,7 +2593,7 @@ describe('vnode', () => {
);
- expect((div as VirtualVNode).getAttr('dangerouslySetInnerHTML')).toBe('new content');
+ expect(vnode_getProp(div, 'dangerouslySetInnerHTML', null)).toBe('new content');
});
it('should have empty child for dangerouslySetInnerHTML', () => {
parent.innerHTML = 'content ';
@@ -2614,7 +2620,7 @@ describe('vnode', () => {
it('should update textContent', () => {
parent.innerHTML = '';
const textarea = vnode_getFirstChild(vParent) as ElementVNode;
- (textarea as VirtualVNode).setAttr('value', 'new content', journal);
+ vnode_setAttr(journal, textarea as VirtualVNode, 'value', 'new content');
vnode_applyJournal(journal);
expect(parent.innerHTML).toBe('');
expect(vParent).toMatchVDOM(
@@ -2623,7 +2629,7 @@ describe('vnode', () => {
);
- expect((textarea as VirtualVNode).getAttr('value')).toBe('new content');
+ expect(vnode_getProp(textarea, 'value', null)).toBe('new content');
});
it('should have empty child for value', () => {
parent.innerHTML = '';
@@ -2728,10 +2734,10 @@ describe('vnode', () => {
it('should set attribute', () => {
parent.innerHTML = '';
const div = vnode_getFirstChild(vParent) as ElementVNode;
- (div as VirtualVNode).setAttr('key', '123', journal);
+ vnode_setAttr(journal, div as VirtualVNode, 'key', '123');
vnode_applyJournal(journal);
expect(parent.innerHTML).toBe('');
- (div as VirtualVNode).setAttr('foo', null, journal);
+ vnode_setAttr(journal, div as VirtualVNode, 'foo', null);
vnode_applyJournal(journal);
expect(parent.innerHTML).toBe('');
});
diff --git a/packages/qwik/src/core/debug.ts b/packages/qwik/src/core/debug.ts
index 5645370d4c4..b17ac760b3a 100644
--- a/packages/qwik/src/core/debug.ts
+++ b/packages/qwik/src/core/debug.ts
@@ -1,6 +1,6 @@
import { isSignal } from './reactive-primitives/utils';
// ^ keep this first to avoid circular dependency breaking class extend
-import { vnode_isVNode } from './client/vnode';
+import { vnode_getProp, vnode_isVNode } from './client/vnode-utils';
import { ComputedSignalImpl } from './reactive-primitives/impl/computed-signal-impl';
import { isStore } from './reactive-primitives/impl/store';
import { WrappedSignalImpl } from './reactive-primitives/impl/wrapped-signal-impl';
@@ -8,54 +8,60 @@ import { isJSXNode } from './shared/jsx/jsx-node';
import { isQrl } from './shared/qrl/qrl-utils';
import { DEBUG_TYPE } from './shared/types';
import { isTask } from './use/use-task';
+import { SERIALIZABLE_STATE } from './shared/component.public';
const stringifyPath: any[] = [];
export function qwikDebugToString(value: any): any {
- if (value === null) {
- return 'null';
- } else if (value === undefined) {
- return 'undefined';
- } else if (typeof value === 'string') {
- return '"' + value + '"';
- } else if (typeof value === 'number' || typeof value === 'boolean') {
- return String(value);
- } else if (isTask(value)) {
- return `Task(${qwikDebugToString(value.$qrl$)})`;
- } else if (isQrl(value)) {
- return `Qrl(${value.$symbol$})`;
- } else if (typeof value === 'object' || typeof value === 'function') {
- if (stringifyPath.includes(value)) {
- return '*';
- }
- if (stringifyPath.length > 10) {
- // debugger;
- }
- try {
- stringifyPath.push(value);
- if (Array.isArray(value)) {
- if (vnode_isVNode(value)) {
- return '(' + value.getProp(DEBUG_TYPE, null) + ')';
- } else {
- return value.map(qwikDebugToString);
- }
- } else if (isSignal(value)) {
- if (value instanceof WrappedSignalImpl) {
- return 'WrappedSignal';
- } else if (value instanceof ComputedSignalImpl) {
- return 'ComputedSignal';
- } else {
- return 'Signal';
+ try {
+ if (value === null) {
+ return 'null';
+ } else if (value === undefined) {
+ return 'undefined';
+ } else if (typeof value === 'string') {
+ return '"' + value + '"';
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value);
+ } else if (isTask(value)) {
+ return `Task(${qwikDebugToString(value.$qrl$)})`;
+ } else if (isQrl(value)) {
+ return `Qrl(${value.$symbol$})`;
+ } else if (typeof value === 'object' || typeof value === 'function') {
+ if (stringifyPath.includes(value)) {
+ return '*';
+ }
+ if (stringifyPath.length > 10) {
+ // debugger;
+ }
+ try {
+ stringifyPath.push(value);
+ if (Array.isArray(value)) {
+ if (vnode_isVNode(value)) {
+ return '(' + (vnode_getProp(value, DEBUG_TYPE, null) || 'vnode') + ')';
+ } else {
+ return value.map(qwikDebugToString);
+ }
+ } else if (isSignal(value)) {
+ if (value instanceof WrappedSignalImpl) {
+ return 'WrappedSignal';
+ } else if (value instanceof ComputedSignalImpl) {
+ return 'ComputedSignal';
+ } else {
+ return 'Signal';
+ }
+ } else if (isStore(value)) {
+ return 'Store';
+ } else if (isJSXNode(value)) {
+ return jsxToString(value);
+ } else if (vnode_isVNode(value)) {
+ return '(' + (vnode_getProp(value, DEBUG_TYPE, null) || 'vnode') + ')';
}
- } else if (isStore(value)) {
- return 'Store';
- } else if (isJSXNode(value)) {
- return jsxToString(value);
- } else if (vnode_isVNode(value)) {
- return '(' + value.getProp(DEBUG_TYPE, null) + ')';
+ } finally {
+ stringifyPath.pop();
}
- } finally {
- stringifyPath.pop();
}
+ } catch (e) {
+ console.error('ERROR in qwikDebugToString', e);
+ return '*error*';
}
return value;
}
@@ -69,6 +75,14 @@ export const pad = (text: string, prefix: string) => {
export const jsxToString = (value: any): string => {
if (isJSXNode(value)) {
+ if (typeof value.type === 'function') {
+ const componentMeta = (value.type as any)[SERIALIZABLE_STATE];
+ if (componentMeta) {
+ const [componentQRL] = componentMeta;
+ return 'Component(' + componentQRL.$symbol$ + ')';
+ }
+ return 'Function(' + value.type.name + ')';
+ }
let str = '<' + value.type;
if (value.props) {
for (const [key, val] of Object.entries(value.props)) {
diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts
index 317dff3543d..7fd4bcb07ac 100644
--- a/packages/qwik/src/core/internal.ts
+++ b/packages/qwik/src/core/internal.ts
@@ -20,23 +20,21 @@ export {
vnode_ensureElementInflated as _vnode_ensureElementInflated,
vnode_getAttrKeys as _vnode_getAttrKeys,
vnode_getFirstChild as _vnode_getFirstChild,
- vnode_getProps as _vnode_getProps,
vnode_isMaterialized as _vnode_isMaterialized,
vnode_isTextVNode as _vnode_isTextVNode,
vnode_isVirtualVNode as _vnode_isVirtualVNode,
vnode_toString as _vnode_toString,
-} from './client/vnode';
-export type {
- ElementVNode as _ElementVNode,
- TextVNode as _TextVNode,
- VirtualVNode as _VirtualVNode,
- VNode as _VNode,
-} from './client/vnode-impl';
+} from './client/vnode-utils';
+export type { VNode as _VNode } from './shared/vnode/vnode';
+export type { ElementVNode as _ElementVNode } from './shared/vnode/element-vnode';
+export type { TextVNode as _TextVNode } from './shared/vnode/text-vnode';
+export type { VirtualVNode as _VirtualVNode } from './shared/vnode/virtual-vnode';
+export { _executeSsrChores } from './shared/cursor/ssr-chore-execution';
export { _hasStoreEffects, isStore as _isStore } from './reactive-primitives/impl/store';
export { _wrapProp, _wrapSignal } from './reactive-primitives/internal-api';
export { SubscriptionData as _SubscriptionData } from './reactive-primitives/subscription-data';
-export { _EFFECT_BACK_REF } from './reactive-primitives/types';
+export { _EFFECT_BACK_REF } from './reactive-primitives/backref';
export {
isStringifiable as _isStringifiable,
type Stringifiable as _Stringifiable,
diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md
index 536b668d225..8cfa094b8e0 100644
--- a/packages/qwik/src/core/qwik.core.api.md
+++ b/packages/qwik/src/core/qwik.core.api.md
@@ -41,14 +41,8 @@ export type ClassList = string | undefined | null | false | Record | null;
- // Warning: (ae-forgotten-export) The symbol "VNodeJournal" needs to be exported by the entry point index.d.ts
- //
- // (undocumented)
- $journal$: VNodeJournal;
// (undocumented)
$locale$: string;
// (undocumented)
@@ -219,16 +213,15 @@ class DomContainer extends _SharedContainer implements ClientContainer {
$forwardRefs$: Array | null;
// (undocumented)
$getObjectById$: (id: number | string) => unknown;
+ $hoistStyles$(): void;
// (undocumented)
$instanceHash$: string;
// (undocumented)
- $journal$: VNodeJournal;
- // (undocumented)
$qFuncs$: Array<(...args: unknown[]) => unknown>;
// (undocumented)
$rawStateData$: unknown[];
// (undocumented)
- $setRawState$(id: number, vParent: _ElementVNode | _VirtualVNode): void;
+ $setRawState$(id: number, vParent: _VNode): void;
// Warning: (ae-forgotten-export) The symbol "ObjToProxyMap" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -280,17 +273,18 @@ export const _EFFECT_BACK_REF: unique symbol;
// @internal (undocumented)
export class _ElementVNode extends _VNode {
- constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined, element: QElement, elementName: string | undefined);
- // Warning: (ae-forgotten-export) The symbol "QElement" needs to be exported by the entry point index.d.ts
- //
- // (undocumented)
- element: QElement;
+ // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
+ constructor(key: string | null, flags: _VNodeFlags, parent: _VNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined, node: Element, elementName: string | undefined);
// (undocumented)
elementName: string | undefined;
// (undocumented)
firstChild: _VNode | null | undefined;
// (undocumented)
+ key: string | null;
+ // (undocumented)
lastChild: _VNode | null | undefined;
+ // (undocumented)
+ node: Element;
}
// @internal (undocumented)
@@ -315,6 +309,12 @@ export type EventHandler = {
// @internal (undocumented)
export const eventQrl: (qrl: QRL) => QRL;
+// Warning: (ae-forgotten-export) The symbol "SSRContainer" needs to be exported by the entry point index.d.ts
+// Warning: (ae-forgotten-export) The symbol "ISsrNode" needs to be exported by the entry point index.d.ts
+//
+// @internal (undocumented)
+export function _executeSsrChores(container: SSRContainer, ssrNode: ISsrNode): ValueOrPromise;
+
// Warning: (ae-forgotten-export) The symbol "WrappedSignalImpl" needs to be exported by the entry point index.d.ts
//
// @internal (undocumented)
@@ -337,7 +337,6 @@ export type FunctionComponent = {
}['renderFn'];
// Warning: (ae-forgotten-export) The symbol "PropsProxy" needs to be exported by the entry point index.d.ts
-// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
//
// @internal
export const _getConstProps: (props: PropsProxy | Record | null | undefined) => Props | null;
@@ -351,11 +350,10 @@ export const _getContextElement: () => unknown;
// @internal (undocumented)
export const _getContextEvent: () => unknown;
-// Warning: (ae-incompatible-release-tags) The symbol "getDomContainer" is marked as @public, but its signature references "_VNode" which is marked as @internal
// Warning: (ae-incompatible-release-tags) The symbol "getDomContainer" is marked as @public, but its signature references "ClientContainer" which is marked as @internal
//
// @public (undocumented)
-function getDomContainer(element: Element | _VNode): ClientContainer;
+function getDomContainer(element: Element): ClientContainer;
export { getDomContainer as _getDomContainer }
export { getDomContainer }
@@ -366,7 +364,7 @@ export function getLocale(defaultLocale?: string): string;
export const getPlatform: () => CorePlatform;
// @internal (undocumented)
-export function _getQContainerElement(element: Element | _VNode): Element | null;
+export function _getQContainerElement(element: Element): Element | null;
// @internal
export const _getVarProps: (props: PropsProxy | Record | null | undefined) => Props | null;
@@ -417,8 +415,6 @@ export const isSignal: (value: any) => value is Signal;
//
// @internal (undocumented)
export interface ISsrComponentFrame {
- // Warning: (ae-forgotten-export) The symbol "ISsrNode" needs to be exported by the entry point index.d.ts
- //
// (undocumented)
componentNode: ISsrNode;
// (undocumented)
@@ -455,7 +451,7 @@ export const _isTask: (value: any) => value is Task;
// @public
export const jsx: >(type: T, props: T extends FunctionComponent ? PROPS : Props, key?: string | number | null, _isStatic?: boolean, dev?: JsxDevOpts) => JSXNode;
-// @internal (undocumented)
+// @internal @deprecated (undocumented)
export const _jsxBranch: (input?: T) => T | undefined;
// @internal @deprecated (undocumented)
@@ -930,24 +926,24 @@ export abstract class _SharedContainer implements Container {
// (undocumented)
$currentUniqueId$: number;
// (undocumented)
- $flushEpoch$: number;
+ $cursorCount$: number;
// (undocumented)
readonly $getObjectById$: (id: number | string) => any;
// (undocumented)
$instanceHash$: string | null;
// (undocumented)
readonly $locale$: string;
- // Warning: (ae-forgotten-export) The symbol "Scheduler" needs to be exported by the entry point index.d.ts
- //
// (undocumented)
- readonly $scheduler$: Scheduler;
+ $renderPromise$: Promise | null;
+ // (undocumented)
+ $resolveRenderPromise$: (() => void) | null;
// (undocumented)
$serverData$: Record;
// (undocumented)
readonly $storeProxyMap$: ObjToProxyMap;
// (undocumented)
readonly $version$: string;
- constructor(journalFlush: () => void, serverData: Record, locale: string);
+ constructor(serverData: Record, locale: string);
// (undocumented)
abstract ensureProjectionResolved(host: HostElement): void;
// (undocumented)
@@ -1669,11 +1665,11 @@ export interface TaskOptions {
// @internal (undocumented)
export class _TextVNode extends _VNode {
- constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, textNode: Text | null, text: string | undefined);
+ constructor(flags: _VNodeFlags, parent: _VNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null, node: Text | null, text: string | undefined);
// (undocumented)
- text: string | undefined;
+ node: Text | null;
// (undocumented)
- textNode: Text | null;
+ text: string | undefined;
}
// @public
@@ -1840,10 +1836,12 @@ export const version: string;
// @internal (undocumented)
export class _VirtualVNode extends _VNode {
- constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined);
+ constructor(key: string | null, flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined);
// (undocumented)
firstChild: _VNode | null | undefined;
// (undocumented)
+ key: string | null;
+ // (undocumented)
lastChild: _VNode | null | undefined;
}
@@ -1854,31 +1852,25 @@ export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | '
//
// @internal (undocumented)
export abstract class _VNode extends BackRef {
- constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined);
- // (undocumented)
- blockedChores: ChoreArray | null;
- // Warning: (ae-forgotten-export) The symbol "ChoreArray" needs to be exported by the entry point index.d.ts
+ constructor(flags: _VNodeFlags, parent: _VNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null);
+ // Warning: (ae-forgotten-export) The symbol "ChoreBits" needs to be exported by the entry point index.d.ts
//
// (undocumented)
- chores: ChoreArray | null;
+ dirty: ChoreBits;
// (undocumented)
- flags: _VNodeFlags;
+ dirtyChildren: _VNode[] | null;
// (undocumented)
- getAttr(key: string): string | null;
+ flags: _VNodeFlags;
// (undocumented)
- getProp(key: string, getObject: ((id: string) => any) | null): T | null;
+ nextDirtyChildIndex: number;
// (undocumented)
nextSibling: _VNode | null | undefined;
// (undocumented)
- parent: _ElementVNode | _VirtualVNode | null;
+ parent: _VNode | null;
// (undocumented)
previousSibling: _VNode | null | undefined;
// (undocumented)
- props: unknown[] | null;
- // (undocumented)
- setAttr(key: string, value: string | null | boolean, journal: VNodeJournal | null): void;
- // (undocumented)
- setProp(key: string, value: any): void;
+ props: Props | null;
// (undocumented)
slotParent: _VNode | null;
// (undocumented)
@@ -1894,9 +1886,6 @@ export const _vnode_getAttrKeys: (vnode: _ElementVNode | _VirtualVNode) => strin
// @internal (undocumented)
export const _vnode_getFirstChild: (vnode: _VNode) => _VNode | null;
-// @internal (undocumented)
-export const _vnode_getProps: (vnode: _ElementVNode | _VirtualVNode) => unknown[];
-
// @internal (undocumented)
export const _vnode_isMaterialized: (vNode: _VNode) => boolean;
@@ -1911,6 +1900,8 @@ export function _vnode_toString(this: _VNode | null, depth?: number, offset?: st
// @internal
export const enum _VNodeFlags {
+ // (undocumented)
+ Cursor = 64,
// (undocumented)
Deleted = 32,
// (undocumented)
@@ -1924,15 +1915,15 @@ export const enum _VNodeFlags {
// (undocumented)
INFLATED_TYPE_MASK = 15,
// (undocumented)
- NAMESPACE_MASK = 192,
+ NAMESPACE_MASK = 384,
// (undocumented)
- NEGATED_NAMESPACE_MASK = -193,
+ NEGATED_NAMESPACE_MASK = -385,
// (undocumented)
NS_html = 0,
// (undocumented)
- NS_math = 128,
+ NS_math = 256,
// (undocumented)
- NS_svg = 64,
+ NS_svg = 128,
// (undocumented)
Resolved = 16,
// (undocumented)
@@ -1946,8 +1937,6 @@ export const enum _VNodeFlags {
// @internal (undocumented)
export const _waitUntilRendered: (elm: Element) => Promise;
-// Warning: (ae-forgotten-export) The symbol "SSRContainer" needs to be exported by the entry point index.d.ts
-//
// @internal (undocumented)
export function _walkJSX(ssr: SSRContainer, value: JSXOutput, options: {
currentStyleScoped: string | null;
diff --git a/packages/qwik/src/core/reactive-primitives/backref.ts b/packages/qwik/src/core/reactive-primitives/backref.ts
new file mode 100644
index 00000000000..ee9a49d0778
--- /dev/null
+++ b/packages/qwik/src/core/reactive-primitives/backref.ts
@@ -0,0 +1,7 @@
+/** @internal */
+export const _EFFECT_BACK_REF = Symbol('backRef');
+
+/** Class for back reference to the EffectSubscription */
+export abstract class BackRef {
+ [_EFFECT_BACK_REF]: Map | undefined = undefined;
+}
diff --git a/packages/qwik/src/core/reactive-primitives/cleanup.ts b/packages/qwik/src/core/reactive-primitives/cleanup.ts
index b8a192e76ea..517fa8822b7 100644
--- a/packages/qwik/src/core/reactive-primitives/cleanup.ts
+++ b/packages/qwik/src/core/reactive-primitives/cleanup.ts
@@ -1,23 +1,13 @@
-import { ensureMaterialized, vnode_isElementVNode, vnode_isVNode } from '../client/vnode';
+import { ensureMaterialized, vnode_isElementVNode, vnode_isVNode } from '../client/vnode-utils';
import type { Container } from '../shared/types';
import { SignalImpl } from './impl/signal-impl';
import { WrappedSignalImpl } from './impl/wrapped-signal-impl';
import { StoreHandler, getStoreHandler } from './impl/store';
-import {
- EffectSubscriptionProp,
- _EFFECT_BACK_REF,
- type Consumer,
- type EffectProperty,
- type EffectSubscription,
-} from './types';
import { AsyncComputedSignalImpl } from './impl/async-computed-signal-impl';
-import { isPropsProxy, type PropsProxyHandler } from '../shared/jsx/props-proxy';
import { _PROPS_HANDLER } from '../shared/utils/constants';
-
-/** Class for back reference to the EffectSubscription */
-export abstract class BackRef {
- [_EFFECT_BACK_REF]: Map | undefined = undefined;
-}
+import { BackRef, _EFFECT_BACK_REF } from './backref';
+import { EffectSubscriptionProp, type Consumer, type EffectSubscription } from './types';
+import { isPropsProxy, type PropsProxyHandler } from '../shared/jsx/props-proxy';
export function clearAllEffects(container: Container, consumer: Consumer): void {
if (vnode_isVNode(consumer) && vnode_isElementVNode(consumer)) {
diff --git a/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts
index 46b6e694af3..6af91e49de7 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts
+++ b/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts
@@ -1,13 +1,11 @@
import { qwikDebugToString } from '../../debug';
import type { NoSerialize } from '../../shared/serdes/verify';
import type { Container } from '../../shared/types';
-import { ChoreType } from '../../shared/util-chore-type';
import { isPromise, retryOnPromise } from '../../shared/utils/promises';
import { cleanupDestroyable } from '../../use/utils/destroyable';
import { cleanupFn, trackFn } from '../../use/utils/tracker';
-import type { BackRef } from '../cleanup';
+import { _EFFECT_BACK_REF, type BackRef } from '../backref';
import {
- _EFFECT_BACK_REF,
AsyncComputeQRL,
EffectProperty,
EffectSubscription,
@@ -69,12 +67,8 @@ export class AsyncComputedSignalImpl
set untrackedLoading(value: boolean) {
if (value !== this.$untrackedLoading$) {
this.$untrackedLoading$ = value;
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- this.$loadingEffects$
- );
+ DEBUG && log('Set untrackedLoading', value);
+ scheduleEffects(this.$container$, this, this.$loadingEffects$);
}
}
@@ -94,12 +88,7 @@ export class AsyncComputedSignalImpl
set untrackedError(value: Error | undefined) {
if (value !== this.$untrackedError$) {
this.$untrackedError$ = value;
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- this.$errorEffects$
- );
+ scheduleEffects(this.$container$, this, this.$errorEffects$);
}
}
@@ -108,9 +97,9 @@ export class AsyncComputedSignalImpl
}
override invalidate() {
- super.invalidate();
// clear the promise, we need to get function again
this.$promise$ = null;
+ super.invalidate();
}
async promise(): Promise {
@@ -151,6 +140,8 @@ export class AsyncComputedSignalImpl
this.untrackedError = undefined;
if (this.setValue(promiseValue)) {
DEBUG && log('Scheduling effects for subscribers', this.$effects$?.size);
+
+ this.$flags$ &= ~SignalFlags.RUN_EFFECTS;
scheduleEffects(this.$container$, this, this.$effects$);
}
})
diff --git a/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts
index cd29db54ad2..e6387844b2d 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts
+++ b/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts
@@ -2,16 +2,15 @@ import { qwikDebugToString } from '../../debug';
import { assertFalse } from '../../shared/error/assert';
import { QError, qError } from '../../shared/error/error';
import type { Container } from '../../shared/types';
-import { ChoreType } from '../../shared/util-chore-type';
-import { isPromise } from '../../shared/utils/promises';
+import { isPromise, maybeThen, retryOnPromise } from '../../shared/utils/promises';
import { tryGetInvokeContext } from '../../use/use-core';
-import { throwIfQRLNotResolved } from '../utils';
-import type { BackRef } from '../cleanup';
+import { scheduleEffects, throwIfQRLNotResolved } from '../utils';
import { getSubscriber } from '../subscriber';
import { SerializationSignalFlags, ComputeQRL, EffectSubscription } from '../types';
-import { _EFFECT_BACK_REF, EffectProperty, NEEDS_COMPUTATION, SignalFlags } from '../types';
+import { EffectProperty, NEEDS_COMPUTATION, SignalFlags } from '../types';
import { SignalImpl } from './signal-impl';
import type { QRLInternal } from '../../shared/qrl/qrl-class';
+import { _EFFECT_BACK_REF, type BackRef } from '../backref';
const DEBUG = false;
// eslint-disable-next-line no-console
@@ -53,11 +52,14 @@ export class ComputedSignalImpl>
invalidate() {
this.$flags$ |= SignalFlags.INVALID;
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- this.$effects$
+ maybeThen(
+ retryOnPromise(() => this.$computeIfNeeded$()),
+ () => {
+ if (this.$flags$ & SignalFlags.RUN_EFFECTS) {
+ this.$flags$ &= ~SignalFlags.RUN_EFFECTS;
+ scheduleEffects(this.$container$, this, this.$effects$);
+ }
+ }
);
}
diff --git a/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts
index 3c59a0cb242..b47b10574c2 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts
+++ b/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts
@@ -9,10 +9,10 @@ import {
addQrlToSerializationCtx,
ensureContainsBackRef,
ensureContainsSubscription,
+ scheduleEffects,
} from '../utils';
import type { Signal } from '../signal.public';
import { SignalFlags, type EffectSubscription } from '../types';
-import { ChoreType } from '../../shared/util-chore-type';
import type { WrappedSignalImpl } from './wrapped-signal-impl';
const DEBUG = false;
@@ -38,12 +38,7 @@ export class SignalImpl implements Signal {
* remained the same object
*/
force() {
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- this.$effects$
- );
+ scheduleEffects(this.$container$, this, this.$effects$);
}
get untrackedValue() {
@@ -68,12 +63,7 @@ export class SignalImpl implements Signal {
DEBUG &&
log('Signal.set', this.$untrackedValue$, '->', value, pad('\n' + this.toString(), ' '));
this.$untrackedValue$ = value;
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- this.$effects$
- );
+ scheduleEffects(this.$container$, this, this.$effects$);
}
}
diff --git a/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx b/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx
index f8e4fd0c6af..9dd6a507253 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx
+++ b/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx
@@ -1,12 +1,11 @@
import { $, _wrapProp, isBrowser } from '@qwik.dev/core';
-import { createDocument, getTestPlatform } from '@qwik.dev/core/testing';
+import { createDocument } from '@qwik.dev/core/testing';
import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from 'vitest';
import { getDomContainer } from '../../client/dom-container';
import { implicit$FirstArg } from '../../shared/qrl/implicit_dollar';
import { inlinedQrl } from '../../shared/qrl/qrl';
import { type QRLInternal } from '../../shared/qrl/qrl-class';
import { type QRL } from '../../shared/qrl/qrl.public';
-import { ChoreType } from '../../shared/util-chore-type';
import type { Container, HostElement } from '../../shared/types';
import { retryOnPromise } from '../../shared/utils/promises';
import { invoke, newInvokeContext } from '../../use/use-core';
@@ -27,6 +26,8 @@ import {
type Signal,
} from '../signal.public';
import { getSubscriber } from '../subscriber';
+import { vnode_newVirtual, vnode_setProp } from '../../client/vnode-utils';
+import { ELEMENT_SEQ } from '../../shared/utils/markers';
class Foo {
constructor(public val: number = 0) {}
@@ -118,8 +119,7 @@ describe('signal', () => {
afterEach(async () => {
delayMap.clear();
- await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
- await getTestPlatform().flush();
+ await container.$renderPromise$;
container = null!;
});
@@ -189,9 +189,9 @@ describe('signal', () => {
await withContainer(async () => {
const a = createSignal(true) as InternalSignal;
const b = createSignal(true) as InternalSignal;
- await retryOnPromise(async () => {
- let signal!: InternalReadonlySignal;
- effect$(() => {
+ let signal!: InternalReadonlySignal;
+ await retryOnPromise(() =>
+ effect$(async () => {
signal =
signal ||
createComputedQrl(
@@ -201,14 +201,13 @@ describe('signal', () => {
})
)
);
- log.push(signal.value); // causes subscription
- });
- expect(log).toEqual([true]);
- a.value = !a.untrackedValue;
- await flushSignals();
- b.value = !b.untrackedValue;
- });
- await flushSignals();
+ const signalValue = await retryOnPromise(() => signal.value);
+ log.push(signalValue); // causes subscription
+ })
+ );
+ expect(log).toEqual([true]);
+ a.value = !a.untrackedValue;
+ b.value = !b.untrackedValue;
expect(log).toEqual([true, false]);
});
});
@@ -264,7 +263,7 @@ describe('signal', () => {
}
async function flushSignals() {
- await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
+ await container.$renderPromise$;
}
/** Simulates the QRLs being lazy loaded once per test. */
@@ -286,8 +285,9 @@ describe('signal', () => {
function effectQrl(fnQrl: QRL<() => void>) {
const qrl = fnQrl as QRLInternal<() => void>;
- const element: HostElement = null!;
+ const element: HostElement = vnode_newVirtual();
const task = new Task(0, 0, element, fnQrl as QRLInternal, undefined, null);
+ vnode_setProp(element, ELEMENT_SEQ, [task]);
if (!qrl.resolved) {
throw qrl.resolve();
} else {
diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.ts b/packages/qwik/src/core/reactive-primitives/impl/store.ts
index 391f3a9d933..2e6903ff514 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/store.ts
+++ b/packages/qwik/src/core/reactive-primitives/impl/store.ts
@@ -7,6 +7,7 @@ import {
addQrlToSerializationCtx,
ensureContainsBackRef,
ensureContainsSubscription,
+ scheduleEffects,
} from '../utils';
import {
STORE_ALL_PROPS,
@@ -16,7 +17,6 @@ import {
type EffectSubscription,
type StoreTarget,
} from '../types';
-import { ChoreType } from '../../shared/util-chore-type';
import type { PropsProxy, PropsProxyHandler } from '../../shared/jsx/props-proxy';
const DEBUG = false;
@@ -109,12 +109,7 @@ export class StoreHandler implements ProxyHandler {
force(prop: keyof StoreTarget): void {
const target = getStoreTarget(this)!;
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- getEffects(target, prop, this.$effects$)
- );
+ scheduleEffects(this.$container$, this, getEffects(target, prop, this.$effects$));
}
get(target: StoreTarget, prop: string | symbol) {
@@ -199,12 +194,7 @@ export class StoreHandler implements ProxyHandler {
if (!Array.isArray(target)) {
// If the target is an array, we don't need to trigger effects.
// Changing the length property will trigger effects.
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- this,
- getEffects(target, prop, this.$effects$)
- );
+ scheduleEffects(this.$container$, this, getEffects(target, prop, this.$effects$));
}
return true;
}
@@ -292,12 +282,7 @@ function setNewValueAndTriggerEffects>(
(target as any)[prop] = value;
const effects = getEffects(target, prop, currentStore.$effects$);
if (effects) {
- currentStore.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- currentStore,
- effects
- );
+ scheduleEffects(currentStore.$container$, currentStore, effects);
}
}
diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx b/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx
index e4b1f2a292b..002a0019dc9 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx
+++ b/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx
@@ -1,28 +1,29 @@
import { getDomContainer, implicit$FirstArg, type QRL } from '@qwik.dev/core';
-import { createDocument, getTestPlatform } from '@qwik.dev/core/testing';
+import { createDocument } from '@qwik.dev/core/testing';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { Container, HostElement } from '../../shared/types';
import { getOrCreateStore, isStore } from './store';
import { EffectProperty, StoreFlags } from '../types';
import { invoke } from '../../use/use-core';
import { newInvokeContext } from '../../use/use-core';
-import { ChoreType } from '../../shared/util-chore-type';
import type { QRLInternal } from '../../shared/qrl/qrl-class';
import { Task } from '../../use/use-task';
import { getSubscriber } from '../subscriber';
+import { vnode_newVirtual, vnode_setProp } from '../../client/vnode-utils';
+import { ELEMENT_SEQ } from 'packages/qwik/src/server/qwik-copy';
describe('v2/store', () => {
const log: any[] = [];
let container: Container = null!;
+ let document: Document = null!;
beforeEach(() => {
log.length = 0;
- const document = createDocument({ html: '' });
+ document = createDocument({ html: '' });
container = getDomContainer(document.body);
});
afterEach(async () => {
- await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
- await getTestPlatform().flush();
+ await container.$renderPromise$;
container = null!;
});
@@ -61,13 +62,14 @@ describe('v2/store', () => {
}
async function flushSignals() {
- await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
+ await container.$renderPromise$;
}
function effectQrl(fnQrl: QRL<() => void>) {
const qrl = fnQrl as QRLInternal<() => void>;
- const element: HostElement = null!;
+ const element: HostElement = vnode_newVirtual();
const task = new Task(0, 0, element, fnQrl as QRLInternal, undefined, null);
+ vnode_setProp(element, ELEMENT_SEQ, [task]);
if (!qrl.resolved) {
throw qrl.resolve();
} else {
diff --git a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts
index 164ffdc39fc..fbab0d36acd 100644
--- a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts
+++ b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts
@@ -1,20 +1,16 @@
import { assertFalse } from '../../shared/error/assert';
import { QError, qError } from '../../shared/error/error';
import type { Container, HostElement } from '../../shared/types';
-import { ChoreType } from '../../shared/util-chore-type';
+import { ChoreBits } from '../../shared/vnode/enums/chore-bits.enum';
import { trackSignal } from '../../use/use-core';
-import type { BackRef } from '../cleanup';
import { getValueProp } from '../internal-api';
import type { AllSignalFlags, EffectSubscription } from '../types';
-import {
- _EFFECT_BACK_REF,
- EffectProperty,
- NEEDS_COMPUTATION,
- SignalFlags,
- WrappedSignalFlags,
-} from '../types';
+import { EffectProperty, NEEDS_COMPUTATION, SignalFlags, WrappedSignalFlags } from '../types';
import { isSignal, scheduleEffects } from '../utils';
import { SignalImpl } from './signal-impl';
+import { markVNodeDirty } from '../../shared/vnode/vnode-dirty';
+import { _EFFECT_BACK_REF, type BackRef } from '../backref';
+import { HOST_SIGNAL } from '../../shared/cursor/cursor-props';
export class WrappedSignalImpl extends SignalImpl implements BackRef {
$args$: any[];
@@ -48,12 +44,10 @@ export class WrappedSignalImpl extends SignalImpl implements BackRef {
try {
this.$computeIfNeeded$();
} catch (_) {
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- this.$hostElement$,
- this,
- this.$effects$
- );
+ if (this.$container$ && this.$hostElement$) {
+ this.$container$.setHostProp(this.$hostElement$ as HostElement, HOST_SIGNAL, this);
+ markVNodeDirty(this.$container$, this.$hostElement$, ChoreBits.COMPUTE);
+ }
}
// if the computation not failed, we can run the effects directly
if (this.$flags$ & SignalFlags.RUN_EFFECTS) {
@@ -68,12 +62,10 @@ export class WrappedSignalImpl extends SignalImpl implements BackRef {
*/
force() {
this.$flags$ |= SignalFlags.RUN_EFFECTS;
- this.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- this.$hostElement$,
- this,
- this.$effects$
- );
+ if (this.$container$ && this.$hostElement$) {
+ this.$container$.setHostProp(this.$hostElement$ as HostElement, HOST_SIGNAL, this);
+ markVNodeDirty(this.$container$, this.$hostElement$, ChoreBits.COMPUTE);
+ }
}
get untrackedValue() {
diff --git a/packages/qwik/src/core/reactive-primitives/subscriber.ts b/packages/qwik/src/core/reactive-primitives/subscriber.ts
index d46ef1223d8..47a60acd2c7 100644
--- a/packages/qwik/src/core/reactive-primitives/subscriber.ts
+++ b/packages/qwik/src/core/reactive-primitives/subscriber.ts
@@ -1,9 +1,9 @@
import { isServer } from '@qwik.dev/core/build';
import { QBackRefs } from '../shared/utils/markers';
import type { ISsrNode } from '../ssr/ssr-types';
-import { BackRef } from './cleanup';
import type { Consumer, EffectProperty, EffectSubscription } from './types';
-import { _EFFECT_BACK_REF, EffectSubscriptionProp } from './types';
+import { EffectSubscriptionProp } from './types';
+import { _EFFECT_BACK_REF, type BackRef } from './backref';
export function getSubscriber(
effect: Consumer,
diff --git a/packages/qwik/src/core/reactive-primitives/subscription-data.ts b/packages/qwik/src/core/reactive-primitives/subscription-data.ts
index 7d55126d3ac..da834e8d523 100644
--- a/packages/qwik/src/core/reactive-primitives/subscription-data.ts
+++ b/packages/qwik/src/core/reactive-primitives/subscription-data.ts
@@ -17,3 +17,9 @@ export class SubscriptionData {
this.data = data;
}
}
+
+export interface NodeProp {
+ isConst: boolean;
+ scopedStyleIdPrefix: string | null;
+ value: Signal | string;
+}
diff --git a/packages/qwik/src/core/reactive-primitives/types.ts b/packages/qwik/src/core/reactive-primitives/types.ts
index c874d17377e..b02dd4b4966 100644
--- a/packages/qwik/src/core/reactive-primitives/types.ts
+++ b/packages/qwik/src/core/reactive-primitives/types.ts
@@ -1,4 +1,3 @@
-import type { ISsrNode } from '../ssr/ssr-types';
import type { Task, Tracker } from '../use/use-task';
import type { SubscriptionData } from './subscription-data';
import type { ReadonlySignal } from './signal.public';
@@ -8,7 +7,8 @@ import type { SerializerSymbol } from '../shared/serdes/verify';
import type { ComputedFn } from '../use/use-computed';
import type { AsyncComputedFn } from '../use/use-async-computed';
import type { Container, SerializationStrategy } from '../shared/types';
-import type { VNode } from '../client/vnode-impl';
+import type { VNode } from '../shared/vnode/vnode';
+import type { ISsrNode } from '../ssr/ssr-types';
/**
* # ================================
@@ -24,9 +24,6 @@ import type { VNode } from '../client/vnode-impl';
*/
export const NEEDS_COMPUTATION: any = Symbol('invalid');
-/** @internal */
-export const _EFFECT_BACK_REF = Symbol('backRef');
-
export interface InternalReadonlySignal extends ReadonlySignal {
readonly untrackedValue: T;
}
@@ -77,7 +74,7 @@ export type AllSignalFlags = SignalFlags | WrappedSignalFlags | SerializationSig
* - `VNode` and `ISsrNode`: Either a component or ``
* - `Signal2`: A derived signal which contains a computation function.
*/
-export type Consumer = Task | VNode | ISsrNode | SignalImpl;
+export type Consumer = Task | VNode | SignalImpl | ISsrNode;
/**
* An effect consumer plus type of effect, back references to producers and additional data
diff --git a/packages/qwik/src/core/reactive-primitives/utils.ts b/packages/qwik/src/core/reactive-primitives/utils.ts
index 1e90d186af7..30935833c60 100644
--- a/packages/qwik/src/core/reactive-primitives/utils.ts
+++ b/packages/qwik/src/core/reactive-primitives/utils.ts
@@ -1,14 +1,10 @@
import { isDomContainer } from '../client/dom-container';
-import { pad, qwikDebugToString } from '../debug';
-import type { OnRenderFn } from '../shared/component.public';
+import { qwikDebugToString } from '../debug';
import { assertDefined } from '../shared/error/assert';
-import type { Props } from '../shared/jsx/jsx-runtime';
import { isServerPlatform } from '../shared/platform/platform';
-import { type QRLInternal } from '../shared/qrl/qrl-class';
import type { QRL } from '../shared/qrl/qrl.public';
-import type { Container, HostElement, SerializationStrategy } from '../shared/types';
-import { ChoreType } from '../shared/util-chore-type';
-import { ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers';
+import type { Container, SerializationStrategy } from '../shared/types';
+import { OnRenderProp } from '../shared/utils/markers';
import { SerializerSymbol } from '../shared/serdes/verify';
import { isObject } from '../shared/utils/types';
import type { ISsrNode, SSRContainer } from '../ssr/ssr-types';
@@ -17,7 +13,7 @@ import { ComputedSignalImpl } from './impl/computed-signal-impl';
import { SignalImpl } from './impl/signal-impl';
import type { WrappedSignalImpl } from './impl/wrapped-signal-impl';
import type { Signal } from './signal.public';
-import { SubscriptionData, type NodePropPayload } from './subscription-data';
+import { SubscriptionData, type NodeProp } from './subscription-data';
import {
SerializationSignalFlags,
EffectProperty,
@@ -27,6 +23,11 @@ import {
type EffectSubscription,
type StoreTarget,
} from './types';
+import { markVNodeDirty } from '../shared/vnode/vnode-dirty';
+import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum';
+import { setNodeDiffPayload, setNodePropData } from '../shared/cursor/chore-execution';
+import type { VNode } from '../shared/vnode/vnode';
+import { NODE_PROPS_DATA_KEY } from '../shared/cursor/cursor-props';
const DEBUG = false;
@@ -77,7 +78,7 @@ export const addQrlToSerializationCtx = (
} else if (effect instanceof ComputedSignalImpl) {
qrl = effect.$computeQrl$;
} else if (property === EffectProperty.COMPONENT) {
- qrl = container.getHostProp(effect as ISsrNode, OnRenderProp);
+ qrl = container.getHostProp(effect as VNode, OnRenderProp);
}
if (qrl) {
(container as SSRContainer).serializationCtx.$eventQrls$.add(qrl);
@@ -98,45 +99,38 @@ export const scheduleEffects = (
assertDefined(container, 'Container must be defined.');
if (isTask(consumer)) {
consumer.$flags$ |= TaskFlags.DIRTY;
- DEBUG && log('schedule.consumer.task', pad('\n' + String(consumer), ' '));
- let choreType = ChoreType.TASK;
- if (consumer.$flags$ & TaskFlags.VISIBLE_TASK) {
- choreType = ChoreType.VISIBLE;
- }
- container.$scheduler$(choreType, consumer);
+ markVNodeDirty(container, consumer.$el$, ChoreBits.TASKS);
} else if (consumer instanceof SignalImpl) {
- // we don't schedule ComputedSignal/DerivedSignal directly, instead we invalidate it and
- // and schedule the signals effects (recursively)
- if (consumer instanceof ComputedSignalImpl) {
- // Ensure that the computed signal's QRL is resolved.
- // If not resolved schedule it to be resolved.
- if (!consumer.$computeQrl$.resolved) {
- container.$scheduler$(ChoreType.QRL_RESOLVE, null, consumer.$computeQrl$);
- }
- }
-
(consumer as ComputedSignalImpl | WrappedSignalImpl).invalidate();
} else if (property === EffectProperty.COMPONENT) {
- const host: HostElement = consumer as any;
- 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);
+ markVNodeDirty(container, consumer, ChoreBits.COMPONENT);
} else if (property === EffectProperty.VNODE) {
if (isBrowser) {
- const host: HostElement = consumer;
- container.$scheduler$(ChoreType.NODE_DIFF, host, host, signal as SignalImpl);
+ setNodeDiffPayload(consumer as VNode, signal as Signal);
+ markVNodeDirty(container, consumer, ChoreBits.NODE_DIFF);
}
} else {
- const host: HostElement = consumer;
const effectData = effectSubscription[EffectSubscriptionProp.DATA];
if (effectData instanceof SubscriptionData) {
const data = effectData.data;
- const payload: NodePropPayload = {
- ...data,
- $value$: signal as SignalImpl,
+ const payload: NodeProp = {
+ isConst: data.$isConst$,
+ scopedStyleIdPrefix: data.$scopedStyleIdPrefix$,
+ value: signal as SignalImpl,
};
- container.$scheduler$(ChoreType.NODE_PROP, host, property, payload);
+ if (isBrowser) {
+ setNodePropData(consumer as VNode, property, payload);
+ } else {
+ const node = consumer as ISsrNode;
+ let data = node.getProp(NODE_PROPS_DATA_KEY) as Map | null;
+ if (!data) {
+ data = new Map();
+ node.setProp(NODE_PROPS_DATA_KEY, data);
+ }
+ data.set(property, payload);
+ (consumer as ISsrNode).setProp(property, payload);
+ }
+ markVNodeDirty(container, consumer, ChoreBits.NODE_PROPS);
}
}
};
diff --git a/packages/qwik/src/core/shared/component-execution.ts b/packages/qwik/src/core/shared/component-execution.ts
index 31613b7b426..f010fe51f79 100644
--- a/packages/qwik/src/core/shared/component-execution.ts
+++ b/packages/qwik/src/core/shared/component-execution.ts
@@ -1,8 +1,13 @@
import { isDev } from '@qwik.dev/core/build';
-import { vnode_isVNode } from '../client/vnode';
+import { vnode_isVNode } from '../client/vnode-utils';
import { isSignal } from '../reactive-primitives/utils';
import { clearAllEffects } from '../reactive-primitives/cleanup';
-import { invokeApply, newInvokeContext, untrack } from '../use/use-core';
+import {
+ invokeApply,
+ newRenderInvokeContext,
+ untrack,
+ type RenderInvokeContext,
+} from '../use/use-core';
import { type EventQRL, type UseOnMap } from '../use/use-on';
import { isQwikComponent, type OnRenderFn } from './component.public';
import { assertDefined } from './error/assert';
@@ -20,7 +25,6 @@ import {
ELEMENT_PROPS,
ELEMENT_SEQ_IDX,
OnRenderProp,
- RenderEvent,
USE_ON_LOCAL,
USE_ON_LOCAL_SEQ_IDX,
} from './utils/markers';
@@ -58,12 +62,11 @@ export const executeComponent = (
componentQRL: OnRenderFn | QRLInternal> | null,
props: Props | null
): ValueOrPromise => {
- const iCtx = newInvokeContext(
+ const iCtx = newRenderInvokeContext(
container.$locale$,
- subscriptionHost || undefined,
- undefined,
- RenderEvent
- );
+ subscriptionHost || renderHost,
+ container
+ ) as RenderInvokeContext;
if (subscriptionHost) {
iCtx.$effectSubscriber$ = getSubscriber(subscriptionHost, EffectProperty.COMPONENT);
iCtx.$container$ = container;
@@ -77,6 +80,7 @@ export const executeComponent = (
}
if (isQrl(componentQRL)) {
props = props || container.getHostProp(renderHost, ELEMENT_PROPS) || EMPTY_OBJ;
+ // TODO is this possible? JSXNode handles this, no?
if ('children' in props) {
delete props.children;
}
@@ -106,7 +110,7 @@ export const executeComponent = (
clearAllEffects(container, renderHost);
}
- return componentFn(props);
+ return maybeThen(componentFn(props), (jsx) => maybeThen(iCtx.$waitOn$, () => jsx));
},
(jsx) => {
const useOnEvents = container.getHostProp(renderHost, USE_ON_LOCAL);
diff --git a/packages/qwik/src/core/shared/cursor/chore-execution.ts b/packages/qwik/src/core/shared/cursor/chore-execution.ts
new file mode 100644
index 00000000000..a42a234591f
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/chore-execution.ts
@@ -0,0 +1,357 @@
+import { type VNodeJournal } from '../../client/vnode-utils';
+import { vnode_diff } from '../../client/vnode-diff';
+import { runResource, type ResourceDescriptor } from '../../use/use-resource';
+import { Task, TaskFlags, runTask, type TaskFn } from '../../use/use-task';
+import { executeComponent } from '../component-execution';
+import type { OnRenderFn } from '../component.public';
+import type { Props } from '../jsx/jsx-runtime';
+import type { QRLInternal } from '../qrl/qrl-class';
+import { ChoreBits } from '../vnode/enums/chore-bits.enum';
+import { ELEMENT_SEQ, ELEMENT_PROPS, OnRenderProp, QScopedStyle } from '../utils/markers';
+import { addComponentStylePrefix } from '../utils/scoped-styles';
+import { isPromise, maybeThen, retryOnPromise, safeCall } from '../utils/promises';
+import type { ValueOrPromise } from '../utils/types';
+import type { Container, HostElement } from '../types';
+import type { VNode } from '../vnode/vnode';
+import { VNodeFlags, type ClientContainer } from '../../client/types';
+import type { NodeProp } from '../../reactive-primitives/subscription-data';
+import { isSignal, scheduleEffects } from '../../reactive-primitives/utils';
+import type { Signal } from '../../reactive-primitives/signal.public';
+import { serializeAttribute } from '../utils/styles';
+import type { ElementVNode } from '../vnode/element-vnode';
+import { VNodeOperationType } from '../vnode/enums/vnode-operation-type.enum';
+import type { JSXOutput } from '../jsx/types/jsx-node';
+import {
+ HOST_SIGNAL,
+ NODE_DIFF_DATA_KEY,
+ NODE_PROPS_DATA_KEY,
+ type CursorData,
+} from './cursor-props';
+import { invoke, newInvokeContext } from '../../use/use-core';
+import type { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl';
+import { SignalFlags } from '../../reactive-primitives/types';
+import { cleanupDestroyable } from '../../use/utils/destroyable';
+import type { ISsrNode } from '../../ssr/ssr-types';
+
+/**
+ * Executes tasks for a vNode if the TASKS dirty bit is set. Tasks are stored in the ELEMENT_SEQ
+ * property and executed in order.
+ *
+ * Behavior:
+ *
+ * - Resources: Just run, don't save promise anywhere
+ * - Tasks: Chain promises only between each other
+ * - VisibleTasks: Store promises in afterFlush on cursor root for client, we need to wait for all
+ * visible tasks to complete before flushing changes to the DOM. On server, we keep them on vNode
+ * for streaming.
+ *
+ * @param vNode - The vNode to execute tasks for
+ * @param container - The container
+ * @param cursor - The cursor root vNode, should be set on client only
+ * @returns Promise if any regular task returns a promise, void otherwise
+ */
+export function executeTasks(
+ vNode: VNode,
+ container: Container,
+ cursorData: CursorData
+): ValueOrPromise {
+ vNode.dirty &= ~ChoreBits.TASKS;
+
+ const elementSeq = container.getHostProp(vNode, ELEMENT_SEQ);
+
+ if (!elementSeq || elementSeq.length === 0) {
+ // No tasks to execute, clear the bit
+ return;
+ }
+
+ // Execute all tasks in sequence
+ let taskPromise: Promise | undefined;
+
+ for (const item of elementSeq) {
+ if (item instanceof Task) {
+ const task = item as Task;
+
+ // Skip if task is not dirty
+ if (!(task.$flags$ & TaskFlags.DIRTY)) {
+ continue;
+ }
+
+ // Check if it's a resource
+ if (task.$flags$ & TaskFlags.RESOURCE) {
+ // Resources: just run, don't save promise anywhere
+ runResource(task as ResourceDescriptor, container, vNode);
+ } else if (task.$flags$ & TaskFlags.VISIBLE_TASK) {
+ // VisibleTasks: store for execution after flush (don't execute now)
+ (cursorData.afterFlushTasks ||= []).push(task);
+ } else {
+ // Regular tasks: chain promises only between each other
+ const isRenderBlocking = !!(task.$flags$ & TaskFlags.RENDER_BLOCKING);
+ // Set blocking flag before running task so signal changes during
+ // sync portion of task execution know to defer
+ if (isRenderBlocking) {
+ cursorData.isBlocking = true;
+ }
+ const result = runTask(task, container, vNode);
+ if (isPromise(result)) {
+ if (isRenderBlocking) {
+ taskPromise = taskPromise
+ ? taskPromise.then(() => result as Promise)
+ : (result as Promise);
+ } else {
+ // TODO: set extrapromises on vNode instead of cursorData if server
+ (cursorData.extraPromises ||= []).push(result as Promise);
+ }
+ } else if (isRenderBlocking) {
+ // Task completed synchronously, clear the blocking flag
+ cursorData.isBlocking = false;
+ }
+ }
+ }
+ }
+
+ return taskPromise;
+}
+
+function getNodeDiffPayload(vNode: VNode): JSXOutput | null {
+ const props = vNode.props as Props;
+ return props[NODE_DIFF_DATA_KEY] as JSXOutput | null;
+}
+
+export function setNodeDiffPayload(vNode: VNode, payload: JSXOutput | Signal): void {
+ const props = vNode.props as Props;
+ props[NODE_DIFF_DATA_KEY] = payload;
+}
+
+export function executeNodeDiff(
+ vNode: VNode,
+ container: Container,
+ journal: VNodeJournal
+): ValueOrPromise {
+ vNode.dirty &= ~ChoreBits.NODE_DIFF;
+
+ const domVNode = vNode as ElementVNode;
+ let jsx = getNodeDiffPayload(vNode);
+ if (!jsx) {
+ return;
+ }
+ if (isSignal(jsx)) {
+ jsx = jsx.value as any;
+ }
+ return vnode_diff(container as ClientContainer, journal, jsx, domVNode, null);
+}
+
+/**
+ * Executes a component for a vNode if the COMPONENT dirty bit is set. Gets the component QRL from
+ * OnRenderProp and executes it.
+ *
+ * @param vNode - The vNode to execute component for
+ * @param container - The container
+ * @returns Promise if component execution is async, void otherwise
+ */
+export function executeComponentChore(
+ vNode: VNode,
+ container: Container,
+ journal: VNodeJournal
+): ValueOrPromise {
+ vNode.dirty &= ~ChoreBits.COMPONENT;
+ const host = vNode as HostElement;
+ const componentQRL = container.getHostProp> | null>(
+ host,
+ OnRenderProp
+ );
+
+ if (!componentQRL) {
+ return;
+ }
+
+ const props = container.getHostProp(host, ELEMENT_PROPS) || null;
+
+ const result = safeCall(
+ () => executeComponent(container, host, host, componentQRL, props),
+ (jsx) => {
+ const styleScopedId = container.getHostProp(host, QScopedStyle);
+ return retryOnPromise(() =>
+ vnode_diff(
+ container as ClientContainer,
+ journal,
+ jsx,
+ host as VNode,
+ addComponentStylePrefix(styleScopedId)
+ )
+ );
+ },
+ (err: any) => {
+ container.handleError(err, host);
+ }
+ );
+
+ if (isPromise(result)) {
+ return result as Promise;
+ }
+
+ return;
+}
+
+/**
+ * Gets node prop data from a vNode.
+ *
+ * @param vNode - The vNode to get node prop data from
+ * @returns Array of NodeProp, or null if none
+ */
+function getNodePropData(vNode: VNode): Map | null {
+ const props = (vNode.props ||= {}) as Props;
+ return (props[NODE_PROPS_DATA_KEY] as Map | null) ?? null;
+}
+
+/**
+ * Sets node prop data for a vNode.
+ *
+ * @param vNode - The vNode to set node prop data for
+ * @param property - The property to set node prop data for
+ * @param nodeProp - The node prop data to set
+ */
+export function setNodePropData(vNode: VNode, property: string, nodeProp: NodeProp): void {
+ const props = (vNode.props ||= {}) as Props;
+ let data = props[NODE_PROPS_DATA_KEY] as Map | null;
+ if (!data) {
+ data = new Map();
+ props[NODE_PROPS_DATA_KEY] = data;
+ }
+ data.set(property, nodeProp);
+}
+
+/**
+ * Clears node prop data from a vNode.
+ *
+ * @param vNode - The vNode to clear node prop data from
+ */
+function clearNodePropData(vNode: VNode): void {
+ const props = (vNode.props ||= {}) as Props;
+ delete props[NODE_PROPS_DATA_KEY];
+}
+
+function setNodeProp(
+ domVNode: ElementVNode,
+ journal: VNodeJournal,
+ property: string,
+ value: string | boolean | null,
+ isConst: boolean
+): void {
+ journal.push({
+ operationType: VNodeOperationType.SetAttribute,
+ target: domVNode.node!,
+ attrName: property,
+ attrValue: value,
+ });
+ if (!isConst) {
+ if (domVNode.props && value == null) {
+ delete domVNode.props[property];
+ } else {
+ (domVNode.props ||= {})[property] = value;
+ }
+ }
+}
+
+/**
+ * Executes node prop updates for a vNode if the NODE_PROPS dirty bit is set. Processes all pending
+ * node prop updates that were stored via addPendingNodeProp.
+ *
+ * @param vNode - The vNode to execute node props for
+ * @param container - The container
+ * @returns Void
+ */
+export function executeNodeProps(vNode: VNode, container: Container, journal: VNodeJournal): void {
+ vNode.dirty &= ~ChoreBits.NODE_PROPS;
+ if (!(vNode.flags & VNodeFlags.Element)) {
+ return;
+ }
+
+ const allPropData = getNodePropData(vNode);
+ if (!allPropData || allPropData.size === 0) {
+ return;
+ }
+
+ const domVNode = vNode as ElementVNode;
+
+ // Process all pending node prop updates
+ for (const [property, nodeProp] of allPropData.entries()) {
+ let value: Signal | string = nodeProp.value;
+ if (isSignal(value)) {
+ // TODO: Handle async signals (promises) - need to track pending async prop data
+ value = value.value as any;
+ }
+
+ // Process synchronously (same logic as scheduler)
+ const serializedValue = serializeAttribute(property, value, nodeProp.scopedStyleIdPrefix);
+ const isConst = nodeProp.isConst;
+ setNodeProp(domVNode, journal, property, serializedValue, isConst);
+ }
+
+ // Clear pending prop data after processing
+ clearNodePropData(vNode);
+}
+
+/**
+ * Execute visible task cleanups and add promises to extraPromises.
+ *
+ * @param vNode - The vNode to cleanup
+ * @param container - The container
+ * @returns Void
+ */
+export function executeCleanup(vNode: VNode, container: Container): void {
+ vNode.dirty &= ~ChoreBits.CLEANUP;
+
+ // TODO add promises to extraPromises
+
+ const elementSeq = container.getHostProp(vNode, ELEMENT_SEQ);
+
+ if (!elementSeq || elementSeq.length === 0) {
+ // No tasks to execute, clear the bit
+ return;
+ }
+
+ for (const item of elementSeq) {
+ if (item instanceof Task) {
+ if (item.$flags$ & TaskFlags.NEEDS_CLEANUP) {
+ item.$flags$ &= ~TaskFlags.NEEDS_CLEANUP;
+ const task = item as Task;
+ cleanupDestroyable(task);
+ }
+ }
+ }
+}
+
+/**
+ * Executes compute/recompute chores for a vNode if the COMPUTE dirty bit is set. This handles
+ * signal recomputation and effect scheduling.
+ *
+ * @param vNode - The vNode to execute compute for
+ * @param container - The container
+ * @returns Promise if computation is async, void otherwise
+ */
+export function executeCompute(
+ vNode: VNode | ISsrNode,
+ container: Container
+): ValueOrPromise {
+ vNode.dirty &= ~ChoreBits.COMPUTE;
+ const target = container.getHostProp | null>(vNode, HOST_SIGNAL);
+ if (!target) {
+ return;
+ }
+ const effects = target.$effects$;
+
+ const ctx = newInvokeContext();
+ ctx.$container$ = container;
+ // needed for computed signals and throwing QRLs
+ return maybeThen(
+ retryOnPromise(() =>
+ invoke.call(target, ctx, (target as WrappedSignalImpl).$computeIfNeeded$)
+ ),
+ () => {
+ if ((target as WrappedSignalImpl).$flags$ & SignalFlags.RUN_EFFECTS) {
+ (target as WrappedSignalImpl).$flags$ &= ~SignalFlags.RUN_EFFECTS;
+ return scheduleEffects(container, target, effects);
+ }
+ }
+ );
+}
diff --git a/packages/qwik/src/core/shared/cursor/cursor-flush.ts b/packages/qwik/src/core/shared/cursor/cursor-flush.ts
new file mode 100644
index 00000000000..9ecca7dddb8
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/cursor-flush.ts
@@ -0,0 +1,131 @@
+import { type VNodeJournal } from '../../client/vnode-utils';
+import { runTask } from '../../use/use-task';
+import { QContainerValue, type Container } from '../types';
+import { dangerouslySetInnerHTML, QContainerAttr } from '../utils/markers';
+import { isPromise } from '../utils/promises';
+import { VNodeOperationType } from '../vnode/enums/vnode-operation-type.enum';
+import type { Cursor } from './cursor';
+import { getCursorData, type CursorData } from './cursor-props';
+
+/**
+ * Executes the flush phase for a cursor.
+ *
+ * @param cursor - The cursor to execute the flush phase for
+ * @param container - The container to execute the flush phase for
+ */
+export function executeFlushPhase(cursor: Cursor, container: Container): void {
+ const cursorData = getCursorData(cursor)!;
+ const journal = cursorData.journal;
+ if (journal && journal.length > 0) {
+ _flushJournal(journal);
+ cursorData.journal = null;
+ }
+ executeAfterFlush(container, cursorData);
+}
+
+export function _flushJournal(journal: VNodeJournal): void {
+ // console.log(vnode_journalToString(journal));
+ for (const operation of journal) {
+ switch (operation.operationType) {
+ case VNodeOperationType.InsertOrMove: {
+ const insertBefore = operation.beforeTarget;
+ const insertBeforeParent = operation.parent;
+ insertBeforeParent.insertBefore(operation.target, insertBefore);
+ break;
+ }
+ case VNodeOperationType.Delete: {
+ operation.target.remove();
+ break;
+ }
+ case VNodeOperationType.SetText: {
+ operation.target.nodeValue = operation.text;
+ break;
+ }
+ case VNodeOperationType.SetAttribute: {
+ const element = operation.target;
+ const attrName = operation.attrName;
+ const attrValue = operation.attrValue;
+ const shouldRemove = attrValue == null || attrValue === false;
+ if (isBooleanAttr(element, attrName)) {
+ (element as any)[attrName] = parseBoolean(attrValue);
+ } else if (attrName === dangerouslySetInnerHTML) {
+ (element as any).innerHTML = attrValue;
+ element.setAttribute(QContainerAttr, QContainerValue.HTML);
+ } else if (shouldRemove) {
+ element.removeAttribute(attrName);
+ } else if (attrName === 'value' && attrName in element) {
+ (element as any).value = attrValue;
+ } else {
+ element.setAttribute(attrName, attrValue as string);
+ }
+ break;
+ }
+ case VNodeOperationType.RemoveAllChildren: {
+ const removeParent = operation.target;
+ if (removeParent.replaceChildren) {
+ removeParent.replaceChildren();
+ } else {
+ // fallback if replaceChildren is not supported
+ removeParent.textContent = '';
+ }
+ break;
+ }
+ }
+ }
+}
+
+function executeAfterFlush(container: Container, cursorData: CursorData): void {
+ const visibleTasks = cursorData.afterFlushTasks;
+ if (!visibleTasks || visibleTasks.length === 0) {
+ cursorData.afterFlushTasks = null;
+ return;
+ }
+ let visibleTaskPromise: Promise | undefined;
+ for (const visibleTask of visibleTasks) {
+ const task = visibleTask;
+ const result = runTask(task, container, task.$el$);
+ if (isPromise(result)) {
+ visibleTaskPromise = visibleTaskPromise ? visibleTaskPromise.then(() => result) : result;
+ }
+ }
+ if (visibleTaskPromise) {
+ (cursorData.extraPromises ||= []).push(visibleTaskPromise);
+ }
+ cursorData.afterFlushTasks = null;
+}
+
+const isBooleanAttr = (element: Element, key: string): boolean => {
+ const isBoolean =
+ key == 'allowfullscreen' ||
+ key == 'async' ||
+ key == 'autofocus' ||
+ key == 'autoplay' ||
+ key == 'checked' ||
+ key == 'controls' ||
+ key == 'default' ||
+ key == 'defer' ||
+ key == 'disabled' ||
+ key == 'formnovalidate' ||
+ key == 'inert' ||
+ key == 'ismap' ||
+ key == 'itemscope' ||
+ key == 'loop' ||
+ key == 'multiple' ||
+ key == 'muted' ||
+ key == 'nomodule' ||
+ key == 'novalidate' ||
+ key == 'open' ||
+ key == 'playsinline' ||
+ key == 'readonly' ||
+ key == 'required' ||
+ key == 'reversed' ||
+ key == 'selected';
+ return isBoolean && key in element;
+};
+
+const parseBoolean = (value: string | boolean | null): boolean => {
+ if (value === 'false') {
+ return false;
+ }
+ return Boolean(value);
+};
diff --git a/packages/qwik/src/core/shared/cursor/cursor-props.ts b/packages/qwik/src/core/shared/cursor/cursor-props.ts
new file mode 100644
index 00000000000..4a6a1a6d3b3
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/cursor-props.ts
@@ -0,0 +1,104 @@
+import type { VNode } from '../vnode/vnode';
+import { isCursor } from './cursor';
+import { removeCursorFromQueue } from './cursor-queue';
+import type { Container } from '../types';
+import type { VNodeJournal } from '../../client/vnode-utils';
+import type { Task } from '../../use/use-task';
+
+/**
+ * Keys used to store cursor-related data in vNode props. These are internal properties that should
+ * not conflict with user props.
+ */
+const CURSOR_DATA_KEY = ':cursor';
+
+/** Key used to store pending node prop updates in vNode props. */
+export const NODE_PROPS_DATA_KEY = ':nodeProps';
+export const NODE_DIFF_DATA_KEY = ':nodeDiff';
+export const HOST_SIGNAL = ':signal';
+
+export interface CursorData {
+ afterFlushTasks: Task[] | null;
+ extraPromises: Promise[] | null;
+ journal: VNodeJournal | null;
+ container: Container;
+ position: VNode | null;
+ priority: number;
+ promise: Promise | null;
+ /** True when executing a render-blocking task (before promise is set) */
+ isBlocking: boolean;
+}
+
+/**
+ * Sets the cursor position in a cursor vNode.
+ *
+ * @param vNode - The cursor vNode
+ * @param position - The vNode position to set, or null for root
+ */
+export function setCursorPosition(
+ container: Container,
+ cursorData: CursorData,
+ position: VNode | null
+): void {
+ cursorData.position = position;
+ if (position && isCursor(position)) {
+ mergeCursors(container, cursorData, position);
+ }
+}
+
+function mergeCursors(container: Container, newCursorData: CursorData, oldCursor: VNode): void {
+ // delete from global cursors queue
+ removeCursorFromQueue(oldCursor, container);
+ const oldCursorData = getCursorData(oldCursor)!;
+ // merge after flush tasks
+ const oldAfterFlushTasks = oldCursorData.afterFlushTasks;
+ if (oldAfterFlushTasks && oldAfterFlushTasks.length > 0) {
+ const newAfterFlushTasks = newCursorData.afterFlushTasks;
+ if (newAfterFlushTasks) {
+ newAfterFlushTasks.push(...oldAfterFlushTasks);
+ } else {
+ newCursorData.afterFlushTasks = oldAfterFlushTasks;
+ }
+ }
+ // merge extra promises
+ const oldExtraPromises = oldCursorData.extraPromises;
+ if (oldExtraPromises && oldExtraPromises.length > 0) {
+ const newExtraPromises = newCursorData.extraPromises;
+ if (newExtraPromises) {
+ newExtraPromises.push(...oldExtraPromises);
+ } else {
+ newCursorData.extraPromises = oldExtraPromises;
+ }
+ }
+ // merge journal
+ const oldJournal = oldCursorData.journal;
+ if (oldJournal && oldJournal.length > 0) {
+ const newJournal = newCursorData.journal;
+ if (newJournal) {
+ newJournal.push(...oldJournal);
+ } else {
+ newCursorData.journal = oldJournal;
+ }
+ }
+}
+
+/**
+ * Gets the cursor data from a vNode.
+ *
+ * @param vNode - The vNode
+ * @returns The cursor data, or null if none or not a cursor
+ */
+export function getCursorData(vNode: VNode): CursorData | null {
+ const props = vNode.props;
+ return (props?.[CURSOR_DATA_KEY] as CursorData | null) ?? null;
+}
+
+/**
+ * Sets the cursor data on a vNode.
+ *
+ * @param vNode - The vNode
+ * @param cursorData - The cursor data to set, or null to clear
+ */
+export function setCursorData(vNode: VNode, cursorData: CursorData | null): void {
+ const props = (vNode.props ||= {});
+ props[CURSOR_DATA_KEY] = cursorData;
+}
diff --git a/packages/qwik/src/core/shared/cursor/cursor-queue.ts b/packages/qwik/src/core/shared/cursor/cursor-queue.ts
new file mode 100644
index 00000000000..0840e459d19
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/cursor-queue.ts
@@ -0,0 +1,96 @@
+/**
+ * @file Cursor queue management for cursor-based scheduling.
+ *
+ * Maintains a priority queue of cursors sorted by priority (lower = higher priority).
+ */
+
+import { VNodeFlags } from '../../client/types';
+import type { Container } from '../types';
+import type { Cursor } from './cursor';
+import { getCursorData } from './cursor-props';
+
+/** Global cursor queue array. Cursors are sorted by priority. */
+let globalCursorQueue: Cursor[] = [];
+
+/**
+ * Adds a cursor to the global queue. If the cursor already exists, it's removed and re-added to
+ * maintain correct priority order.
+ *
+ * @param cursor - The cursor to add
+ */
+export function addCursorToQueue(container: Container, cursor: Cursor): void {
+ const priority = getCursorData(cursor)!.priority;
+ let insertIndex = globalCursorQueue.length;
+
+ for (let i = 0; i < globalCursorQueue.length; i++) {
+ const existingPriority = getCursorData(globalCursorQueue[i])!.priority;
+ if (priority < existingPriority) {
+ insertIndex = i;
+ break;
+ }
+ }
+
+ globalCursorQueue.splice(insertIndex, 0, cursor);
+
+ container.$cursorCount$++;
+ container.$renderPromise$ ||= new Promise((r) => (container.$resolveRenderPromise$ = r));
+}
+
+/**
+ * Gets the highest priority cursor (lowest priority number) from the queue.
+ *
+ * @returns The highest priority cursor, or null if queue is empty
+ */
+export function getHighestPriorityCursor(): Cursor | null {
+ return globalCursorQueue.length > 0 ? globalCursorQueue[0] : null;
+}
+
+/**
+ * Removes a cursor from the global queue.
+ *
+ * @param cursor - The cursor to remove
+ */
+export function removeCursorFromQueue(
+ cursor: Cursor,
+ container: Container,
+ keepCursorFlag?: boolean
+): void {
+ container.$cursorCount$--;
+ if (!keepCursorFlag) {
+ cursor.flags &= ~VNodeFlags.Cursor;
+ }
+ const index = globalCursorQueue.indexOf(cursor);
+ if (index !== -1) {
+ // TODO: we can't use swap-and-remove algorithm because it will break the priority order
+ // // Move last element to the position of the element to remove, then pop
+ // const lastIndex = globalCursorQueue.length - 1;
+ // if (index !== lastIndex) {
+ // globalCursorQueue[index] = globalCursorQueue[lastIndex];
+ // }
+ // globalCursorQueue.pop();
+ globalCursorQueue.splice(index, 1);
+ }
+}
+
+/**
+ * Checks if the global cursor queue is empty.
+ *
+ * @returns True if the queue is empty
+ */
+export function isCursorQueueEmpty(): boolean {
+ return globalCursorQueue.length === 0;
+}
+
+/**
+ * Gets the number of cursors in the global queue.
+ *
+ * @returns The number of cursors
+ */
+export function getCursorQueueSize(): number {
+ return globalCursorQueue.length;
+}
+
+/** Clears all cursors from the global queue. */
+export function clearCursorQueue(): void {
+ globalCursorQueue = [];
+}
diff --git a/packages/qwik/src/core/shared/cursor/cursor-walker.ts b/packages/qwik/src/core/shared/cursor/cursor-walker.ts
new file mode 100644
index 00000000000..c6c217f54a7
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/cursor-walker.ts
@@ -0,0 +1,294 @@
+/**
+ * @file Cursor walker implementation for cursor-based scheduling.
+ *
+ * Implements depth-first traversal of the vDOM tree, processing dirty vNodes and their children.
+ * Handles promise blocking, time-slicing, and cursor position tracking.
+ */
+
+import { isServerPlatform } from '../platform/platform';
+import type { VNode } from '../vnode/vnode';
+import {
+ executeCleanup,
+ executeComponentChore,
+ executeCompute,
+ executeNodeDiff,
+ executeNodeProps,
+ executeTasks,
+} from './chore-execution';
+import { type Cursor } from './cursor';
+import { setCursorPosition, getCursorData, type CursorData } from './cursor-props';
+import { ChoreBits } from '../vnode/enums/chore-bits.enum';
+import { addCursorToQueue, getHighestPriorityCursor, removeCursorFromQueue } from './cursor-queue';
+import { executeFlushPhase } from './cursor-flush';
+import { createNextTick } from '../platform/next-tick';
+import { isPromise } from '../utils/promises';
+import type { ValueOrPromise } from '../utils/types';
+import { assertDefined, assertFalse } from '../error/assert';
+import type { Container } from '../types';
+import { VNodeFlags } from '../../client/types';
+
+const DEBUG = false;
+
+const nextTick = createNextTick(processCursorQueue);
+let isNextTickScheduled = false;
+
+export function triggerCursors(): void {
+ if (!isNextTickScheduled) {
+ isNextTickScheduled = true;
+ nextTick();
+ }
+}
+
+/** Options for walking a cursor. */
+export interface WalkOptions {
+ /** Time budget in milliseconds (for DOM time-slicing). If exceeded, walk pauses. */
+ timeBudget: number;
+}
+
+/**
+ * Processes the cursor queue, walking each cursor in turn.
+ *
+ * @param options - Walk options (time budget, etc.)
+ */
+export function processCursorQueue(
+ options: WalkOptions = {
+ timeBudget: 1000 / 60, // 60fps
+ }
+): void {
+ isNextTickScheduled = false;
+
+ let cursor: Cursor | null = null;
+ while ((cursor = getHighestPriorityCursor())) {
+ walkCursor(cursor, options);
+ }
+}
+
+/**
+ * Walks a cursor through the vDOM tree, processing dirty vNodes in depth-first order.
+ *
+ * The walker:
+ *
+ * 1. Starts from the cursor root (or resumes from cursor position)
+ * 2. Processes dirty vNodes using executeChoreSequence
+ * 3. If the vNode is not dirty, moves to the next vNode
+ * 4. If the vNode is dirty, executes the chores
+ * 5. If the chore is a promise, pauses the cursor and resumes in next tick
+ * 6. If the time budget is exceeded, pauses the cursor and resumes in next tick
+ * 7. Updates cursor position as it walks
+ *
+ * Note that there is only one walker for all containers in the app with the same Qwik version.
+ *
+ * @param cursor - The cursor to walk
+ * @param options - Walk options (time budget, etc.)
+ * @returns Walk result indicating completion status
+ */
+export function walkCursor(cursor: Cursor, options: WalkOptions): void {
+ const { timeBudget } = options;
+ const isServer = isServerPlatform();
+ const startTime = performance.now();
+
+ const cursorData = getCursorData(cursor)!;
+
+ // Check if cursor is blocked by a promise
+ const blockingPromise = cursorData.promise;
+ if (blockingPromise) {
+ return;
+ }
+
+ const container = cursorData.container;
+ assertDefined(container, 'Cursor container not found');
+
+ // Check if cursor is already complete
+ if (!cursor.dirty) {
+ finishWalk(container, cursor, cursorData, isServer);
+ return;
+ }
+
+ const journal = (cursorData.journal ||= []);
+
+ // Get starting position (resume from last position or start at root)
+ let currentVNode: VNode | null = null;
+
+ let count = 0;
+ while ((currentVNode = cursorData.position)) {
+ DEBUG && console.warn('walkCursor', currentVNode.toString());
+ if (DEBUG && count++ > 1000) {
+ throw new Error('Infinite loop detected in cursor walker');
+ }
+ // Check time budget (only for DOM, not SSR)
+ if (!isServer && !import.meta.env.TEST) {
+ const elapsed = performance.now() - startTime;
+ if (elapsed >= timeBudget) {
+ // Run in next tick
+ triggerCursors();
+ return;
+ }
+ }
+
+ if (cursorData.promise) {
+ return;
+ }
+
+ // Skip if the vNode is not dirty
+ if (!(currentVNode.dirty & ChoreBits.DIRTY_MASK)) {
+ // Move to next node
+ setCursorPosition(container, cursorData, getNextVNode(currentVNode));
+ continue;
+ }
+
+ // Skip if the vNode is deleted
+ if (currentVNode.flags & VNodeFlags.Deleted) {
+ // if deleted, run cleanup if needed
+ if (currentVNode.dirty & ChoreBits.CLEANUP) {
+ executeCleanup(currentVNode, container);
+ }
+ // Clear dirty bits and move to next node
+ currentVNode.dirty &= ~ChoreBits.DIRTY_MASK;
+ setCursorPosition(container, cursorData, getNextVNode(currentVNode));
+ continue;
+ }
+
+ let result: ValueOrPromise | undefined;
+ try {
+ // Execute chores in order
+ if (currentVNode.dirty & ChoreBits.TASKS) {
+ result = executeTasks(currentVNode, container, cursorData);
+ } else if (currentVNode.dirty & ChoreBits.NODE_DIFF) {
+ result = executeNodeDiff(currentVNode, container, journal);
+ } else if (currentVNode.dirty & ChoreBits.COMPONENT) {
+ result = executeComponentChore(currentVNode, container, journal);
+ } else if (currentVNode.dirty & ChoreBits.NODE_PROPS) {
+ executeNodeProps(currentVNode, container, journal);
+ } else if (currentVNode.dirty & ChoreBits.COMPUTE) {
+ result = executeCompute(currentVNode, container);
+ } else if (currentVNode.dirty & ChoreBits.CHILDREN) {
+ const dirtyChildren = currentVNode.dirtyChildren;
+ if (!dirtyChildren || dirtyChildren.length === 0) {
+ // No dirty children
+ currentVNode.dirty &= ~ChoreBits.CHILDREN;
+ } else {
+ partitionDirtyChildren(dirtyChildren, currentVNode);
+ currentVNode.nextDirtyChildIndex = 0;
+ // descend
+ currentVNode = getNextVNode(dirtyChildren[0])!;
+ setCursorPosition(container, cursorData, currentVNode);
+ continue;
+ }
+ }
+ } catch (error) {
+ container.handleError(error, currentVNode);
+ }
+
+ // Handle blocking promise
+ if (result && isPromise(result)) {
+ DEBUG && console.warn('walkCursor: blocking promise', currentVNode.toString());
+ // Store promise on cursor and pause
+ cursorData.promise = result;
+ removeCursorFromQueue(cursor, container, true);
+
+ const host = currentVNode;
+ result
+ .catch((error) => {
+ container.handleError(error, host);
+ })
+ .finally(() => {
+ cursorData.promise = null;
+ addCursorToQueue(container, cursor);
+ triggerCursors();
+ });
+ return;
+ }
+ }
+ assertFalse(
+ !!(cursor.dirty & ChoreBits.DIRTY_MASK && !cursorData.position),
+ 'Cursor is still dirty and position is not set after walking'
+ );
+ finishWalk(container, cursor, cursorData, isServer);
+}
+
+function finishWalk(
+ container: Container,
+ cursor: Cursor,
+ cursorData: CursorData,
+ isServer: boolean
+): void {
+ if (!(cursor.dirty & ChoreBits.DIRTY_MASK)) {
+ removeCursorFromQueue(cursor, container);
+ if (!isServer) {
+ executeFlushPhase(cursor, container);
+ }
+
+ if (cursorData.extraPromises) {
+ Promise.all(cursorData.extraPromises).then(() => {
+ resolveCursor(container);
+ });
+ return;
+ }
+
+ resolveCursor(container);
+ }
+}
+
+export function resolveCursor(container: Container): void {
+ // TODO streaming as a cursor? otherwise we need to wait separately for it
+ // or just ignore and resolve manually
+ if (container.$cursorCount$ === 0) {
+ container.$resolveRenderPromise$!();
+ container.$renderPromise$ = null;
+ }
+}
+
+/**
+ * Partitions dirtyChildren array so non-projections come first, projections last. Uses in-place
+ * swapping to avoid allocations.
+ */
+function partitionDirtyChildren(dirtyChildren: VNode[], parent: VNode): void {
+ let writeIndex = 0;
+ for (let readIndex = 0; readIndex < dirtyChildren.length; readIndex++) {
+ const child = dirtyChildren[readIndex];
+ if (child.parent === parent) {
+ // Non-projection, move to front
+ if (writeIndex !== readIndex) {
+ const temp = dirtyChildren[writeIndex];
+ dirtyChildren[writeIndex] = child;
+ dirtyChildren[readIndex] = temp;
+ }
+ writeIndex++;
+ }
+ }
+}
+
+/** @returns Next vNode to process, or null if traversal is complete */
+function getNextVNode(vNode: VNode): VNode | null {
+ // Prefer parent if it's dirty, otherwise try slotParent
+ let parent: VNode | null = null;
+ if (vNode.parent && vNode.parent.dirty & ChoreBits.CHILDREN) {
+ parent = vNode.parent;
+ } else if (vNode.slotParent && vNode.slotParent.dirty & ChoreBits.CHILDREN) {
+ parent = vNode.slotParent;
+ }
+
+ if (!parent) {
+ return null;
+ }
+ const dirtyChildren = parent.dirtyChildren!;
+ let index = parent.nextDirtyChildIndex;
+
+ const len = dirtyChildren!.length;
+ let count = len;
+ while (count-- > 0) {
+ const nextVNode = dirtyChildren[index];
+ if (nextVNode.dirty & ChoreBits.DIRTY_MASK) {
+ parent.nextDirtyChildIndex = (index + 1) % len;
+ return nextVNode;
+ }
+ index++;
+ if (index === len) {
+ index = 0;
+ }
+ }
+ // all array items checked, children are no longer dirty
+ parent!.dirty &= ~ChoreBits.CHILDREN;
+ parent!.dirtyChildren = null;
+ return getNextVNode(parent!);
+}
diff --git a/packages/qwik/src/core/shared/cursor/cursor.ts b/packages/qwik/src/core/shared/cursor/cursor.ts
new file mode 100644
index 00000000000..1c87053876f
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/cursor.ts
@@ -0,0 +1,83 @@
+import { VNodeFlags } from '../../client/types';
+import type { Container } from '../types';
+import type { VNode } from '../vnode/vnode';
+import { type CursorData, setCursorData } from './cursor-props';
+import { addCursorToQueue } from './cursor-queue';
+import { triggerCursors } from './cursor-walker';
+
+/**
+ * A cursor is a vNode that has the CURSOR flag set and priority stored in props.
+ *
+ * The cursor root is the vNode where the cursor was created (the dirty root). The cursor's current
+ * position is tracked in the vNode's props.
+ */
+export type Cursor = VNode;
+
+/**
+ * Adds a cursor to the given vNode (makes the vNode a cursor). Sets the cursor priority and
+ * position to the root vNode itself.
+ *
+ * @param root - The vNode that will become the cursor root (dirty root)
+ * @param priority - Priority level (lower = higher priority, 0 is default)
+ * @returns The vNode itself, now acting as a cursor
+ */
+export function addCursor(container: Container, root: VNode, priority: number): Cursor {
+ const cursorData: CursorData = {
+ afterFlushTasks: null,
+ extraPromises: null,
+ journal: null,
+ container: container,
+ position: root,
+ priority: priority,
+ promise: null,
+ isBlocking: false,
+ };
+
+ setCursorData(root, cursorData);
+
+ const cursor = root as Cursor;
+ cursor.flags |= VNodeFlags.Cursor;
+ // Add cursor to global queue
+ addCursorToQueue(container, cursor);
+
+ triggerCursors();
+
+ return cursor;
+}
+
+/**
+ * Checks if a vNode is a cursor (has CURSOR flag set).
+ *
+ * @param vNode - The vNode to check
+ * @returns True if the vNode has the CURSOR flag set
+ */
+export function isCursor(vNode: VNode): vNode is Cursor {
+ return (vNode.flags & VNodeFlags.Cursor) !== 0;
+}
+
+/**
+ * Checks if a cursor is complete (root vNode is clean). According to RFC section 3.2: "when a
+ * cursor finally marks its root vNode clean, that means the entire subtree is clean."
+ *
+ * @param cursor - The cursor to check
+ * @returns True if the cursor's root vNode has no dirty bits
+ */
+export function isCursorComplete(cursor: Cursor): boolean {
+ return cursor.dirty === 0;
+}
+
+/**
+ * Finds the root cursor for the given vNode.
+ *
+ * @param vNode - The vNode to find the cursor for
+ * @returns The cursor that contains the vNode, or null if no cursor is found
+ */
+export function findCursor(vNode: VNode): Cursor | null {
+ while (vNode) {
+ if (isCursor(vNode)) {
+ return vNode;
+ }
+ vNode = (vNode as VNode).parent || (vNode as VNode).slotParent!;
+ }
+ return null;
+}
diff --git a/packages/qwik/src/core/shared/cursor/ssr-chore-execution.ts b/packages/qwik/src/core/shared/cursor/ssr-chore-execution.ts
new file mode 100644
index 00000000000..34a5b148e74
--- /dev/null
+++ b/packages/qwik/src/core/shared/cursor/ssr-chore-execution.ts
@@ -0,0 +1,110 @@
+import type { ISsrNode, SSRContainer } from '../../ssr/ssr-types';
+import { runResource, type ResourceDescriptor } from '../../use/use-resource';
+import { runTask, Task, TaskFlags, type TaskFn } from '../../use/use-task';
+import { SsrNodeFlags, type Container } from '../types';
+import { ELEMENT_SEQ } from '../utils/markers';
+import type { ValueOrPromise } from '../utils/types';
+import { ChoreBits } from '../vnode/enums/chore-bits.enum';
+import { logWarn } from '../utils/log';
+import { serializeAttribute } from '../utils/styles';
+import { NODE_PROPS_DATA_KEY } from './cursor-props';
+import type { NodeProp } from '../../reactive-primitives/subscription-data';
+import { isSignal, type Signal } from '../../reactive-primitives/signal.public';
+import { executeCompute } from './chore-execution';
+import { isPromise } from '../utils/promises';
+
+/** @internal */
+export function _executeSsrChores(
+ container: SSRContainer,
+ ssrNode: ISsrNode
+): ValueOrPromise {
+ if (!(ssrNode.flags & SsrNodeFlags.Updatable)) {
+ if (ssrNode.dirty & ChoreBits.NODE_PROPS) {
+ executeNodePropChore(container, ssrNode);
+ }
+ if (ssrNode.dirty & ChoreBits.COMPUTE) {
+ executeCompute(ssrNode, container);
+ }
+ if (ssrNode.dirty & ChoreBits.DIRTY_MASK) {
+ // 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 and that is not supported because of streaming.
+ const warningMessage = `A chore was scheduled on a host element that has already been streamed to the client.
+ This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR).
+
+ Problematic chore:
+ - Host: ${ssrNode.toString()}
+ - Nearest element location: ${ssrNode.currentFile}
+
+ This is often caused by modifying a signal in an already rendered component during SSR.`;
+ logWarn(warningMessage);
+ }
+ ssrNode.dirty &= ~ChoreBits.DIRTY_MASK;
+ return;
+ }
+
+ let promise: ValueOrPromise | null = null;
+ if (ssrNode.dirty & ChoreBits.TASKS) {
+ const result = executeTasksChore(container, ssrNode);
+ if (isPromise(result)) {
+ promise = result;
+ }
+ }
+ if (ssrNode.dirty & ChoreBits.COMPONENT) {
+ // should not happen on SSR with non-streamed node
+ }
+ ssrNode.dirty &= ~ChoreBits.DIRTY_MASK;
+ if (promise) {
+ return promise;
+ }
+}
+
+function executeTasksChore(container: Container, ssrNode: ISsrNode): ValueOrPromise | null {
+ ssrNode.dirty &= ~ChoreBits.TASKS;
+ const elementSeq = ssrNode.getProp(ELEMENT_SEQ);
+ if (!elementSeq || elementSeq.length === 0) {
+ // No tasks to execute, clear the bit
+ return null;
+ }
+ let promise: ValueOrPromise | null = null;
+ for (const item of elementSeq) {
+ if (item instanceof Task) {
+ const task = item as Task;
+
+ // Skip if task is not dirty
+ if (!(task.$flags$ & TaskFlags.DIRTY)) {
+ continue;
+ }
+
+ let result: ValueOrPromise;
+ // Check if it's a resource
+ if (task.$flags$ & TaskFlags.RESOURCE) {
+ result = runResource(task as ResourceDescriptor, container, ssrNode);
+ } else {
+ result = runTask(task, container, ssrNode);
+ }
+ promise = promise ? promise.then(() => result as Promise) : (result as Promise);
+ }
+ }
+ return promise;
+}
+
+export function executeNodePropChore(container: SSRContainer, ssrNode: ISsrNode): void {
+ ssrNode.dirty &= ~ChoreBits.NODE_PROPS;
+
+ const allPropData = ssrNode.getProp(NODE_PROPS_DATA_KEY) as Map | null;
+ if (!allPropData || allPropData.size === 0) {
+ return;
+ }
+
+ for (const [property, nodeProp] of allPropData.entries()) {
+ let value: Signal | string = nodeProp.value;
+ if (isSignal(value)) {
+ // TODO: Handle async signals (promises) - need to track pending async prop data
+ value = value.value as any;
+ }
+ const serializedValue = serializeAttribute(property, value, nodeProp.scopedStyleIdPrefix);
+ container.addBackpatchEntry(ssrNode.id, property, serializedValue);
+ }
+}
diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts
index 9d0cf93c3c5..d087336849e 100644
--- a/packages/qwik/src/core/shared/error/error.ts
+++ b/packages/qwik/src/core/shared/error/error.ts
@@ -38,7 +38,7 @@ export const codeToText = (code: number, ...parts: any[]): string => {
'useComputed$ QRL {{0}} {{1}} cannot return a Promise', // 29
'ComputedSignal is read-only', // 30
'WrappedSignal is read-only', // 31
- 'Attribute value is unsafe for SSR', // 32
+ 'Attribute value is unsafe for SSR {{0}}', // 32
'SerializerSymbol function returned rejected promise', // 33
'Serialization Error: Cannot serialize function: {{0}}', // 34
];
diff --git a/packages/qwik/src/core/shared/jsx/jsx-node.ts b/packages/qwik/src/core/shared/jsx/jsx-node.ts
index 20652bf3fce..d96a5b506af 100644
--- a/packages/qwik/src/core/shared/jsx/jsx-node.ts
+++ b/packages/qwik/src/core/shared/jsx/jsx-node.ts
@@ -82,7 +82,6 @@ export class JSXNodeImpl implements JSXNodeInternal {
}
}
- // TODO let the optimizer do this instead
if ('className' in this.varProps) {
this.varProps.class = this.varProps.className;
this.varProps.className = undefined;
@@ -93,6 +92,7 @@ export class JSXNodeImpl implements JSXNodeInternal {
);
}
}
+ // TODO let the optimizer do this instead
if (this.constProps && 'className' in this.constProps) {
this.constProps.class = this.constProps.className;
this.constProps.className = undefined;
diff --git a/packages/qwik/src/core/shared/jsx/jsx-runtime.ts b/packages/qwik/src/core/shared/jsx/jsx-runtime.ts
index 15c9fa72674..c81c228119e 100644
--- a/packages/qwik/src/core/shared/jsx/jsx-runtime.ts
+++ b/packages/qwik/src/core/shared/jsx/jsx-runtime.ts
@@ -71,9 +71,6 @@ export function h, PROPS extends
}
}
- if (typeof type === 'string' && !key && 'dangerouslySetInnerHTML' in normalizedProps) {
- key = 'innerhtml';
- }
return _jsxSplit(type, props!, null, normalizedProps.children, 0, key);
}
diff --git a/packages/qwik/src/core/shared/jsx/props-proxy.ts b/packages/qwik/src/core/shared/jsx/props-proxy.ts
index 1b4e9f16948..fc7b4c09e40 100644
--- a/packages/qwik/src/core/shared/jsx/props-proxy.ts
+++ b/packages/qwik/src/core/shared/jsx/props-proxy.ts
@@ -10,7 +10,7 @@ import type { Props } from './jsx-runtime';
import type { JSXNodeInternal } from './types/jsx-node';
import type { Container } from '../types';
import { assertTrue } from '../error/assert';
-import { ChoreType } from '../util-chore-type';
+import { scheduleEffects } from '../../reactive-primitives/utils';
export function createPropsProxy(owner: JSXNodeImpl): Props {
// TODO don't make a proxy but populate getters? benchmark
@@ -174,12 +174,7 @@ const addPropsProxyEffect = (propsProxy: PropsProxyHandler, prop: string | symbo
export const triggerPropsProxyEffect = (propsProxy: PropsProxyHandler, prop: string | symbol) => {
const effects = getEffects(propsProxy.$effects$, prop);
if (effects) {
- propsProxy.$container$?.$scheduler$(
- ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
- undefined,
- propsProxy,
- effects
- );
+ scheduleEffects(propsProxy.$container$, propsProxy, effects);
}
};
diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx b/packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx
new file mode 100644
index 00000000000..aa8dab5fcdb
--- /dev/null
+++ b/packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx
@@ -0,0 +1,70 @@
+import type { EventHandler, FunctionComponent, PropsOf } from '@qwik.dev/core';
+import { component$ } from '@qwik.dev/core';
+import { describe, expectTypeOf, test } from 'vitest';
+
+// This is in a separate file because it makes TS very slow
+describe('polymorphism', () => {
+ test('polymorphic component', () => () => {
+ const Poly = component$(
+ ({
+ as,
+ ...props
+ }: { as?: C } & PropsOf) => {
+ const Cmp = as || 'div';
+ return hi;
+ }
+ );
+ expectTypeOf>[0]['popovertarget']>().toEqualTypeOf<
+ string | undefined
+ >();
+ expectTypeOf>[0]['href']>().toEqualTypeOf();
+ expectTypeOf>[0]>().not.toHaveProperty('href');
+ expectTypeOf>[0]>().not.toHaveProperty('popovertarget');
+ expectTypeOf<
+ Parameters[0]['onClick$'], EventHandler>>[1]
+ >().toEqualTypeOf();
+
+ const MyCmp = component$((p: { name: string }) => Hi {p.name});
+
+ return (
+ <>
+ {
+ expectTypeOf(ev).not.toBeAny();
+ expectTypeOf(ev).toEqualTypeOf();
+ expectTypeOf(el).toEqualTypeOf();
+ }}
+ // This should error
+ // popovertarget
+ >
+ Foo
+
+ {
+ expectTypeOf(ev).not.toBeAny();
+ expectTypeOf(ev).toEqualTypeOf();
+ expectTypeOf(el).toEqualTypeOf();
+ }}
+ href="hi"
+ // This should error
+ // popovertarget
+ >
+ Foo
+
+ {
+ expectTypeOf(ev).not.toBeAny();
+ expectTypeOf(ev).toEqualTypeOf();
+ expectTypeOf(el).toEqualTypeOf();
+ }}
+ popovertarget="foo"
+ >
+ Bar
+
+
+ >
+ );
+ });
+});
diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx b/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx
index 5cc888c20b4..386c7c16fbc 100644
--- a/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx
+++ b/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx
@@ -148,6 +148,18 @@ describe('types', () => {
type: 'button';
popovertarget?: string;
}>().toMatchTypeOf>();
+
+ $((_, element) => {
+ element.select();
+ expectTypeOf(element).toEqualTypeOf();
+ }) as QRLEventHandlerMulti;
+
+ const t = $>((_, element) => {
+ element.select();
+ expectTypeOf(element).toEqualTypeOf();
+ });
+ expectTypeOf(t).toExtend>();
+
<>
@@ -225,70 +237,6 @@ describe('types', () => {
>;
});
- test('polymorphic component', () => () => {
- const Poly = component$(
- ({
- as,
- ...props
- }: { as?: C } & PropsOf) => {
- const Cmp = as || 'div';
- return hi;
- }
- );
- expectTypeOf>[0]['popovertarget']>().toEqualTypeOf<
- string | undefined
- >();
- expectTypeOf>[0]['href']>().toEqualTypeOf();
- expectTypeOf>[0]>().not.toHaveProperty('href');
- expectTypeOf>[0]>().not.toHaveProperty('popovertarget');
- expectTypeOf<
- Parameters[0]['onClick$'], EventHandler>>[1]
- >().toEqualTypeOf();
-
- const MyCmp = component$((p: { name: string }) => Hi {p.name});
-
- return (
- <>
- {
- expectTypeOf(ev).not.toBeAny();
- expectTypeOf(ev).toEqualTypeOf();
- expectTypeOf(el).toEqualTypeOf();
- }}
- // This should error
- // popovertarget
- >
- Foo
-
- {
- expectTypeOf(ev).not.toBeAny();
- expectTypeOf(ev).toEqualTypeOf();
- expectTypeOf(el).toEqualTypeOf();
- }}
- href="hi"
- // This should error
- // popovertarget
- >
- Foo
-
- {
- expectTypeOf(ev).not.toBeAny();
- expectTypeOf(ev).toEqualTypeOf();
- expectTypeOf(el).toEqualTypeOf();
- }}
- popovertarget="foo"
- >
- Bar
-
-
- >
- );
- });
-
test('FunctionComponent', () => () => {
const Cmp = component$((props: { foo: string }) => null);
expectTypeOf(Cmp).toMatchTypeOf>();
diff --git a/packages/qwik/src/core/shared/qrl/qrl-class.ts b/packages/qwik/src/core/shared/qrl/qrl-class.ts
index 1e8a91ac3f6..3e90aa3c3e8 100644
--- a/packages/qwik/src/core/shared/qrl/qrl-class.ts
+++ b/packages/qwik/src/core/shared/qrl/qrl-class.ts
@@ -23,6 +23,7 @@ import { getSymbolHash, SYNC_QRL } from './qrl-utils';
import type { QRL, QrlArgs, QrlReturn } from './qrl.public';
// @ts-expect-error we don't have types for the preloader
import { p as preload } from '@qwik.dev/core/preloader';
+import { ElementVNode } from '../vnode/element-vnode';
interface SyncQRLSymbol {
$symbol$: typeof SYNC_QRL;
@@ -201,7 +202,12 @@ export const createQRL = (
}
if (isPromise(symbolRef)) {
symbolRef.then(
- () => emitUsedSymbol(symbol, ctx?.$element$, start),
+ () =>
+ emitUsedSymbol(
+ symbol,
+ ctx?.$hostElement$ instanceof ElementVNode ? ctx?.$hostElement$.node : undefined,
+ start
+ ),
(err) => {
console.error(`qrl ${symbol} failed to load`, err);
// We shouldn't cache rejections, we can try again later
diff --git a/packages/qwik/src/core/shared/scheduler-document-position.ts b/packages/qwik/src/core/shared/scheduler-document-position.ts
deleted file mode 100644
index cac04737d89..00000000000
--- a/packages/qwik/src/core/shared/scheduler-document-position.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import type { VNode } from '../client/vnode-impl';
-import type { ISsrNode } from '../ssr/ssr-types';
-
-/// These global variables are used to avoid creating new arrays for each call to `vnode_documentPosition`.
-const aVNodePath: VNode[] = [];
-const bVNodePath: VNode[] = [];
-/**
- * Compare two VNodes and determine their document position relative to each other.
- *
- * @param a VNode to compare
- * @param b VNode to compare
- * @returns -1 if `a` is before `b`, 0 if `a` is the same as `b`, 1 if `a` is after `b`.
- */
-export const vnode_documentPosition = (a: VNode, b: VNode): -1 | 0 | 1 => {
- if (a === b) {
- return 0;
- }
-
- let aDepth = -1;
- let bDepth = -1;
- while (a) {
- const vNode = (aVNodePath[++aDepth] = a);
- a = (vNode.parent || a.slotParent)!;
- }
- while (b) {
- const vNode = (bVNodePath[++bDepth] = b);
- b = (vNode.parent || b.slotParent)!;
- }
-
- while (aDepth >= 0 && bDepth >= 0) {
- a = aVNodePath[aDepth] as VNode;
- b = bVNodePath[bDepth] as VNode;
- if (a === b) {
- // if the nodes are the same, we need to check the next level.
- aDepth--;
- bDepth--;
- } else {
- // We found a difference so we need to scan nodes at this level.
- let cursor: VNode | null | undefined = b;
- do {
- cursor = cursor.nextSibling;
- if (cursor === a) {
- return 1;
- }
- } while (cursor);
- cursor = b;
- do {
- cursor = cursor.previousSibling;
- if (cursor === a) {
- return -1;
- }
- } while (cursor);
- if (b.slotParent) {
- // The "b" node is a projection, so we need to set it after "a" node,
- // because the "a" node could be a context provider.
- return -1;
- }
- // The node is not in the list of siblings, that means it must be disconnected.
- return 1;
- }
- }
- return aDepth < bDepth ? -1 : 1;
-};
-
-/// These global variables are used to avoid creating new arrays for each call to `ssrNodeDocumentPosition`.
-const aSsrNodePath: ISsrNode[] = [];
-const bSsrNodePath: ISsrNode[] = [];
-/**
- * Compare two SSR nodes and determine their document position relative to each other. Compares only
- * position between parent and child.
- *
- * @param a SSR node to compare
- * @param b SSR node to compare
- * @returns -1 if `a` is before `b`, 0 if `a` is the same as `b`, 1 if `a` is after `b`.
- */
-export const ssrNodeDocumentPosition = (a: ISsrNode, b: ISsrNode): -1 | 0 | 1 => {
- if (a === b) {
- return 0;
- }
-
- let aDepth = -1;
- let bDepth = -1;
- while (a) {
- const ssrNode = (aSsrNodePath[++aDepth] = a);
- a = ssrNode.parentComponent!;
- }
- while (b) {
- const ssrNode = (bSsrNodePath[++bDepth] = b);
- b = ssrNode.parentComponent!;
- }
-
- while (aDepth >= 0 && bDepth >= 0) {
- a = aSsrNodePath[aDepth] as ISsrNode;
- b = bSsrNodePath[bDepth] as ISsrNode;
- if (a === b) {
- // if the nodes are the same, we need to check the next level.
- aDepth--;
- bDepth--;
- } else {
- return 1;
- }
- }
- return aDepth < bDepth ? -1 : 1;
-};
diff --git a/packages/qwik/src/core/shared/scheduler-document-position.unit.ts b/packages/qwik/src/core/shared/scheduler-document-position.unit.ts
deleted file mode 100644
index 0a1db1ac5c6..00000000000
--- a/packages/qwik/src/core/shared/scheduler-document-position.unit.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it } from 'vitest';
-import { vnode_getFirstChild, vnode_newUnMaterializedElement } from '../client/vnode';
-import type { ContainerElement, QDocument } from '../client/types';
-import { vnode_documentPosition } from './scheduler-document-position';
-import { createDocument } from '@qwik.dev/dom';
-import type { ElementVNode } from '../client/vnode-impl';
-
-describe('vnode_documentPosition', () => {
- let parent: ContainerElement;
- let document: QDocument;
- let vParent: ElementVNode;
- beforeEach(() => {
- document = createDocument() as QDocument;
- document.qVNodeData = new WeakMap();
- parent = document.createElement('test') as ContainerElement;
- parent.qVNodeRefs = new Map();
- vParent = vnode_newUnMaterializedElement(parent);
- });
- afterEach(() => {
- parent = null!;
- document = null!;
- vParent = null!;
- });
-
- it('should compare two elements', () => {
- parent.innerHTML = '';
- const b = vnode_getFirstChild(vParent) as ElementVNode;
- const i = b.nextSibling as ElementVNode;
- expect(vnode_documentPosition(b, i)).toBe(-1);
- expect(vnode_documentPosition(i, b)).toBe(1);
- });
- it('should compare two virtual vNodes', () => {
- parent.innerHTML = 'AB';
- document.qVNodeData.set(parent, '{B}{B}');
- const a = vnode_getFirstChild(vParent) as ElementVNode;
- const b = a.nextSibling as ElementVNode;
- expect(vnode_documentPosition(a, b)).toBe(-1);
- expect(vnode_documentPosition(b, a)).toBe(1);
- });
- it('should compare two virtual vNodes', () => {
- parent.innerHTML = 'AB';
- document.qVNodeData.set(parent, '{{B}}{B}');
- const a = vnode_getFirstChild(vParent) as ElementVNode;
- const a2 = vnode_getFirstChild(a) as ElementVNode;
- const b = a.nextSibling as ElementVNode;
- expect(vnode_documentPosition(a2, b)).toBe(-1);
- expect(vnode_documentPosition(b, a2)).toBe(1);
- });
-});
diff --git a/packages/qwik/src/core/shared/scheduler-journal-block.unit.tsx b/packages/qwik/src/core/shared/scheduler-journal-block.unit.tsx
deleted file mode 100644
index 63e9e99c0e5..00000000000
--- a/packages/qwik/src/core/shared/scheduler-journal-block.unit.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { $ } from '@qwik.dev/core';
-import { delay } from './utils/promises';
-import { Chore, createScheduler } from './scheduler';
-import { createDocument } from '@qwik.dev/core/testing';
-import { QContainerAttr } from './utils/markers';
-import { getDomContainer } from '../client/dom-container';
-import { ChoreArray } from '../client/chore-array';
-import { ChoreType } from './util-chore-type';
-import type { HostElement } from './types';
-import { _jsxSorted } from '../internal';
-import type { ElementVNode, VirtualVNode } from '../client/vnode-impl';
-import {
- vnode_insertBefore,
- vnode_locate,
- vnode_newUnMaterializedElement,
- vnode_newVirtual,
-} from '../client/vnode';
-
-vi.mock('../client/vnode-diff', () => ({
- vnode_diff: vi.fn().mockImplementation(async () => {
- await delay(100);
- }),
-}));
-
-describe('should block journal flush during node-diff and component runs', () => {
- let scheduler: ReturnType = null!;
- let document: ReturnType = null!;
- let vBody: ElementVNode = null!;
- let vA: ElementVNode = null!;
- let vAHost: VirtualVNode = null!;
- let choreQueue: ChoreArray;
- let blockedChores: ChoreArray;
- let runningChores: Set;
-
- async function waitForDrain() {
- await scheduler(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
- }
-
- beforeEach(() => {
- (globalThis as any).testLog = [];
- document = createDocument();
- document.body.setAttribute(QContainerAttr, 'paused');
- const container = getDomContainer(document.body);
- container.handleError = vi.fn();
- choreQueue = new ChoreArray();
- blockedChores = new ChoreArray();
- runningChores = new Set();
- scheduler = createScheduler(
- container,
- () => testLog.push('journalFlush'),
- choreQueue,
- blockedChores,
- runningChores
- );
- document.body.innerHTML = '';
- vBody = vnode_newUnMaterializedElement(document.body);
- vA = vnode_locate(vBody, document.querySelector('a') as Element) as ElementVNode;
- vAHost = vnode_newVirtual();
- vAHost.setProp('q:id', 'A');
- vnode_insertBefore([], vA, vAHost, null);
- vi.useFakeTimers();
- });
-
- afterEach(async () => {
- vi.useRealTimers();
- await waitForDrain();
- });
-
- it('should block journal flush when NODE_DIFF is scheduled and executing', async () => {
- scheduler(
- ChoreType.NODE_DIFF,
- vAHost as HostElement,
- vAHost as HostElement,
- _jsxSorted('div', {}, null, null, 0, null)
- );
-
- expect(choreQueue.length).toBe(1);
- vi.advanceTimersToNextTimer();
- expect(runningChores.size).toBe(1);
- await vi.advanceTimersByTimeAsync(20);
- // no journal flush even if time elapsed, because NODE_DIFF is running
- expect(testLog).toEqual([]);
- // finish VNODE_DIFF
- await vi.advanceTimersByTimeAsync(80);
- // no running chores
- expect(runningChores.size).toBe(0);
- // journal flush should have happened
- expect(testLog).toEqual(['journalFlush']);
- });
-
- it('should block journal flush when NODE_DIFF and COMPONENT is scheduled and executing', async () => {
- scheduler(
- ChoreType.NODE_DIFF,
- vAHost as HostElement,
- vAHost as HostElement,
- _jsxSorted('div', {}, null, null, 0, null)
- );
-
- expect(choreQueue.length).toBe(1);
- vi.advanceTimersToNextTimer();
- expect(runningChores.size).toBe(1);
- await vi.advanceTimersByTimeAsync(80);
- // no journal flush even if time elapsed, because NODE_DIFF is running
- expect(testLog).toEqual([]);
- // schedule component chore while NODE_DIFF is running
- scheduler(
- ChoreType.COMPONENT,
- vAHost as HostElement,
- $(async () => {
- await delay(50);
- }) as any,
- null
- );
- // finish VNODE_DIFF
- await vi.advanceTimersByTimeAsync(20);
- // no running chores
- expect(runningChores.size).toBe(1);
- // still no journal flush because COMPONENT is running
- expect(testLog).toEqual([]);
- await vi.advanceTimersByTimeAsync(20);
- // still no journal flush because COMPONENT is running
- expect(testLog).toEqual([]);
- // finish COMPONENT + next VNODE_DIFF
- await vi.advanceTimersByTimeAsync(110);
- expect(runningChores.size).toBe(0);
- expect(testLog).toEqual(['journalFlush']);
- });
-});
diff --git a/packages/qwik/src/core/shared/scheduler-rules.ts b/packages/qwik/src/core/shared/scheduler-rules.ts
deleted file mode 100644
index 96605399e8a..00000000000
--- a/packages/qwik/src/core/shared/scheduler-rules.ts
+++ /dev/null
@@ -1,262 +0,0 @@
-import {
- vnode_getProjectionParentOrParent,
- vnode_isDescendantOf,
- vnode_isVNode,
-} from '../client/vnode';
-import { Task, TaskFlags } from '../use/use-task';
-import type { QRLInternal } from './qrl/qrl-class';
-import { ChoreState, type Chore } from './scheduler';
-import type { Container, HostElement } from './types';
-import { ChoreType } from './util-chore-type';
-import { ELEMENT_SEQ } from './utils/markers';
-import { isNumber } from './utils/types';
-import type { VNode } from '../client/vnode-impl';
-import type { ChoreArray } from '../client/chore-array';
-
-type BlockingRule = {
- blockedType: ChoreType;
- blockingType: ChoreType;
- match: (blocked: Chore, blocking: Chore, container: Container) => boolean;
-};
-
-const enum ChoreSetType {
- CHORES,
- BLOCKED_CHORES,
-}
-
-/**
- * Rules for determining if a chore is blocked by another chore. Some chores can block other chores.
- * They cannot run until the blocking chore has completed.
- *
- * The match function is used to determine if the blocked chore is blocked by the blocking chore.
- * The match function is called with the blocked chore, the blocking chore, and the container.
- */
-
-const VISIBLE_BLOCKING_RULES: BlockingRule[] = [
- // NODE_DIFF blocks VISIBLE on same host,
- // if the blocked chore is a child of the blocking chore
- // or the blocked chore is a sibling of the blocking chore
- {
- blockedType: ChoreType.VISIBLE,
- blockingType: ChoreType.NODE_DIFF,
- match: (blocked, blocking) =>
- isDescendant(blocked, blocking) || isDescendant(blocking, blocked),
- },
- // COMPONENT blocks VISIBLE on same host
- // if the blocked chore is a child of the blocking chore
- // or the blocked chore is a sibling of the blocking chore
- {
- blockedType: ChoreType.VISIBLE,
- blockingType: ChoreType.COMPONENT,
- match: (blocked, blocking) =>
- isDescendant(blocked, blocking) || isDescendant(blocking, blocked),
- },
-];
-
-const BLOCKING_RULES: BlockingRule[] = [
- // QRL_RESOLVE blocks RUN_QRL, TASK, VISIBLE on same host
- {
- blockedType: ChoreType.RUN_QRL,
- blockingType: ChoreType.QRL_RESOLVE,
- match: (blocked, blocking) => {
- const blockedQrl = blocked.$target$ as QRLInternal;
- const blockingQrl = blocking.$target$ as QRLInternal;
- return isSameHost(blocked, blocking) && isSameQrl(blockedQrl, blockingQrl);
- },
- },
- {
- blockedType: ChoreType.TASK,
- blockingType: ChoreType.QRL_RESOLVE,
- match: (blocked, blocking) => {
- const blockedTask = blocked.$payload$ as Task;
- const blockingQrl = blocking.$target$ as QRLInternal;
- return isSameHost(blocked, blocking) && isSameQrl(blockedTask.$qrl$, blockingQrl);
- },
- },
- {
- blockedType: ChoreType.VISIBLE,
- blockingType: ChoreType.QRL_RESOLVE,
- match: (blocked, blocking) => {
- const blockedTask = blocked.$payload$ as Task;
- const blockingQrl = blocking.$target$ as QRLInternal;
- return isSameHost(blocked, blocking) && isSameQrl(blockedTask.$qrl$, blockingQrl);
- },
- },
- // COMPONENT blocks NODE_DIFF, NODE_PROP on same host
- {
- blockedType: ChoreType.NODE_DIFF,
- blockingType: ChoreType.COMPONENT,
- match: (blocked, blocking) => blocked.$host$ === blocking.$host$,
- },
- {
- blockedType: ChoreType.NODE_PROP,
- blockingType: ChoreType.COMPONENT,
- match: (blocked, blocking) => blocked.$host$ === blocking.$host$,
- },
- ...VISIBLE_BLOCKING_RULES,
- // TASK blocks subsequent TASKs in the same component
- {
- blockedType: ChoreType.TASK,
- blockingType: ChoreType.TASK,
- match: (blocked, blocking, container) => {
- if (blocked.$host$ !== blocking.$host$) {
- return false;
- }
-
- const blockedIdx = blocked.$idx$ as number;
- if (!isNumber(blockedIdx) || blockedIdx <= 0) {
- return false;
- }
- const previousTask = findPreviousTaskInComponent(blocked.$host$, blockedIdx, container);
- return previousTask === blocking.$payload$;
- },
- },
-];
-
-function isDescendant(descendantChore: Chore, ancestorChore: Chore): boolean {
- const descendantHost = descendantChore.$host$;
- const ancestorHost = ancestorChore.$host$;
- if (!vnode_isVNode(descendantHost) || !vnode_isVNode(ancestorHost)) {
- return false;
- }
- return vnode_isDescendantOf(descendantHost, ancestorHost);
-}
-
-function isSameHost(a: Chore, b: Chore): boolean {
- return a.$host$ === b.$host$;
-}
-
-function isSameQrl(a: QRLInternal, b: QRLInternal): boolean {
- return a.$symbol$ === b.$symbol$;
-}
-
-function findAncestorBlockingChore(chore: Chore, type: ChoreSetType): Chore | null {
- const host = chore.$host$;
- if (!vnode_isVNode(host)) {
- return null;
- }
- const isNormalQueue = type === ChoreSetType.CHORES;
- // Walk up the ancestor tree and check the map
- let current: VNode | null = host;
- current = vnode_getProjectionParentOrParent(current);
- while (current) {
- const blockingChores = isNormalQueue ? current.chores : current.blockedChores;
- if (blockingChores) {
- for (const blockingChore of blockingChores) {
- if (
- blockingChore.$type$ < ChoreType.VISIBLE &&
- blockingChore.$type$ !== ChoreType.TASK &&
- blockingChore.$type$ !== ChoreType.QRL_RESOLVE &&
- blockingChore.$type$ !== ChoreType.RUN_QRL &&
- blockingChore.$state$ === ChoreState.NONE
- ) {
- return blockingChore;
- }
- }
- }
- current = vnode_getProjectionParentOrParent(current);
- }
- return null;
-}
-
-export function findBlockingChore(
- chore: Chore,
- choreQueue: ChoreArray,
- blockedChores: ChoreArray,
- runningChores: Set,
- container: Container
-): Chore | null {
- const blockingChoreInChoreQueue = findAncestorBlockingChore(chore, ChoreSetType.CHORES);
- if (blockingChoreInChoreQueue) {
- return blockingChoreInChoreQueue;
- }
- const blockingChoreInBlockedChores = findAncestorBlockingChore(
- chore,
- ChoreSetType.BLOCKED_CHORES
- );
- if (blockingChoreInBlockedChores) {
- return blockingChoreInBlockedChores;
- }
-
- for (const rule of BLOCKING_RULES) {
- if (chore.$type$ !== rule.blockedType) {
- continue;
- }
-
- // Check in choreQueue
- // TODO(perf): better to iterate in reverse order?
- for (const candidate of choreQueue) {
- if (
- candidate.$type$ === rule.blockingType &&
- rule.match(chore, candidate, container) &&
- candidate.$state$ === ChoreState.NONE
- ) {
- return candidate;
- }
- }
- // Check in blockedChores
- for (const candidate of blockedChores) {
- if (
- candidate.$type$ === rule.blockingType &&
- rule.match(chore, candidate, container) &&
- candidate.$state$ === ChoreState.NONE
- ) {
- return candidate;
- }
- }
-
- // Check in runningChores
- for (const candidate of runningChores) {
- if (
- candidate.$type$ === rule.blockingType &&
- rule.match(chore, candidate, container) &&
- candidate.$state$ !== ChoreState.FAILED
- ) {
- return candidate;
- }
- }
- }
- return null;
-}
-
-function findPreviousTaskInComponent(
- host: HostElement,
- currentTaskIdx: number,
- container: Container
-): Task | null {
- const elementSeq = container.getHostProp(host, ELEMENT_SEQ);
- if (!elementSeq || elementSeq.length <= currentTaskIdx) {
- return null;
- }
-
- for (let i = currentTaskIdx - 1; i >= 0; i--) {
- const candidate = elementSeq[i];
- if (candidate instanceof Task && candidate.$flags$ & TaskFlags.TASK) {
- return candidate;
- }
- }
- return null;
-}
-
-export function findBlockingChoreForVisible(
- chore: Chore,
- runningChores: Set,
- container: Container
-): Chore | null {
- for (const rule of VISIBLE_BLOCKING_RULES) {
- if (chore.$type$ !== rule.blockedType) {
- continue;
- }
-
- for (const candidate of runningChores) {
- if (
- candidate.$type$ === rule.blockingType &&
- rule.match(chore, candidate, container) &&
- candidate.$state$ !== ChoreState.FAILED
- ) {
- return candidate;
- }
- }
- }
- return null;
-}
diff --git a/packages/qwik/src/core/shared/scheduler-rules.unit.tsx b/packages/qwik/src/core/shared/scheduler-rules.unit.tsx
deleted file mode 100644
index 2daafedd0f8..00000000000
--- a/packages/qwik/src/core/shared/scheduler-rules.unit.tsx
+++ /dev/null
@@ -1,1218 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { findBlockingChore, findBlockingChoreForVisible } from './scheduler-rules';
-import { ChoreType } from './util-chore-type';
-import { addBlockedChore, type Chore } from './scheduler';
-import { Task, TaskFlags } from '../use/use-task';
-import { ELEMENT_SEQ } from './utils/markers';
-import type { Container } from './types';
-import { vnode_newVirtual } from '../client/vnode';
-import { $, type QRL } from './qrl/qrl.public';
-import { createQRL } from './qrl/qrl-class';
-import { ChoreArray } from '../client/chore-array';
-import type { VNode } from '../client/vnode-impl';
-
-const createMockChore = (
- type: ChoreType,
- host: object,
- idx: number | string = 0,
- payload: any = null,
- target: any = null
-): Chore => ({
- $type$: type,
- $host$: host as any,
- $idx$: idx,
- $payload$: payload,
- $target$: target,
- $state$: 0,
- $blockedChores$: null,
- $returnValue$: null,
- $startTime$: undefined,
- $endTime$: undefined,
- $resolve$: undefined,
- $reject$: undefined,
-});
-
-let rootVNode = vnode_newVirtual();
-
-const createMockContainer = (elementSeqMap: Map |
|---|