From 0d70d3d0490a751a439eb2d4b12de6ed6a2f382b Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 15:47:11 +0100 Subject: [PATCH 01/17] chore(core): _getQContainerElement only on element Co-authored-by: Varixo --- packages/qwik/src/core/client/dom-container.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 1f2d6624542..a006b63a7e8 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -81,11 +81,8 @@ export function getDomContainerFromQContainerElement(qContainerElement: Element) } /** @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 => { From 1ee27e52ea114b42bd4e2808811f51d37493ad6b Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 15:51:27 +0100 Subject: [PATCH 02/17] chore(core): _run should not wait Co-authored-by: Varixo --- packages/qwik/src/core/client/run-qrl.ts | 27 +++++++----------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/qwik/src/core/client/run-qrl.ts b/packages/qwik/src/core/client/run-qrl.ts index e0c717edcd2..90d370f8a3a 100644 --- a/packages/qwik/src/core/client/run-qrl.ts +++ b/packages/qwik/src/core/client/run-qrl.ts @@ -1,11 +1,9 @@ -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 { retryOnPromise } from '../shared/utils/promises'; import type { ValueOrPromise } from '../shared/utils/types'; 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 +15,11 @@ 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 runQrl(...args); + } + }); } - - 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); }; From 04746ad0904392826a3096ff9ac2eb4783623fff Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 16:04:42 +0100 Subject: [PATCH 03/17] chore(serdes): name SerializationBackRef Co-authored-by: Varixo --- .../src/core/shared/serdes/serialization-context.ts | 4 ++-- packages/qwik/src/core/shared/serdes/serialize.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/qwik/src/core/shared/serdes/serialization-context.ts b/packages/qwik/src/core/shared/serdes/serialization-context.ts index 89a88764d60..6ca72a4dd9b 100644 --- a/packages/qwik/src/core/shared/serdes/serialization-context.ts +++ b/packages/qwik/src/core/shared/serdes/serialization-context.ts @@ -30,7 +30,7 @@ export let isDomRef = (obj: unknown): obj is DomRef => false; * A back reference to a previously serialized object. Before deserialization, all backrefs are * swapped with their original locations. */ -export class BackRef { +export class SerializationBackRef { constructor( /** The path from root to the original object */ public $path$: string @@ -156,7 +156,7 @@ export const createSerializationContext = ( if (index === undefined) { index = roots.length; } - roots[index] = new BackRef(path); + roots[index] = new SerializationBackRef(path); ref.$parent$ = null; ref.$index$ = index; }; diff --git a/packages/qwik/src/core/shared/serdes/serialize.ts b/packages/qwik/src/core/shared/serdes/serialize.ts index c72af3464ff..1236f99da33 100644 --- a/packages/qwik/src/core/shared/serdes/serialize.ts +++ b/packages/qwik/src/core/shared/serdes/serialize.ts @@ -38,7 +38,11 @@ import { isPromise, maybeThen } from '../utils/promises'; import { fastSkipSerialize, SerializerSymbol } from './verify'; import { Constants, TypeIds } from './constants'; import { qrlToString } from './qrl-to-string'; -import { BackRef, type SeenRef, type SerializationContext } from './serialization-context'; +import { + SerializationBackRef, + type SeenRef, + type SerializationContext, +} from './serialization-context'; /** * Format: @@ -169,7 +173,7 @@ export async function serialize(serializationContext: SerializationContext): Pro } // Now we know it's a root and we should output a RootRef - const rootIdx = value instanceof BackRef ? value.$path$ : seen.$index$; + const rootIdx = value instanceof SerializationBackRef ? value.$path$ : seen.$index$; // But make sure we do output ourselves if (!parent && rootIdx === index) { @@ -280,7 +284,7 @@ export async function serialize(serializationContext: SerializationContext): Pro output(TypeIds.Constant, Constants.EMPTY_OBJ); } else if (value === null) { output(TypeIds.Constant, Constants.Null); - } else if (value instanceof BackRef) { + } else if (value instanceof SerializationBackRef) { output(TypeIds.RootRef, value.$path$); } else { const newSeenRef = getSeenRefOrOutput(value, index); From 9f7ca253a97418c4112a44493e3173fe62190b44 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 16:05:56 +0100 Subject: [PATCH 04/17] refactor(core): ignore className Co-authored-by: Varixo --- packages/qwik/src/core/shared/jsx/jsx-node.ts | 2 +- packages/qwik/src/core/shared/utils/scoped-styles.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/utils/scoped-styles.ts b/packages/qwik/src/core/shared/utils/scoped-styles.ts index 5c6ec6625d5..23b691ad283 100644 --- a/packages/qwik/src/core/shared/utils/scoped-styles.ts +++ b/packages/qwik/src/core/shared/utils/scoped-styles.ts @@ -6,11 +6,11 @@ export const styleContent = (styleId: string): string => { }; export function hasClassAttr(props: Props): boolean { - return 'class' in props || 'className' in props; + return 'class' in props; } export function isClassAttr(key: string): boolean { - return key === 'class' || key === 'className'; + return key === 'class'; } export function getScopedStyleIdsAsPrefix(scopedStyleIds: Set): string { From 78e966cd158706fa8f8aa30031af01374350ad38 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 16:14:34 +0100 Subject: [PATCH 05/17] wip cursors scheduling - add cursor management - remove chore based scheduler - refactor VNode - remove journal Co-authored-by: Varixo --- .../qwik/src/core/client/dom-container.ts | 86 ++-- packages/qwik/src/core/client/dom-render.ts | 13 +- packages/qwik/src/core/client/types.ts | 42 +- packages/qwik/src/core/client/vnode-diff.ts | 427 +++++++----------- packages/qwik/src/core/client/vnode-impl.ts | 3 +- .../qwik/src/core/client/vnode-namespace.ts | 25 +- packages/qwik/src/core/client/vnode.ts | 349 +++++++------- packages/qwik/src/core/debug.ts | 91 ++-- packages/qwik/src/core/internal.ts | 11 +- .../src/core/reactive-primitives/backref.ts | 7 + .../src/core/reactive-primitives/cleanup.ts | 16 +- .../impl/async-computed-signal-impl.ts | 18 +- .../impl/computed-signal-impl.ts | 14 +- .../reactive-primitives/impl/signal-impl.ts | 16 +- .../core/reactive-primitives/impl/store.ts | 23 +- .../impl/wrapped-signal-impl.ts | 35 +- .../core/reactive-primitives/subscriber.ts | 4 +- .../reactive-primitives/subscription-data.ts | 6 + .../src/core/reactive-primitives/types.ts | 8 +- .../src/core/reactive-primitives/utils.ts | 58 +-- .../src/core/shared/component-execution.ts | 7 +- .../src/core/shared/cursor/chore-execution.ts | 338 ++++++++++++++ .../src/core/shared/cursor/cursor-flush.ts | 151 +++++++ .../src/core/shared/cursor/cursor-props.ts | 136 ++++++ .../src/core/shared/cursor/cursor-queue.ts | 87 ++++ .../src/core/shared/cursor/cursor-walker.ts | 247 ++++++++++ .../qwik/src/core/shared/cursor/cursor.ts | 84 ++++ .../qwik/src/core/shared/jsx/props-proxy.ts | 9 +- .../qwik/src/core/shared/serdes/inflate.ts | 8 +- .../src/core/shared/serdes/serdes.public.ts | 1 - .../qwik/src/core/shared/shared-container.ts | 20 +- packages/qwik/src/core/shared/types.ts | 12 +- .../qwik/src/core/shared/utils/markers.ts | 4 + .../src/core/shared/vnode/element-vnode.ts | 23 + .../shared/vnode/enums/chore-bits.enum.ts | 14 + .../vnode/enums/vnode-operation-type.enum.ts | 10 + .../qwik/src/core/shared/vnode/ssr-vnode.ts | 21 + .../qwik/src/core/shared/vnode/text-vnode.ts | 21 + .../shared/vnode/types/dom-vnode-operation.ts | 23 + .../src/core/shared/vnode/virtual-vnode.ts | 22 + .../qwik/src/core/shared/vnode/vnode-dirty.ts | 69 +++ packages/qwik/src/core/shared/vnode/vnode.ts | 30 ++ packages/qwik/src/core/use/use-core.ts | 33 +- packages/qwik/src/core/use/use-resource.ts | 17 +- packages/qwik/src/core/use/use-task.ts | 25 +- .../qwik/src/core/use/use-visible-task.ts | 9 +- packages/qwik/src/server/ssr-container.ts | 2 +- packages/qwik/src/testing/element-fixture.ts | 13 +- .../qwik/src/testing/rendering.unit-util.tsx | 51 +-- packages/qwik/src/testing/util.ts | 3 +- .../qwik/src/testing/vdom-diff.unit-util.ts | 30 +- 51 files changed, 1930 insertions(+), 842 deletions(-) create mode 100644 packages/qwik/src/core/reactive-primitives/backref.ts create mode 100644 packages/qwik/src/core/shared/cursor/chore-execution.ts create mode 100644 packages/qwik/src/core/shared/cursor/cursor-flush.ts create mode 100644 packages/qwik/src/core/shared/cursor/cursor-props.ts create mode 100644 packages/qwik/src/core/shared/cursor/cursor-queue.ts create mode 100644 packages/qwik/src/core/shared/cursor/cursor-walker.ts create mode 100644 packages/qwik/src/core/shared/cursor/cursor.ts create mode 100644 packages/qwik/src/core/shared/vnode/element-vnode.ts create mode 100644 packages/qwik/src/core/shared/vnode/enums/chore-bits.enum.ts create mode 100644 packages/qwik/src/core/shared/vnode/enums/vnode-operation-type.enum.ts create mode 100644 packages/qwik/src/core/shared/vnode/ssr-vnode.ts create mode 100644 packages/qwik/src/core/shared/vnode/text-vnode.ts create mode 100644 packages/qwik/src/core/shared/vnode/types/dom-vnode-operation.ts create mode 100644 packages/qwik/src/core/shared/vnode/virtual-vnode.ts create mode 100644 packages/qwik/src/core/shared/vnode/vnode-dirty.ts create mode 100644 packages/qwik/src/core/shared/vnode/vnode.ts diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index a006b63a7e8..9483e3f9430 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, @@ -48,22 +49,21 @@ import { 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, - type VNodeJournal, + vnode_setProp, } from './vnode'; -import type { ElementVNode, VNode, VirtualVNode } from './vnode-impl'; +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); @@ -96,7 +96,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>; @@ -108,29 +107,14 @@ 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.$hoistStyles$(); this.$buildBase$ = element.getAttribute(QBaseAttr)!; this.$instanceHash$ = element.getAttribute(QInstanceAttr)!; this.qManifestHash = element.getAttribute(QManifestHashAttr)!; @@ -157,7 +141,24 @@ export class DomContainer extends _SharedContainer implements IClientContainer { } } - $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; } @@ -168,17 +169,15 @@ 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 vErrorDiv = vnode_createErrorDiv(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(insertHost as ElementVNode | VirtualVNode, vErrorDiv, insertBefore); } if (err && err instanceof Error) { @@ -220,7 +219,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 = @@ -236,7 +235,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 { @@ -255,20 +254,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 = prop; + if (typeof value == 'string') { + const projection = this.vNodeLocate(value); + props[prop] = projection; + } } } } @@ -304,7 +304,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..1aabca8aad9 100644 --- a/packages/qwik/src/core/client/dom-render.ts +++ b/packages/qwik/src/core/client/dom-render.ts @@ -1,13 +1,15 @@ -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'; -import { QContainerAttr } from '../shared/utils/markers'; +import { NODE_DIFF_DATA_KEY, 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'; +import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; +import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; /** * Render JSX. @@ -42,8 +44,9 @@ 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); 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..9a3a6f492e8 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, @@ -61,17 +57,15 @@ 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,17 +79,24 @@ 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'; 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, @@ -103,8 +104,6 @@ export const vnode_diff = ( vStartNode: VNode, scopedStyleIdPrefix: string | null ) => { - let journal = (container as DomContainer).$journal$; - /** * Stack is used to keep track of the state of the traversal. * @@ -247,7 +246,6 @@ export const vnode_diff = ( } } else if (jsxValue === (SkipRender as JSXChildren)) { // do nothing, we are skipping this node - journal = []; } else { expectText(''); } @@ -415,14 +413,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 +461,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 +472,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,45 +484,42 @@ 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( - journal, vParent as ElementVNode | VirtualVNode, (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( - journal, vParent as ElementVNode | VirtualVNode, (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; } @@ -548,7 +544,7 @@ export const vnode_diff = ( continue; } cleanup(container, vNode); - vnode_remove(journal, vParent, vNode, true); + vnode_remove(vParent, vNode, true); } vSideBuffer.clear(); vSideBuffer = null; @@ -593,7 +589,7 @@ export const vnode_diff = ( cleanup(container, vChild); vChild = vChild.nextSibling as VNode | null; } - vnode_truncate(journal, vCurrent as ElementVNode | VirtualVNode, vFirstChild); + vnode_truncate(vCurrent as ElementVNode | VirtualVNode, vFirstChild); } } @@ -608,7 +604,7 @@ export const vnode_diff = ( cleanup(container, toRemove); // 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); + vnode_remove(vParent, toRemove, true); } } } @@ -619,7 +615,7 @@ export const vnode_diff = ( cleanup(container, vCurrent); const toRemove = vCurrent; advanceToNextSibling(); - vnode_remove(journal, vParent, toRemove, true); + vnode_remove(vParent, toRemove, true); } } @@ -667,10 +663,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(vNewNode!, key, ''); } // register an event for qwik loader (window/document prefixed with '-') registerQwikLoaderEvent(loaderScopedEvent); @@ -736,7 +732,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 @@ -748,7 +744,7 @@ export const vnode_diff = ( } } - vnode_insertBefore(journal, vParent as ElementVNode, vNewNode as ElementVNode, vCurrent); + vnode_insertBefore(vParent as ElementVNode, vNewNode as ElementVNode, vCurrent); return needsQDispatchEventPatch; } @@ -771,7 +767,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,37 +779,23 @@ 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) { @@ -821,97 +803,50 @@ export const vnode_diff = ( 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; - } + qrl(event, element); } - }); - 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(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 +860,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 +875,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 +897,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; - - // Skip special keys in destination HANDLER_PREFIX - if (dstKey?.startsWith(HANDLER_PREFIX)) { - dstIdx += 2; // skip key and value - continue; - } + // Actual diffing logic + // Apply all new attributes + for (const key in newAttrs) { + const newValue = newAttrs[key]; + const isEvent = isHtmlAttributeAnEventName(key); - 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); + if (key in oldAttrs) { + if (newValue !== oldAttrs[key]) { + isEvent ? recordJsxEvent(key, newValue) : record(key, newValue); } - 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 - } + isEvent ? recordJsxEvent(key, newValue) : record(key, newValue); + } + } + + // 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 +951,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 +993,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 +1011,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 +1079,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 +1092,7 @@ export const vnode_diff = ( } } } - vnode_insertBefore(journal, parentForInsert as any, buffered, vCurrent); + vnode_insertBefore(parentForInsert as ElementVNode | VirtualVNode, buffered, vCurrent); vCurrent = buffered; vNewNode = null; return; @@ -1222,7 +1105,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); @@ -1234,13 +1117,12 @@ export const vnode_diff = ( const createNew = () => { vnode_insertBefore( - journal, vParent as VirtualVNode, (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 +1175,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 +1187,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 +1195,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); } } descendContentToProject(jsxNode.children, host); @@ -1344,7 +1227,8 @@ export const vnode_diff = ( while ( componentHost && (vnode_isVirtualVNode(componentHost) - ? (componentHost as VirtualVNode).getProp | null>( + ? vnode_getProp | null>( + componentHost as VirtualVNode, OnRenderProp, null ) === null @@ -1375,30 +1259,28 @@ export const vnode_diff = ( clearAllEffects(container, host); } vnode_insertBefore( - journal, vParent as VirtualVNode, (vNewNode = vnode_newVirtual()), 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() { vnode_insertBefore( - journal, vParent as VirtualVNode, (vNewNode = vnode_newVirtual()), 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; } } @@ -1407,14 +1289,13 @@ export const vnode_diff = ( const type = vnode_getType(vCurrent); if (type === 3 /* Text */) { if (text !== vnode_getText(vCurrent as TextVNode)) { - vnode_setText(journal, vCurrent as TextVNode, text); + vnode_setText(vCurrent as TextVNode, text); return; } return; } } vnode_insertBefore( - journal, vParent as VirtualVNode, (vNewNode = vnode_newText(container.document.createTextNode(text), text)), vCurrent @@ -1428,11 +1309,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 +1324,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; } @@ -1518,7 +1399,7 @@ function handleProps( } 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); + vnode_setProp(host as VirtualVNode, ELEMENT_PROPS, jsxProps); vNodeProps = jsxProps; } } @@ -1618,7 +1499,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 +1509,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.DIRTY; + markVNodeDirty(container, vCursor, ChoreBits.CLEANUP); + // don't call cleanupDestroyable yet, do it by the scheduler continue; } else if (obj instanceof SignalImpl || isStore(obj)) { @@ -1643,24 +1526,24 @@ 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, value] of Object.entries(attrs)) { + if (isSlotProp(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, projectionChild); + projectionChild = projectionChild.nextSibling as VNode | null; + } - cleanupStaleUnclaimedProjection(container.$journal$, projection); + cleanupStaleUnclaimedProjection(projection); + } } } } @@ -1732,7 +1615,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) { } while (true as boolean); } -function cleanupStaleUnclaimedProjection(journal: VNodeJournal, projection: VNode) { +function cleanupStaleUnclaimedProjection(projection: VNode) { // we are removing a node where the projection would go after slot render. // This is not needed, so we need to cleanup still unclaimed projection const projectionParent = projection.parent; @@ -1743,7 +1626,7 @@ function cleanupStaleUnclaimedProjection(journal: VNodeJournal, projection: VNod vnode_getElementName(projectionParent as ElementVNode) === QTemplate ) { // if parent is the q:template element then projection is still unclaimed - remove it - vnode_remove(journal, projectionParent as ElementVNode | VirtualVNode, projection, true); + vnode_remove(projectionParent as ElementVNode | VirtualVNode, projection, true); } } } diff --git a/packages/qwik/src/core/client/vnode-impl.ts b/packages/qwik/src/core/client/vnode-impl.ts index 9d948a39820..79b0e20f28b 100644 --- a/packages/qwik/src/core/client/vnode-impl.ts +++ b/packages/qwik/src/core/client/vnode-impl.ts @@ -7,10 +7,9 @@ import { 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'; +import { BackRef } from '../reactive-primitives/backref'; /** @internal */ export abstract class VNode extends BackRef { diff --git a/packages/qwik/src/core/client/vnode-namespace.ts b/packages/qwik/src/core/client/vnode-namespace.ts index 8f7970a789a..027ca217be9 100644 --- a/packages/qwik/src/core/client/vnode-namespace.ts +++ b/packages/qwik/src/core/client/vnode-namespace.ts @@ -19,9 +19,10 @@ import { vnode_getFirstChild, vnode_isElementVNode, vnode_isTextVNode, - type VNodeJournal, } from './vnode'; -import type { ElementVNode, VNode } from './vnode-impl'; +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'; @@ -50,29 +51,28 @@ export const vnode_getElementNamespaceFlags = (element: Element) => { }; 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(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 - const children = vnode_getDOMChildNodes(journal, newChild, true); + const children = vnode_getDOMChildNodes(newChild, true); for (let i = 0; i < children.length; i++) { 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 +80,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 +93,8 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert( ); if (newChildElement) { - domChildren.push(newChildElement); + childVNode.node = newChildElement; + domChildren.push(childVNode); } } } @@ -154,7 +155,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 +198,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.ts index a1a574fb3a9..076bfd119f7 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -153,7 +153,6 @@ import { 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, @@ -167,8 +166,13 @@ import { } 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'; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -195,21 +199,24 @@ 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 +224,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; }; @@ -245,12 +251,10 @@ export const vnode_newSharedText = ( 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,6 +264,7 @@ 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 ); @@ -272,11 +277,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 +299,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 +329,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 +381,51 @@ export const vnode_getNodeTypeName = (vNode: VNode): string => { return ''; }; +export const vnode_getProp = ( + vNode: VNode, + key: string, + getObject: ((id: string) => T) | null +): T | null => { + if (vnode_isElementVNode(vNode) || vnode_isVirtualVNode(vNode)) { + const value = vNode.props?.[key] ?? null; + if (typeof value === 'string' && getObject) { + return getObject(value); + } + 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 && vNode.props) { + delete vNode.props[key]; + } else { + vNode.props ||= {}; + vNode.props[key] = value; + } + } +}; + +export const vnode_setAttr = (vNode: VNode, key: string, value: string | null | boolean) => { + if (vnode_isElementVNode(vNode)) { + vnode_setProp(vNode, key, value); + addVNodeOperation(vNode, { + operationType: VNodeOperationType.None, + attrs: { + [key]: value, + }, + }); + } +}; + /** @internal */ export const vnode_ensureElementInflated = (vnode: VNode) => { const flags = vnode.flags; if ((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]; @@ -394,16 +435,14 @@ export const vnode_ensureElementInflated = (vnode: VNode) => { // all attributes after the ':' are considered immutable, and so we ignore them. break; } else if (key.startsWith(QContainerAttr)) { - const props = vnode_getProps(elementVNode); if (attr.value === QContainerValue.HTML) { - mapArray_set(props, dangerouslySetInnerHTML, element.innerHTML, 0); + vnode_setProp(elementVNode, 'dangerouslySetInnerHTML', element.innerHTML); } else if (attr.value === QContainerValue.TEXT && 'value' in element) { - mapArray_set(props, 'value', element.value, 0); + 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); } } } @@ -463,19 +502,16 @@ export function vnode_walkVNode( } export function vnode_getDOMChildNodes( - journal: VNodeJournal, root: VNode, isVNode: true, childNodes?: (ElementVNode | TextVNode)[] ): (ElementVNode | TextVNode)[]; export function vnode_getDOMChildNodes( - journal: VNodeJournal, root: VNode, isVNode?: false, childNodes?: (Element | Text)[] ): (Element | Text)[]; export function vnode_getDOMChildNodes( - journal: VNodeJournal, root: VNode, isVNode: boolean = false, childNodes: (ElementVNode | TextVNode | Element | Text)[] = [] @@ -487,7 +523,7 @@ export function vnode_getDOMChildNodes( * we would return a single text node which represents many actual text nodes, or removing a * single text node would remove many text nodes. */ - vnode_ensureTextInflated(journal, root); + vnode_ensureTextInflated(root); } childNodes.push(isVNode ? root : vnode_getNode(root)!); return childNodes; @@ -502,12 +538,12 @@ export function vnode_getDOMChildNodes( * we would return a single text node which represents many actual text nodes, or removing a * single text node would remove many text nodes. */ - vnode_ensureTextInflated(journal, vNode); + vnode_ensureTextInflated(vNode); childNodes.push(isVNode ? vNode : vnode_getNode(vNode)!); } else { isVNode - ? vnode_getDOMChildNodes(journal, vNode, true, childNodes as (ElementVNode | TextVNode)[]) - : vnode_getDOMChildNodes(journal, vNode, false, childNodes as (Element | Text)[]); + ? vnode_getDOMChildNodes(vNode, true, childNodes as (ElementVNode | TextVNode)[]) + : vnode_getDOMChildNodes(vNode, false, childNodes as (Element | Text)[]); } vNode = vNode.nextSibling as VNode | null; } @@ -605,60 +641,67 @@ const vnode_getDomSibling = ( return null; }; -const vnode_ensureInflatedIfText = (journal: VNodeJournal, vNode: VNode): void => { +const vnode_ensureInflatedIfText = (vNode: VNode): void => { if (vnode_isTextVNode(vNode)) { - vnode_ensureTextInflated(journal, vNode); + vnode_ensureTextInflated(vNode); } }; -const vnode_ensureTextInflated = (journal: VNodeJournal, vnode: TextVNode) => { +const vnode_ensureTextInflated = (vnode: TextVNode) => { const textVNode = ensureTextVNode(vnode); const flags = textVNode.flags; 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 subCursor = 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 (subCursor && vnode_isTextVNode(subCursor)) { + if ((subCursor.flags & VNodeFlags.Inflated) === 0) { + const textNode = doc.createTextNode(subCursor.text!); lastPreviousTextNode = textNode; - cursor.textNode = textNode; - cursor.flags |= VNodeFlags.Inflated; + subCursor.node = textNode; + subCursor.flags |= VNodeFlags.Inflated; + addVNodeOperation(subCursor, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode, + target: lastPreviousTextNode, + }); } - cursor = vnode_getDomSibling(cursor, false, true); + subCursor = vnode_getDomSibling(subCursor, false, true); } // Walk the next siblings and inflate them. - cursor = vnode; - while (cursor && vnode_isTextVNode(cursor)) { - const next = vnode_getDomSibling(cursor, true, true); + subCursor = vnode; + while (subCursor && vnode_isTextVNode(subCursor)) { + const next = vnode_getDomSibling(subCursor, true, true); const isLastNode = next ? !vnode_isTextVNode(next) : true; - if ((cursor.flags & VNodeFlags.Inflated) === 0) { + if ((subCursor.flags & VNodeFlags.Inflated) === 0) { if (isLastNode && sharedTextNode) { - journal.push(VNodeJournalOpCode.SetText, sharedTextNode, cursor.text!); + addVNodeOperation(subCursor, { + operationType: VNodeOperationType.SetText, + }); } else { - const textNode = doc.createTextNode(cursor.text!); - journal.push(VNodeJournalOpCode.Insert, parentNode, insertBeforeNode, textNode); - cursor.textNode = textNode; + const textNode = doc.createTextNode(subCursor.text!); + addVNodeOperation(subCursor, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode, + target: insertBeforeNode, + }); + subCursor.node = textNode; } - cursor.flags |= VNodeFlags.Inflated; + subCursor.flags |= VNodeFlags.Inflated; } - cursor = next; + subCursor = next; } } }; @@ -666,7 +709,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 +794,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 +818,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; }; @@ -792,12 +835,7 @@ const indexOfAlphanumeric = (id: string, length: number): number => { return length; }; -export const vnode_createErrorDiv = ( - document: Document, - host: VNode, - err: Error, - journal: VNodeJournal -) => { +export const vnode_createErrorDiv = (document: Document, host: VNode, err: Error) => { const errorDiv = document.createElement('errored-host'); if (err && err instanceof Error) { (errorDiv as any).props = { error: err }; @@ -806,8 +844,8 @@ export const vnode_createErrorDiv = ( const vErrorDiv = vnode_newElement(errorDiv, 'errored-host'); - vnode_getDOMChildNodes(journal, host, true).forEach((child) => { - vnode_insertBefore(journal, vErrorDiv, child, null); + vnode_getDOMChildNodes(host, true).forEach((child) => { + vnode_insertBefore(vErrorDiv, child, null); }); return vErrorDiv; }; @@ -961,6 +999,7 @@ export const vnode_applyJournal = (journal: VNodeJournal) => { } break; case VNodeJournalOpCode.HoistStyles: + // TODO move DOM container start const document = journal[idx++] as Document; const head = document.head; const styles = document.querySelectorAll(QStylesAllSelector); @@ -1002,7 +1041,6 @@ export const vnode_applyJournal = (journal: VNodeJournal) => { ////////////////////////////////////////////////////////////////////////////////////////////////////// export const vnode_insertBefore = ( - journal: VNodeJournal, parent: ElementVNode | VirtualVNode, newChild: VNode, insertBefore: VNode | null @@ -1011,7 +1049,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,14 +1083,10 @@ 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, - domParentVNode, - newChild - ); + domChildren = vnode_getDomChildrenWithCorrectNamespacesToInsert(domParentVNode, newChild); } /** @@ -1096,14 +1130,14 @@ export const vnode_insertBefore = ( newChildCurrentParent && (newChild.previousSibling || newChild.nextSibling || newChildCurrentParent !== parent) ) { - vnode_remove(journal, newChildCurrentParent, newChild, false); + vnode_remove(newChildCurrentParent, newChild, false); } 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. @@ -1118,17 +1152,7 @@ export const vnode_insertBefore = ( } else { adjustedInsertBefore = 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 - ); - } + adjustedInsertBefore && vnode_ensureInflatedIfText(adjustedInsertBefore); } // link newChild into the previous/next list @@ -1150,15 +1174,23 @@ export const vnode_insertBefore = ( if (parentIsDeleted) { // if the parent is deleted, then the new child is also deleted newChild.flags |= VNodeFlags.Deleted; + } else { + // Here we know the insertBefore node + if (domChildren && domChildren.length) { + for (const child of domChildren) { + addVNodeOperation(child, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode!, + target: vnode_getNode(adjustedInsertBefore), + }); + } + } } }; -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 = ( @@ -1172,25 +1204,31 @@ export const vnode_getDomParentVNode = ( }; export const vnode_remove = ( - journal: VNodeJournal, vParent: ElementVNode | VirtualVNode, vToRemove: VNode, removeDOM: boolean ) => { assertEqual(vParent, vToRemove.parent, 'Parent mismatch.'); if (vnode_isTextVNode(vToRemove)) { - vnode_ensureTextInflated(journal, vToRemove); + vnode_ensureTextInflated(vToRemove); } 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(vToRemove, true); + //&& //journal.push(VNodeJournalOpCode.Remove, domParent, ...children); + if (domParent && children.length) { + for (const child of children) { + addVNodeOperation(child, { + operationType: VNodeOperationType.Delete, + }); + } + } } const vPrevious = vToRemove.previousSibling; @@ -1230,19 +1268,23 @@ export const vnode_queryDomNodes = ( } }; -export const vnode_truncate = ( - journal: VNodeJournal, - vParent: ElementVNode | VirtualVNode, - vDelete: VNode -) => { +export const vnode_truncate = (vParent: ElementVNode | VirtualVNode, vDelete: VNode) => { assertDefined(vDelete, 'Missing vDelete.'); const parent = vnode_getDomParent(vParent); if (parent) { if (vnode_isElementVNode(vParent)) { - journal.push(VNodeJournalOpCode.RemoveAll, parent); + addVNodeOperation(vParent, { + operationType: VNodeOperationType.RemoveAllChildren, + }); } else { - const children = vnode_getDOMChildNodes(journal, vParent); - children.length && journal.push(VNodeJournalOpCode.Remove, parent, ...children); + const children = vnode_getDOMChildNodes(vParent, true); + if (children.length) { + for (const child of children) { + addVNodeOperation(child, { + operationType: VNodeOperationType.Delete, + }); + } + } } } const vPrevious = vDelete.previousSibling; @@ -1260,7 +1302,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 +1313,17 @@ 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)); +export const vnode_setText = (textVNode: TextVNode, text: string) => { + vnode_ensureTextInflated(textVNode); + textVNode.text = text; + addVNodeOperation(textVNode, { + operationType: VNodeOperationType.SetText, + }); }; /** @internal */ @@ -1295,7 +1339,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); @@ -1354,7 +1398,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 +1599,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 +1715,14 @@ 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) { + const keys = Object.keys(props); + for (let i = 0; i < Math.min(keys.length, 20); i++) { + const key = keys[i]; + if (!key.startsWith(Q_PROPS_SEPARATOR)) { + keys.push(key); + } } } return keys; @@ -1683,12 +1730,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 +1749,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 +1782,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('') + '>'); @@ -1766,7 +1803,7 @@ export function vnode_toString( const attrs: string[] = []; 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 +1906,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 +1928,11 @@ function materializeFromVNodeData( } else { value = consumeValue(); } - vParent.setAttr(ELEMENT_KEY, value, null); + vnode_setProp(vParent, ELEMENT_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 +1944,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 +1955,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 +1965,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 +2038,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/debug.ts b/packages/qwik/src/core/debug.ts index 5645370d4c4..b67ae7b5225 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'; 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'; @@ -11,51 +11,56 @@ import { isTask } from './use/use-task'; 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) + ')'; + } 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) + ')'; } - } 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; } diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 317dff3543d..1f6f4dda81a 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -20,18 +20,15 @@ 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'; +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 { _hasStoreEffects, isStore as _isStore } from './reactive-primitives/impl/store'; export { _wrapProp, _wrapSignal } from './reactive-primitives/internal-api'; 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..f69bd6b434c 100644 --- a/packages/qwik/src/core/reactive-primitives/cleanup.ts +++ b/packages/qwik/src/core/reactive-primitives/cleanup.ts @@ -3,21 +3,11 @@ 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..4ad842d5fcd 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,7 @@ 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$ - ); + scheduleEffects(this.$container$, this, this.$loadingEffects$); } } @@ -94,12 +87,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$); } } 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..c27c05a0796 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 { 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,12 +52,7 @@ export class ComputedSignalImpl> invalidate() { this.$flags$ |= SignalFlags.INVALID; - 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-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/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/wrapped-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts index 164ffdc39fc..a7783220768 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,17 @@ +import { vnode_setProp } from '../../client/vnode'; 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 { HOST_EFFECTS } from '../../shared/utils/markers'; +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'; export class WrappedSignalImpl extends SignalImpl implements BackRef { $args$: any[]; @@ -48,12 +45,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$) { + vnode_setProp(this.$hostElement$, HOST_EFFECTS, this.$effects$); + 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 +63,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$) { + vnode_setProp(this.$hostElement$, HOST_EFFECTS, this.$effects$); + 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..f62a93fab14 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,7 @@ 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'; /** * # ================================ @@ -24,9 +23,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 +73,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; /** * 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..a7593b98308 100644 --- a/packages/qwik/src/core/reactive-primitives/utils.ts +++ b/packages/qwik/src/core/reactive-primitives/utils.ts @@ -1,23 +1,19 @@ 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'; +import type { SSRContainer } from '../ssr/ssr-types'; import { TaskFlags, isTask } from '../use/use-task'; 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,10 @@ 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'; const DEBUG = false; @@ -77,7 +77,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 +98,27 @@ 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, 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); + setNodePropData(consumer, 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..17515bd2547 100644 --- a/packages/qwik/src/core/shared/component-execution.ts +++ b/packages/qwik/src/core/shared/component-execution.ts @@ -2,7 +2,7 @@ import { isDev } from '@qwik.dev/core/build'; import { vnode_isVNode } from '../client/vnode'; import { isSignal } from '../reactive-primitives/utils'; import { clearAllEffects } from '../reactive-primitives/cleanup'; -import { invokeApply, newInvokeContext, untrack } from '../use/use-core'; +import { invokeApply, newInvokeContext, 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'; @@ -63,7 +63,7 @@ export const executeComponent = ( subscriptionHost || undefined, undefined, RenderEvent - ); + ) as RenderInvokeContext; if (subscriptionHost) { iCtx.$effectSubscriber$ = getSubscriber(subscriptionHost, EffectProperty.COMPONENT); iCtx.$container$ = container; @@ -77,6 +77,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 +107,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..0357a7e091e --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/chore-execution.ts @@ -0,0 +1,338 @@ +import { vnode_isVNode } from '../../client/vnode'; +import { vnode_diff } from '../../client/vnode-diff'; +import { clearAllEffects } from '../../reactive-primitives/cleanup'; +import { runResource, type ResourceDescriptor } from '../../use/use-resource'; +import { Task, TaskFlags, runTask, type TaskFn } from '../../use/use-task'; +import { executeComponent } from '../component-execution'; +import { isServerPlatform } from '../platform/platform'; +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, + NODE_PROPS_DATA_KEY, + NODE_DIFF_DATA_KEY, +} from '../utils/markers'; +import { addComponentStylePrefix } from '../utils/scoped-styles'; +import { isPromise, 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 { Cursor } from './cursor'; +import type { NodeProp } from '../../reactive-primitives/subscription-data'; +import { isSignal } from '../../reactive-primitives/utils'; +import type { Signal } from '../../reactive-primitives/signal.public'; +import { serializeAttribute } from '../utils/styles'; +import type { ISsrNode, SSRContainer } from '../../ssr/ssr-types'; +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 { setExtraPromises } from './cursor-props'; + +/** + * 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, + cursor: Cursor +): 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; + let extraPromises: 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) + // no dirty propagation needed, dirtyChildren array is enough + vNode.dirty |= ChoreBits.VISIBLE_TASKS; + } else { + // Regular tasks: chain promises only between each other + const result = runTask(task, container, vNode); + if (isPromise(result)) { + if (task.$flags$ & TaskFlags.RENDER_BLOCKING) { + taskPromise = taskPromise + ? taskPromise.then(() => result as Promise) + : (result as Promise); + } else { + extraPromises ||= []; + extraPromises.push(result as Promise); + } + } + } + } + } + + if (extraPromises) { + setExtraPromises(isServerPlatform() ? vNode : cursor, extraPromises); + } + 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): 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; + } + const result = vnode_diff(container as ClientContainer, jsx, domVNode, null); + return result; +} + +/** + * 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): ValueOrPromise { + vNode.dirty &= ~ChoreBits.COMPONENT; + const host = vNode as HostElement; + const componentQRL = container.getHostProp> | null>( + host, + OnRenderProp + ); + + if (!componentQRL) { + return; + } + + const isServer = isServerPlatform(); + const props = container.getHostProp(host, ELEMENT_PROPS) || null; + + const result = safeCall( + () => executeComponent(container, host, host, componentQRL, props), + (jsx) => { + if (isServer) { + return jsx; + } else { + const styleScopedId = container.getHostProp(host, QScopedStyle); + return retryOnPromise(() => + vnode_diff( + container as ClientContainer, + jsx, + host, + addComponentStylePrefix(styleScopedId) + ) + ); + } + }, + (err: any) => { + container.handleError(err, host); + throw err; + } + ); + + 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, + property: string, + value: string | boolean | null, + isConst: boolean +): void { + if (!domVNode.operation) { + domVNode.operation = { + operationType: VNodeOperationType.None, + attrs: { + [property]: value, + }, + }; + } else { + // TODO: is it safe to assume attrs is always defined here? + domVNode.operation.attrs![property] = 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): 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; + + const isServer = isServerPlatform(); + + // 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); + if (isServer) { + (container as SSRContainer).addBackpatchEntry( + // TODO: type + (vNode as unknown as ISsrNode).id, + property, + serializedValue + ); + } else { + const isConst = nodeProp.isConst; + setNodeProp(domVNode, 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; + + if (vnode_isVNode(vNode)) { + // TODO I dont think this runs the cleanups of visible tasks + // TODO add promises to extraPromises + clearAllEffects(container, vNode); + } +} + +/** + * 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, container: Container): ValueOrPromise { + vNode.dirty &= ~ChoreBits.COMPUTE; + + // Compute chores are typically handled by the reactive system. + // This is a placeholder for explicit compute chores if needed. + + // TODO remove or use + + return; +} 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..1cdc2fa7633 --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-flush.ts @@ -0,0 +1,151 @@ +import { vnode_isElementOrTextVNode, vnode_isVirtualVNode } from '../../client/vnode'; +import { runTask, Task, TaskFlags } from '../../use/use-task'; +import { QContainerValue, type Container } from '../types'; +import { dangerouslySetInnerHTML, ELEMENT_SEQ, QContainerAttr } from '../utils/markers'; +import type { ElementVNode } from '../vnode/element-vnode'; +import { ChoreBits } from '../vnode/enums/chore-bits.enum'; +import { VNodeOperationType } from '../vnode/enums/vnode-operation-type.enum'; +import type { TextVNode } from '../vnode/text-vnode'; +import type { TargetAndParentDomVNodeOperation } from '../vnode/types/dom-vnode-operation'; +import type { VNode } from '../vnode/vnode'; +import type { Cursor } from './cursor'; + +/** + * 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 visibleTasks: Task[] = []; + flushChanges(cursor, container, visibleTasks); + executeAfterFlush(container, visibleTasks); +} + +function flushChanges( + vNode: VNode, + container: Container, + visibleTasks: Task[], + skipRender?: boolean +): void { + if (!skipRender) { + if (vnode_isVirtualVNode(vNode)) { + if (vNode.dirty & ChoreBits.VISIBLE_TASKS) { + vNode.dirty &= ~ChoreBits.VISIBLE_TASKS; + + const sequence = container.getHostProp(vNode, ELEMENT_SEQ); + if (sequence) { + for (const sequenceItem of sequence) { + if ( + sequenceItem instanceof Task && + sequenceItem.$flags$ & TaskFlags.VISIBLE_TASK && + sequenceItem.$flags$ & TaskFlags.DIRTY + ) { + visibleTasks.push(sequenceItem); + } + } + } + } + if (vNode.operation && vNode.operation.operationType & VNodeOperationType.SkipRender) { + skipRender = true; + } + } else if (vnode_isElementOrTextVNode(vNode) && vNode.operation) { + if (vNode.operation.operationType & VNodeOperationType.RemoveAllChildren) { + const removeParent = (vNode as ElementVNode).node!; + if (removeParent.replaceChildren) { + removeParent.replaceChildren(); + } else { + // fallback if replaceChildren is not supported + removeParent.textContent = ''; + } + } + + if (vNode.operation.operationType & VNodeOperationType.SetText) { + (vNode as TextVNode).node!.nodeValue = (vNode as TextVNode).text!; + } + + if (vNode.operation.operationType & VNodeOperationType.InsertOrMove) { + const operation = vNode.operation as TargetAndParentDomVNodeOperation; + const insertBefore = operation.target; + const insertBeforeParent = operation.parent; + insertBeforeParent.insertBefore(vNode.node!, insertBefore); + } else if (vNode.operation.operationType & VNodeOperationType.Delete) { + vNode.node!.remove(); + } + + if (vNode.operation.attrs) { + const element = (vNode as ElementVNode).node!; + for (const [attrName, attrValue] of Object.entries(vNode.operation.attrs)) { + 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); + } + } + } + vNode.operation = null; + vNode.dirty &= ~ChoreBits.OPERATION; + } + } + + if (vNode.dirtyChildren) { + for (const child of vNode.dirtyChildren) { + flushChanges(child, container, visibleTasks, skipRender); + } + vNode.dirtyChildren = null; + } +} + +function executeAfterFlush(container: Container, visibleTasks: Task[]): void { + if (!visibleTasks.length) { + return; + } + for (const visibleTask of visibleTasks) { + const task = visibleTask; + runTask(task, container, task.$el$); + } +} + +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..15b5409e4d7 --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-props.ts @@ -0,0 +1,136 @@ +import { VNodeFlags } from '../../client/types'; +import type { VNode } from '../vnode/vnode'; +import type { Props } from '../jsx/jsx-runtime'; +import { isCursor } from './cursor'; +import { removeCursorFromQueue } from './cursor-queue'; + +/** + * Keys used to store cursor-related data in vNode props. These are internal properties that should + * not conflict with user props. + */ +const CURSOR_PRIORITY_KEY = 'q:priority'; +const CURSOR_POSITION_KEY = 'q:position'; +const CURSOR_CHILD_KEY = 'q:childIndex'; +const VNODE_PROMISE_KEY = 'q:promise'; +const CURSOR_EXTRA_PROMISES_KEY = 'q:extraPromises'; + +/** + * Gets the priority of a cursor vNode. + * + * @param vNode - The cursor vNode + * @returns The priority, or null if not a cursor + */ +export function getCursorPriority(vNode: VNode): number | null { + if (!(vNode.flags & VNodeFlags.Cursor)) { + return null; + } + const props = vNode.props as Props; + return (props[CURSOR_PRIORITY_KEY] as number) ?? null; +} + +/** + * Sets the priority of a cursor vNode. + * + * @param vNode - The vNode to set priority on + * @param priority - The priority value + */ +export function setCursorPriority(vNode: VNode, priority: number): void { + const props = (vNode.props ||= {}); + props[CURSOR_PRIORITY_KEY] = priority; +} + +/** + * Gets the current cursor position from a cursor vNode. + * + * @param vNode - The cursor vNode + * @returns The cursor position, or null if at root or not a cursor + */ +export function getCursorPosition(vNode: VNode): VNode | null { + if (!(vNode.flags & VNodeFlags.Cursor)) { + return null; + } + const props = vNode.props; + return (props?.[CURSOR_POSITION_KEY] as VNode | null) ?? null; +} + +/** + * Set the next child to process index in a vNode. + * + * @param vNode - The vNode + * @param childIndex - The child index to set + */ +export function setNextChildIndex(vNode: VNode, childIndex: number): void { + const props = vNode.props as Props; + // We could also add a dirtycount to avoid checking all children after completion + // perf: we could also use dirtychild index 0 for the index instead + props[CURSOR_CHILD_KEY] = childIndex; +} + +/** Gets the next child to process index from a vNode. */ +export function getNextChildIndex(vNode: VNode): number | null { + const props = vNode.props as Props; + return (props[CURSOR_CHILD_KEY] as number) ?? null; +} + +/** + * 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(vNode: VNode, position: VNode | null): void { + const props = vNode.props as Props; + props[CURSOR_POSITION_KEY] = position; + if (position && isCursor(position)) { + // delete from global cursors queue + removeCursorFromQueue(position); + // TODO: merge extraPromises + // const extraPromises = getCursorExtraPromises(position); + // if (extraPromises) { + // } + } +} + +/** + * Gets the blocking promise from a vNode. + * + * @param vNode - The vNode + * @returns The promise, or null if none or not a cursor + */ +export function getVNodePromise(vNode: VNode): Promise | null { + const props = vNode.props; + return (props?.[VNODE_PROMISE_KEY] as Promise | null) ?? null; +} + +/** + * Sets the blocking promise on a vNode. + * + * @param vNode - The vNode + * @param promise - The promise to set, or null to clear + */ +export function setVNodePromise(vNode: VNode, promise: Promise | null): void { + const props = (vNode.props ||= {}); + props[VNODE_PROMISE_KEY] = promise; +} + +/** + * Gets extra promises from a vNode. + * + * @param vNode - The vNode + * @returns The extra promises set + */ +export function getExtraPromises(vNode: VNode): Promise[] | null { + const props = vNode.props; + return (props?.[CURSOR_EXTRA_PROMISES_KEY] as Promise[] | null) ?? null; +} + +/** + * Sets extra promises on a vNode. + * + * @param vNode - The vNode + * @param extraPromises - The extra promises set, or null to clear + */ +export function setExtraPromises(vNode: VNode, extraPromises: Promise[] | null): void { + const props = (vNode.props ||= {}); + props[CURSOR_EXTRA_PROMISES_KEY] = extraPromises; +} 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..c32d71e0d1c --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-queue.ts @@ -0,0 +1,87 @@ +/** + * @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 { getCursorPriority } 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 = getCursorPriority(cursor)!; + let insertIndex = globalCursorQueue.length; + + for (let i = 0; i < globalCursorQueue.length; i++) { + const existingPriority = getCursorPriority(globalCursorQueue[i])!; + 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 using swap-and-remove algorithm for O(1) removal. + * + * @param cursor - The cursor to remove + */ +export function removeCursorFromQueue(cursor: Cursor): void { + cursor.flags &= ~VNodeFlags.Cursor; + const index = globalCursorQueue.indexOf(cursor); + if (index !== -1) { + // 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(); + } +} + +/** + * 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..6616b6f88c5 --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-walker.ts @@ -0,0 +1,247 @@ +/** + * @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 { + getCursorPosition, + setCursorPosition, + getVNodePromise, + setVNodePromise, + setNextChildIndex, + getNextChildIndex, +} from './cursor-props'; +import { ChoreBits } from '../vnode/enums/chore-bits.enum'; +import { getHighestPriorityCursor, removeCursorFromQueue } from './cursor-queue'; +import { executeFlushPhase } from './cursor-flush'; +import { getDomContainer } from '../../client/dom-container'; +import { createNextTick } from '../platform/next-tick'; +import { vnode_isElementVNode } from '../../client/vnode'; +import type { Container } from '../types'; +import { VNodeFlags } from '../../client/types'; +import { isPromise } from '../utils/promises'; +import type { ValueOrPromise } from '../utils/types'; + +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 container - The container + * @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); + if (!(cursor.dirty & ChoreBits.DIRTY_MASK)) { + removeCursorFromQueue(cursor); + } + } +} + +export function findContainerForVNode(vNode: VNode): Container { + let element: Element; + if (vnode_isElementVNode(vNode)) { + element = vNode.node; + } else { + let parent = vNode.parent; + while (parent) { + if (vnode_isElementVNode(parent)) { + element = parent.node; + break; + } + parent = parent.parent; + } + } + return getDomContainer(element!); +} +let globalCount = 0; + +/** + * 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 container - The container + * @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(); + + // Check if cursor is already complete + if (!cursor.dirty) { + return; + } + + // Check if cursor is blocked by a promise + const blockingPromise = getVNodePromise(cursor); + if (blockingPromise) { + return; + } + + globalCount++; + if (globalCount > 100) { + throw new Error('Infinite loop detected in cursor walker'); + } + + const container = findContainerForVNode(cursor); + // Get starting position (resume from last position or start at root) + let currentVNode: VNode | null = null; + + let count = 0; + while ((currentVNode = getCursorPosition(cursor))) { + if (count++ > 100) { + 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; + } + } + + // Skip if the vNode is not dirty + if (!(currentVNode.dirty & ChoreBits.DIRTY_MASK) || getVNodePromise(currentVNode)) { + // Move to next node + setCursorPosition(cursor, getNextVNode(currentVNode)); + continue; + } + + let result: ValueOrPromise | undefined; + // Execute chores in order + if (currentVNode.dirty & ChoreBits.TASKS) { + result = executeTasks(currentVNode, container, cursor); + } else if (currentVNode.dirty & ChoreBits.NODE_DIFF) { + result = executeNodeDiff(currentVNode, container); + } else if (currentVNode.dirty & ChoreBits.COMPONENT) { + result = executeComponentChore(currentVNode, container); + } else if (currentVNode.dirty & ChoreBits.NODE_PROPS) { + executeNodeProps(currentVNode, container); + } 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 { + setNextChildIndex(currentVNode, 0); + // descend + currentVNode = getNextVNode(dirtyChildren[0])!; + setCursorPosition(cursor, currentVNode); + continue; + } + } else if (currentVNode.dirty & ChoreBits.CLEANUP) { + executeCleanup(currentVNode, container); + } + + // Handle blocking promise + if (result && isPromise(result)) { + // Store promise on cursor and pause + setVNodePromise(cursor, result); + // pauseCursor(cursor, currentVNode); + + result + .catch((error) => { + setVNodePromise(cursor, null); + container.handleError(error, currentVNode); + }) + .finally(() => { + setVNodePromise(cursor, null); + triggerCursors(); + }); + return; + } + } + if (!(cursor.dirty & ChoreBits.DIRTY_MASK)) { + // Walk complete + cursor.flags &= ~VNodeFlags.Cursor; + if (!isServer) { + executeFlushPhase(cursor, container); + } + // 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; + } + } +} + +/** @returns Next vNode to process, or null if traversal is complete */ +function getNextVNode(vNode: VNode): VNode | null { + const parent = vNode.parent || vNode.slotParent; + if (!parent || !(parent.dirty & ChoreBits.CHILDREN)) { + return null; + } + const dirtyChildren = parent.dirtyChildren!; + let index = getNextChildIndex(parent)!; + + const len = dirtyChildren!.length; + let count = len; + while (count-- > 0) { + const nextVNode = dirtyChildren[index]; + if (nextVNode.dirty & ChoreBits.DIRTY_MASK) { + setNextChildIndex(parent, index + 1); + return nextVNode; + } + index++; + if (index === len) { + index = 0; + } + } + // all array items checked, children are no longer dirty + parent!.dirty &= ~ChoreBits.CHILDREN; + 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..2bd29965520 --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor.ts @@ -0,0 +1,84 @@ +import { VNodeFlags } from '../../client/types'; +import type { Container } from '../types'; +import type { VNode } from '../vnode/vnode'; +import { setCursorPriority, setCursorPosition } 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 { + setCursorPriority(root, priority); + setCursorPosition(root, root); + + 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; +} + +/** + * Pauses a cursor at the given vNode position. Sets the cursor position for time-slicing or promise + * waiting. + * + * @param cursor - The cursor (vNode with CURSOR flag set) to pause + * @param vNode - The vNode position to pause at + */ +export function pauseCursor(cursor: Cursor, vNode: VNode): void { + setCursorPosition(cursor, vNode); +} + +/** + * 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/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/serdes/inflate.ts b/packages/qwik/src/core/shared/serdes/inflate.ts index b2ecee9c596..e16ef370d15 100644 --- a/packages/qwik/src/core/shared/serdes/inflate.ts +++ b/packages/qwik/src/core/shared/serdes/inflate.ts @@ -1,4 +1,3 @@ -import type { DomContainer } from '../../client/dom-container'; import { _EFFECT_BACK_REF } from '../../internal'; import type { AsyncComputedSignalImpl } from '../../reactive-primitives/impl/async-computed-signal-impl'; import type { ComputedSignalImpl } from '../../reactive-primitives/impl/computed-signal-impl'; @@ -24,7 +23,6 @@ import { PropsProxy } from '../jsx/props-proxy'; import { JSXNodeImpl } from '../jsx/jsx-node'; import type { QRLInternal } from '../qrl/qrl-class'; import type { DeserializeContainer, HostElement } from '../types'; -import { ChoreType } from '../util-chore-type'; import { _CONST_PROPS, _OWNER, @@ -38,12 +36,13 @@ import { resolvers } from './allocate'; import { TypeIds } from './constants'; import { vnode_getFirstChild, + vnode_getProp, vnode_getText, vnode_isTextVNode, vnode_isVNode, } from '../../client/vnode'; -import type { VirtualVNode } from '../../client/vnode-impl'; import { isString } from '../utils/types'; +import type { VirtualVNode } from '../vnode/virtual-vnode'; export const inflate = ( container: DeserializeContainer, @@ -207,7 +206,6 @@ export const inflate = ( */ // try to download qrl in this tick computed.$computeQrl$.resolve(); - (container as DomContainer).$scheduler$(ChoreType.QRL_RESOLVE, null, computed.$computeQrl$); } break; } @@ -347,7 +345,7 @@ export function inflateWrappedSignalValue(signal: WrappedSignalImpl) { for (const [_, key] of effects) { if (isString(key)) { // This is an attribute name, try to read its value - const attrValue = hostVNode.getAttr(key); + const attrValue = vnode_getProp(hostVNode, key, null); if (attrValue !== null) { signal.$untrackedValue$ = attrValue; hasAttrValue = true; diff --git a/packages/qwik/src/core/shared/serdes/serdes.public.ts b/packages/qwik/src/core/shared/serdes/serdes.public.ts index e6e360d9bdc..dbdd874167d 100644 --- a/packages/qwik/src/core/shared/serdes/serdes.public.ts +++ b/packages/qwik/src/core/shared/serdes/serdes.public.ts @@ -82,7 +82,6 @@ export function _createDeserializeContainer( $storeProxyMap$: new WeakMap(), element: null, $forwardRefs$: null, - $scheduler$: null, }; preprocessState(stateData, container); state = wrapDeserializerProxy(container as any, stateData); diff --git a/packages/qwik/src/core/shared/shared-container.ts b/packages/qwik/src/core/shared/shared-container.ts index 48a5d9bde19..07180e45348 100644 --- a/packages/qwik/src/core/shared/shared-container.ts +++ b/packages/qwik/src/core/shared/shared-container.ts @@ -4,18 +4,15 @@ import { version } from '../version'; import type { SubscriptionData } from '../reactive-primitives/subscription-data'; import type { Signal } from '../reactive-primitives/signal.public'; import type { StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; -import { createScheduler, Scheduler, type Chore } from './scheduler'; import { createSerializationContext, type SerializationContext, } from './serdes/serialization-context'; import type { Container, HostElement, ObjToProxyMap } from './types'; -import { ChoreArray } from '../client/chore-array'; /** @internal */ export abstract class _SharedContainer implements Container { readonly $version$: string; - readonly $scheduler$: Scheduler; readonly $storeProxyMap$: ObjToProxyMap; /// Current language locale readonly $locale$: string; @@ -25,9 +22,11 @@ export abstract class _SharedContainer implements Container { $currentUniqueId$ = 0; $instanceHash$: string | null = null; $buildBase$: string | null = null; - $flushEpoch$: number = 0; + $renderPromise$: Promise | null = null; + $resolveRenderPromise$: (() => void) | null = null; + $cursorCount$: number = 0; - constructor(journalFlush: () => void, serverData: Record, locale: string) { + constructor(serverData: Record, locale: string) { this.$serverData$ = serverData; this.$locale$ = locale; this.$version$ = version; @@ -35,17 +34,6 @@ export abstract class _SharedContainer implements Container { this.$getObjectById$ = (_id: number | string) => { throw Error('Not implemented'); }; - - const choreQueue = new ChoreArray(); - const blockedChores = new ChoreArray(); - const runningChores = new Set(); - this.$scheduler$ = createScheduler( - this, - journalFlush, - choreQueue, - blockedChores, - runningChores - ); } trackSignalValue( diff --git a/packages/qwik/src/core/shared/types.ts b/packages/qwik/src/core/shared/types.ts index fa4d35b223c..ff09a930585 100644 --- a/packages/qwik/src/core/shared/types.ts +++ b/packages/qwik/src/core/shared/types.ts @@ -1,9 +1,8 @@ import type { ContextId } from '../use/use-context'; -import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; -import type { Scheduler } from './scheduler'; +import type { StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; import type { SerializationContext } from './serdes/index'; -import type { VNode } from '../client/vnode-impl'; import type { ValueOrPromise } from './utils/types'; +import type { VNode } from './vnode/vnode'; export interface DeserializeContainer { $getObjectById$: (id: number | string) => unknown; @@ -12,12 +11,10 @@ export interface DeserializeContainer { $state$?: unknown[]; $storeProxyMap$: ObjToProxyMap; $forwardRefs$: Array | null; - readonly $scheduler$: Scheduler | null; } export interface Container { readonly $version$: string; - readonly $scheduler$: Scheduler; readonly $storeProxyMap$: ObjToProxyMap; /// Current language locale readonly $locale$: string; @@ -26,6 +23,9 @@ export interface Container { readonly $serverData$: Record; $currentUniqueId$: number; $buildBase$: string | null; + $renderPromise$: Promise | null; + $resolveRenderPromise$: (() => void) | null; + $cursorCount$: number; handleError(err: any, $host$: HostElement | null): void; getParentHost(host: HostElement): HostElement | null; @@ -55,7 +55,7 @@ export interface Container { ): SerializationContext; } -export type HostElement = VNode | ISsrNode; +export type HostElement = VNode; export interface QElement extends Element { qDispatchEvent?: (event: Event, scope: QwikLoaderEventScope) => ValueOrPromise; diff --git a/packages/qwik/src/core/shared/utils/markers.ts b/packages/qwik/src/core/shared/utils/markers.ts index ffa75e455c9..2772c587721 100644 --- a/packages/qwik/src/core/shared/utils/markers.ts +++ b/packages/qwik/src/core/shared/utils/markers.ts @@ -81,6 +81,10 @@ export const ELEMENT_PROPS = 'q:props'; export const ELEMENT_SEQ = 'q:seq'; export const ELEMENT_SEQ_IDX = 'q:seqIdx'; export const ELEMENT_BACKPATCH_DATA = 'qwik/backpatch'; +/** Key used to store pending node prop updates in vNode props. */ +export const NODE_PROPS_DATA_KEY = 'q:nodeProps'; +export const NODE_DIFF_DATA_KEY = 'q:nodeDiff'; +export const HOST_EFFECTS = 'q:effects'; export const Q_PREFIX = 'q:'; /** Non serializable markers - always begins with `:` character */ diff --git a/packages/qwik/src/core/shared/vnode/element-vnode.ts b/packages/qwik/src/core/shared/vnode/element-vnode.ts new file mode 100644 index 00000000000..d0eee17eb7f --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/element-vnode.ts @@ -0,0 +1,23 @@ +import type { VNodeFlags } from '../../client/types'; +import type { Props } from '../jsx/jsx-runtime'; +import { VNode } from './vnode'; +import type { VNodeOperation } from './types/dom-vnode-operation'; + +export class ElementVNode extends VNode { + operation: VNodeOperation | null = null; + + constructor( + public key: string | null, + flags: VNodeFlags, + parent: VNode | null, + previousSibling: VNode | null | undefined, + nextSibling: VNode | null | undefined, + props: Props | null, + public firstChild: VNode | null | undefined, + public lastChild: VNode | null | undefined, + public node: Element, + public elementName: string | undefined + ) { + super(flags, parent, previousSibling, nextSibling, props); + } +} diff --git a/packages/qwik/src/core/shared/vnode/enums/chore-bits.enum.ts b/packages/qwik/src/core/shared/vnode/enums/chore-bits.enum.ts new file mode 100644 index 00000000000..3b354ddc500 --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/enums/chore-bits.enum.ts @@ -0,0 +1,14 @@ +export const enum ChoreBits { + NONE = 0, + TASKS = 1 << 0, + NODE_DIFF = 1 << 1, + COMPONENT = 1 << 2, + NODE_PROPS = 1 << 3, + COMPUTE = 1 << 4, + CHILDREN = 1 << 5, + CLEANUP = 1 << 6, + // marker used to identify if vnode has visible tasks + VISIBLE_TASKS = 1 << 7, + OPERATION = 1 << 8, + DIRTY_MASK = TASKS | NODE_DIFF | COMPONENT | NODE_PROPS | COMPUTE | CHILDREN | CLEANUP, +} diff --git a/packages/qwik/src/core/shared/vnode/enums/vnode-operation-type.enum.ts b/packages/qwik/src/core/shared/vnode/enums/vnode-operation-type.enum.ts new file mode 100644 index 00000000000..a82cdfb75bd --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/enums/vnode-operation-type.enum.ts @@ -0,0 +1,10 @@ +export const enum VNodeOperationType { + None = 0, + Delete = 1, + InsertOrMove = 2, + RemoveAllChildren = 4, + /** Only for text nodes */ + SetText = 8, + /** Do not apply changes to the subtree */ + SkipRender = 16, +} diff --git a/packages/qwik/src/core/shared/vnode/ssr-vnode.ts b/packages/qwik/src/core/shared/vnode/ssr-vnode.ts new file mode 100644 index 00000000000..739763e6a81 --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/ssr-vnode.ts @@ -0,0 +1,21 @@ +import type { VNodeFlags } from '../../client/types'; +import type { Props } from '../jsx/jsx-runtime'; +import { VNode } from './vnode'; + +export class SsrVNode extends VNode { + streamed = false; + // TODO: slots collection + + constructor( + public key: string | null, + flags: VNodeFlags, + parent: VNode | null, + previousSibling: VNode | null | undefined, + nextSibling: VNode | null | undefined, + props: Props | null, + public firstChild: VNode | null | undefined, + public lastChild: VNode | null | undefined + ) { + super(flags, parent, previousSibling, nextSibling, props); + } +} diff --git a/packages/qwik/src/core/shared/vnode/text-vnode.ts b/packages/qwik/src/core/shared/vnode/text-vnode.ts new file mode 100644 index 00000000000..9fd21b2e93a --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/text-vnode.ts @@ -0,0 +1,21 @@ +import type { VNodeFlags } from '../../client/types'; +import type { Props } from '../jsx/jsx-runtime'; +import type { VNodeOperation } from './types/dom-vnode-operation'; +import { VNode } from './vnode'; + +export class TextVNode extends VNode { + operation: VNodeOperation | null = null; + + constructor( + flags: VNodeFlags, + parent: VNode | null, + previousSibling: VNode | null | undefined, + nextSibling: VNode | null | undefined, + // normal text nodes don't have props, but we keep it because it can be a cursor + props: Props | null, + public node: Text | null, + public text: string | undefined + ) { + super(flags, parent, previousSibling, nextSibling, props); + } +} diff --git a/packages/qwik/src/core/shared/vnode/types/dom-vnode-operation.ts b/packages/qwik/src/core/shared/vnode/types/dom-vnode-operation.ts new file mode 100644 index 00000000000..231c0f7bf57 --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/types/dom-vnode-operation.ts @@ -0,0 +1,23 @@ +import type { VNodeOperationType } from '../enums/vnode-operation-type.enum'; + +export type VNodeOperation = TargetAndParentDomVNodeOperation | SimpleDomVNodeOperation; + +export type TargetAndParentDomVNodeOperation = { + operationType: VNodeOperationType.InsertOrMove; + target: Element | Text | null; + parent: Element; + attrs?: Record; +}; + +export type SimpleDomVNodeOperation = { + operationType: + | VNodeOperationType.None + | VNodeOperationType.Delete + | VNodeOperationType.RemoveAllChildren + | VNodeOperationType.SetText; + attrs?: Record; +}; + +export type VirtualVNodeOperation = { + operationType: VNodeOperationType.SkipRender; +}; diff --git a/packages/qwik/src/core/shared/vnode/virtual-vnode.ts b/packages/qwik/src/core/shared/vnode/virtual-vnode.ts new file mode 100644 index 00000000000..76bfbee85f3 --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/virtual-vnode.ts @@ -0,0 +1,22 @@ +import type { VNodeFlags } from '../../client/types'; +import type { Props } from '../jsx/jsx-runtime'; +import type { ElementVNode } from './element-vnode'; +import type { VirtualVNodeOperation } from './types/dom-vnode-operation'; +import { VNode } from './vnode'; + +export class VirtualVNode extends VNode { + operation: VirtualVNodeOperation | null = null; + + constructor( + public key: string | null, + flags: VNodeFlags, + parent: ElementVNode | VirtualVNode | null, + previousSibling: VNode | null | undefined, + nextSibling: VNode | null | undefined, + props: Props | null, + public firstChild: VNode | null | undefined, + public lastChild: VNode | null | undefined + ) { + super(flags, parent, previousSibling, nextSibling, props); + } +} diff --git a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts new file mode 100644 index 00000000000..0e1919304c5 --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts @@ -0,0 +1,69 @@ +import { getDomContainer } from '../../client/dom-container'; +import { addCursor, findCursor } from '../cursor/cursor'; +import { getCursorPosition, setCursorPosition } from '../cursor/cursor-props'; +import { findContainerForVNode } from '../cursor/cursor-walker'; +import type { Container } from '../types'; +import type { ElementVNode } from './element-vnode'; +import { ChoreBits } from './enums/chore-bits.enum'; +import type { TextVNode } from './text-vnode'; +import type { VNodeOperation } from './types/dom-vnode-operation'; +import type { VirtualVNode } from './virtual-vnode'; +import type { VNode } from './vnode'; + +export function propagateDirty(vNode: VNode, bits: ChoreBits): void {} +export function markVNodeDirty(container: Container | null, vNode: VNode, bits: ChoreBits): void { + const prevDirty = vNode.dirty; + vNode.dirty |= bits; + const isRealDirty = bits & ChoreBits.DIRTY_MASK; + // If already dirty, no need to propagate again + if (isRealDirty ? prevDirty & ChoreBits.DIRTY_MASK : prevDirty) { + return; + } + const parent = vNode.parent || vNode.slotParent; + // We must attach to a cursor subtree if it exists + if (parent && parent.dirty) { + if (isRealDirty) { + parent.dirty |= ChoreBits.CHILDREN; + } + parent.dirtyChildren ||= []; + parent.dirtyChildren.push(vNode); + + if (isRealDirty && vNode.dirtyChildren) { + // this node is maybe an ancestor of the current cursor position + // if so we must restart from here + const cursor = findCursor(vNode); + if (cursor) { + let cursorPosition = getCursorPosition(cursor); + if (cursorPosition) { + // find the ancestor of the cursor position that is current vNode + while (cursorPosition !== cursor) { + cursorPosition = cursorPosition.parent || cursorPosition.slotParent!; + if (cursorPosition === vNode) { + // set cursor position to this node + setCursorPosition(cursor, vNode); + break; + } + } + } + } + } + } else { + if (!container) { + try { + container = findContainerForVNode(vNode)!; + } catch { + console.error('markVNodeDirty: unable to find container for', vNode); + return; + } + } + addCursor(container, vNode, 0); + } +} + +export function addVNodeOperation( + vNode: ElementVNode | TextVNode | VirtualVNode, + operation: VNodeOperation +): void { + vNode.operation = operation; + markVNodeDirty(null, vNode, ChoreBits.OPERATION); +} diff --git a/packages/qwik/src/core/shared/vnode/vnode.ts b/packages/qwik/src/core/shared/vnode/vnode.ts new file mode 100644 index 00000000000..2dc48c06ccd --- /dev/null +++ b/packages/qwik/src/core/shared/vnode/vnode.ts @@ -0,0 +1,30 @@ +import { isDev } from '@qwik.dev/core'; +import type { VNodeFlags } from '../../client/types'; +import { vnode_toString } from '../../client/vnode'; +import type { Props } from '../jsx/jsx-runtime'; +import type { ChoreBits } from './enums/chore-bits.enum'; +import { BackRef } from '../../reactive-primitives/backref'; + +export abstract class VNode extends BackRef { + slotParent: VNode | null = null; + dirty: ChoreBits = 0; + dirtyChildren: VNode[] | null = null; + + constructor( + public flags: VNodeFlags, + public parent: VNode | null, + public previousSibling: VNode | null | undefined, + public nextSibling: VNode | null | undefined, + public props: Props | null + ) { + super(); + } + + // TODO: this creates debug issues + toString(): string { + if (isDev) { + return vnode_toString.call(this); + } + return String(this); + } +} diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index d5ee807c1c2..1c606a71124 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -10,14 +10,13 @@ import { setLocale } from './use-locale'; import type { Container, HostElement } from '../shared/types'; import { vnode_getNode, vnode_isElementVNode, vnode_isVNode, vnode_locate } from '../client/vnode'; import { _getQContainerElement, getDomContainer } from '../client/dom-container'; -import { type ClientContainer, type ContainerElement } from '../client/types'; +import { type ClientContainer } from '../client/types'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; import { type EffectSubscription, type EffectSubscriptionProp } from '../reactive-primitives/types'; import type { Signal } from '../reactive-primitives/signal.public'; import type { ISsrNode } from 'packages/qwik/src/server/qwik-types'; import { getSubscriber } from '../reactive-primitives/subscriber'; import type { SubscriptionData } from '../reactive-primitives/subscription-data'; -import { ChoreType } from '../shared/util-chore-type'; declare const document: QwikDocument; @@ -39,7 +38,7 @@ export interface RenderInvokeContext extends InvokeContext { // The below are just always-defined attributes of InvokeContext. $hostElement$: HostElement; $event$: PossibleEvents; - $waitOn$: Promise[]; + $waitOn$: Promise | null; $container$: Container; } @@ -283,28 +282,8 @@ export const _jsxBranch = (input?: T) => { }; /** @internal */ -export const _waitUntilRendered = (elm: Element) => { - const container = (_getQContainerElement(elm) as ContainerElement | undefined)?.qContainer; - if (!container) { - return Promise.resolve(); - } - - // Multi-cycle idle: loop WAIT_FOR_QUEUE until the flush epoch stays stable - // across an extra microtask, which signals that no new work re-scheduled. - return (async () => { - for (;;) { - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; - - const firstEpoch = container.$flushEpoch$ || 0; - // Give a microtask for any immediate follow-up scheduling to enqueue - await Promise.resolve(); - const secondEpoch = container.$flushEpoch$ || 0; - - // If no epoch change occurred during and after WAIT_FOR_QUEUE, we are idle. - if (firstEpoch === secondEpoch) { - return; - } - // Continue loop if epoch advanced, meaning more work flushed. - } - })(); +export const _waitUntilRendered = (elm: Element): Promise => { + const container = getDomContainer(elm); + const promise = container?.$renderPromise$; + return promise || Promise.resolve(); }; diff --git a/packages/qwik/src/core/use/use-resource.ts b/packages/qwik/src/core/use/use-resource.ts index 65dd2246bb9..66f72f9d7a8 100644 --- a/packages/qwik/src/core/use/use-resource.ts +++ b/packages/qwik/src/core/use/use-resource.ts @@ -1,11 +1,3 @@ -import { Fragment } from '../shared/jsx/jsx-runtime'; -import { _jsxSorted } from '../shared/jsx/jsx-internal'; -import { isServerPlatform } from '../shared/platform/platform'; -import { assertQrl } from '../shared/qrl/qrl-utils'; -import { type QRL } from '../shared/qrl/qrl.public'; -import { invoke, newInvokeContext, untrack, useBindInvokeContext } from './use-core'; -import { Task, TaskFlags, type DescriptorBase, type Tracker } from './use-task'; - import type { Container, HostElement, ValueOrPromise } from '../../server/qwik-types'; import { clearAllEffects } from '../reactive-primitives/cleanup'; import { @@ -18,13 +10,20 @@ import type { Signal } from '../reactive-primitives/signal.public'; import { StoreFlags } from '../reactive-primitives/types'; import { isSignal } from '../reactive-primitives/utils'; import { assertDefined } from '../shared/error/assert'; +import { _jsxSorted } from '../shared/jsx/jsx-internal'; +import { Fragment } from '../shared/jsx/jsx-runtime'; import type { JSXOutput } from '../shared/jsx/types/jsx-node'; +import { isServerPlatform } from '../shared/platform/platform'; +import { assertQrl } from '../shared/qrl/qrl-utils'; +import { type QRL } from '../shared/qrl/qrl.public'; import { ResourceEvent } from '../shared/utils/markers'; import { delay, isPromise, retryOnPromise, safeCall } from '../shared/utils/promises'; import { isObject } from '../shared/utils/types'; +import { invoke, newInvokeContext, untrack, useBindInvokeContext } from './use-core'; import { useSequentialScope } from './use-sequential-scope'; -import { cleanupFn, trackFn } from './utils/tracker'; +import { Task, TaskFlags, type DescriptorBase, type Tracker } from './use-task'; import { cleanupDestroyable } from './utils/destroyable'; +import { cleanupFn, trackFn } from './utils/tracker'; const DEBUG: boolean = false; diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index 70868a2f497..37df264578c 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -1,21 +1,23 @@ import { getDomContainer } from '../client/dom-container'; -import { BackRef, clearAllEffects } from '../reactive-primitives/cleanup'; +import { BackRef } from '../reactive-primitives/backref'; +import { clearAllEffects } from '../reactive-primitives/cleanup'; import { type Signal } from '../reactive-primitives/signal.public'; import { type QRLInternal } from '../shared/qrl/qrl-class'; import { assertQrl } from '../shared/qrl/qrl-utils'; import type { QRL } from '../shared/qrl/qrl.public'; +import { type NoSerialize } from '../shared/serdes/verify'; import { type Container, type HostElement } from '../shared/types'; -import { ChoreType } from '../shared/util-chore-type'; import { TaskEvent } from '../shared/utils/markers'; -import { isPromise, safeCall } from '../shared/utils/promises'; -import { type NoSerialize } from '../shared/serdes/verify'; +import { isPromise, maybeThen, safeCall } from '../shared/utils/promises'; import { type ValueOrPromise } from '../shared/utils/types'; +import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; +import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; import { newInvokeContext } from './use-core'; import { useLexicalScope } from './use-lexical-scope.public'; import type { ResourceReturnInternal } from './use-resource'; import { useSequentialScope } from './use-sequential-scope'; -import { cleanupFn, trackFn } from './utils/tracker'; import { cleanupDestroyable } from './utils/destroyable'; +import { cleanupFn, trackFn } from './utils/tracker'; export const enum TaskFlags { VISIBLE_TASK = 1 << 0, @@ -166,9 +168,10 @@ export const useTaskQrl = (qrl: QRL, opts?: TaskOptions): void => { // deleted and we need to be able to release the task subscriptions. set(task); const container = iCtx.$container$; - const result = runTask(task, container, iCtx.$hostElement$); + const { $waitOn$: waitOn } = iCtx; + const result = maybeThen(waitOn, () => runTask(task, container, iCtx.$hostElement$)); if (isPromise(result)) { - throw result; + iCtx.$waitOn$ = result; } }; @@ -228,7 +231,11 @@ export const isTask = (value: any): value is Task => { */ export const scheduleTask = (_event: Event, element: Element) => { const [task] = useLexicalScope<[Task]>(); - const type = task.$flags$ & TaskFlags.VISIBLE_TASK ? ChoreType.VISIBLE : ChoreType.TASK; const container = getDomContainer(element); - container.$scheduler$(type, task); + task.$flags$ |= TaskFlags.DIRTY; + markVNodeDirty( + container, + task.$el$, + task.$flags$ & TaskFlags.TASK ? ChoreBits.TASKS : ChoreBits.VISIBLE_TASKS + ); }; diff --git a/packages/qwik/src/core/use/use-visible-task.ts b/packages/qwik/src/core/use/use-visible-task.ts index d6d289099fb..2581c41f403 100644 --- a/packages/qwik/src/core/use/use-visible-task.ts +++ b/packages/qwik/src/core/use/use-visible-task.ts @@ -3,7 +3,8 @@ import { isServerPlatform } from '../shared/platform/platform'; import { createQRL, type QRLInternal } from '../shared/qrl/qrl-class'; import { assertQrl } from '../shared/qrl/qrl-utils'; import type { QRL } from '../shared/qrl/qrl.public'; -import { ChoreType } from '../shared/util-chore-type'; +import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; +import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; import { useOn, useOnDocument } from './use-on'; import { useSequentialScope } from './use-sequential-scope'; import { Task, TaskFlags, scheduleTask, type TaskFn } from './use-task'; @@ -42,8 +43,10 @@ export const useVisibleTaskQrl = (qrl: QRL, opts?: OnVisibleTaskOptions) set(task); useRunTask(task, eagerness); if (!isServerPlatform()) { - (qrl as QRLInternal).resolve(iCtx.$element$); - iCtx.$container$.$scheduler$(ChoreType.VISIBLE, task); + if (!qrl.resolved) { + (qrl as QRLInternal).resolve(iCtx.$element$); + } + markVNodeDirty(iCtx.$container$, iCtx.$hostElement$, ChoreBits.VISIBLE_TASKS); } }; diff --git a/packages/qwik/src/server/ssr-container.ts b/packages/qwik/src/server/ssr-container.ts index 2669f02dd64..92348b72539 100644 --- a/packages/qwik/src/server/ssr-container.ts +++ b/packages/qwik/src/server/ssr-container.ts @@ -233,7 +233,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { private promiseAttributes: Array> | null = null; constructor(opts: Required) { - super(() => null, opts.renderOptions.serverData ?? EMPTY_OBJ, opts.locale); + super(opts.renderOptions.serverData ?? EMPTY_OBJ, opts.locale); this.symbolToChunkResolver = (symbol: string): string => { const idx = symbol.lastIndexOf('_'); const chunk = this.resolvedManifest.mapper[idx == -1 ? symbol : symbol.substring(idx + 1)]; diff --git a/packages/qwik/src/testing/element-fixture.ts b/packages/qwik/src/testing/element-fixture.ts index e08ec185233..1ef88cda2e9 100644 --- a/packages/qwik/src/testing/element-fixture.ts +++ b/packages/qwik/src/testing/element-fixture.ts @@ -8,9 +8,7 @@ import { QFuncsPrefix, QInstanceAttr } from '../core/shared/utils/markers'; import { delay } from '../core/shared/utils/promises'; import { invokeApply, newInvokeContextFromTuple } from '../core/use/use-core'; import { createWindow } from './document'; -import { getTestPlatform } from './platform'; import type { MockDocument, MockWindow } from './types'; -import { ChoreType } from '../core/shared/util-chore-type'; /** * Creates a simple DOM structure for testing components. @@ -132,9 +130,8 @@ export async function trigger( const attrName = prefix + fromCamelToKebabCase(eventName); await dispatch(element, attrName, event, scope); } - const waitForQueueChore = container?.$scheduler$(ChoreType.WAIT_FOR_QUEUE); - if (waitForIdle && waitForQueueChore) { - await waitForQueueChore.$returnValue$; + if (waitForIdle && container) { + await container.$renderPromise$; } } @@ -198,10 +195,8 @@ export const dispatch = async ( export async function advanceToNextTimerAndFlush(container: Container) { vi.advanceTimersToNextTimer(); - const waitForQueueChore = container.$scheduler$(ChoreType.WAIT_FOR_QUEUE); - await getTestPlatform().flush(); - if (waitForQueueChore) { - await waitForQueueChore.$returnValue$; + if (container) { + await container.$renderPromise$; } } diff --git a/packages/qwik/src/testing/rendering.unit-util.tsx b/packages/qwik/src/testing/rendering.unit-util.tsx index aa810ab2665..26d6d734394 100644 --- a/packages/qwik/src/testing/rendering.unit-util.tsx +++ b/packages/qwik/src/testing/rendering.unit-util.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { Slot, componentQrl, render, type JSXOutput, type OnRenderFn } from '@qwik.dev/core'; +import { Slot, componentQrl, render, type JSXOutput } from '@qwik.dev/core'; import { _getDomContainer } from '@qwik.dev/core/internal'; import type { _ContainerElement, @@ -15,6 +15,7 @@ import { expect } from 'vitest'; import { vnode_getElementName, vnode_getFirstChild, + vnode_getProp, vnode_getVNodeForChildNode, vnode_insertBefore, vnode_isElementVNode, @@ -22,18 +23,14 @@ import { vnode_locate, vnode_newVirtual, vnode_remove, + vnode_setProp, vnode_toString, - type VNodeJournal, } from '../core/client/vnode'; -import type { VNode, VirtualVNode } from '../core/client/vnode-impl'; import { ERROR_CONTEXT } from '../core/shared/error/error-handling'; -import type { Props } from '../core/shared/jsx/jsx-runtime'; import { getPlatform, setPlatform } from '../core/shared/platform/platform'; import { inlinedQrl } from '../core/shared/qrl/qrl'; import { _dumpState, preprocessState } from '../core/shared/serdes/index'; -import { ChoreType } from '../core/shared/util-chore-type'; import { - ELEMENT_PROPS, OnRenderProp, QContainerSelector, QFuncsPrefix, @@ -43,11 +40,15 @@ import { } from '../core/shared/utils/markers'; import { useContextProvider } from '../core/use/use-context'; import { DEBUG_TYPE, ELEMENT_BACKPATCH_DATA, VirtualType } from '../server/qwik-copy'; -import type { HostElement, QRLInternal } from '../server/qwik-types'; +import type { HostElement } from '../server/qwik-types'; import { Q_FUNCS_PREFIX, renderToString } from '../server/ssr-render'; import { createDocument } from './document'; -import { getTestPlatform } from './platform'; import './vdom-diff.unit-util'; +import type { VNode } from '../core/shared/vnode/vnode'; +import type { VirtualVNode } from '../core/shared/vnode/virtual-vnode'; +import { ChoreBits } from '../core/shared/vnode/enums/chore-bits.enum'; +import { markVNodeDirty } from '../core/shared/vnode/vnode-dirty'; +import type { ElementVNode } from '../core/shared/vnode/element-vnode'; /** @public */ export async function domRender( @@ -59,7 +60,6 @@ export async function domRender( ) { const document = createDocument(); await render(document.body, jsx); - await getTestPlatform().flush(); const getStyles = getStylesFactory(document); const container = _getDomContainer(document.body); if (opts.debug) { @@ -185,7 +185,7 @@ export async function ssrRenderToDom( // Create a fragment const fragment = vnode_newVirtual(); - fragment.setProp(DEBUG_TYPE, VirtualType.Fragment); + vnode_setProp(fragment, DEBUG_TYPE, VirtualType.Fragment); const childrenToMove = []; @@ -197,9 +197,9 @@ export async function ssrRenderToDom( if ( vnode_isElementVNode(child) && ((vnode_getElementName(child) === 'script' && - (child.getAttr('type') === 'qwik/state' || - child.getAttr('type') === ELEMENT_BACKPATCH_DATA || - child.getAttr('id') === 'qwikloader')) || + (vnode_getProp(child, 'type', null) === 'qwik/state' || + vnode_getProp(child, 'type', null) === ELEMENT_BACKPATCH_DATA || + vnode_getProp(child, 'id', null) === 'qwikloader')) || vnode_getElementName(child) === 'q:template') ) { insertBefore = child; @@ -210,10 +210,10 @@ export async function ssrRenderToDom( } // Set the container vnode as a parent of the fragment - vnode_insertBefore(container.$journal$, containerVNode, fragment, insertBefore); + vnode_insertBefore(containerVNode, fragment, insertBefore); // Set the fragment as a parent of the children for (const child of childrenToMove) { - vnode_moveToVirtual(container.$journal$, fragment, child, null); + vnode_moveToVirtual(fragment, child, null); } vNode = fragment; } else { @@ -223,16 +223,11 @@ export async function ssrRenderToDom( return { container, document, vNode, getStyles }; } -function vnode_moveToVirtual( - journal: VNodeJournal, - parent: VirtualVNode, - newChild: VNode, - insertBefore: VNode | null -) { +function vnode_moveToVirtual(parent: VirtualVNode, newChild: VNode, insertBefore: VNode | null) { // ensure that the previous node is unlinked. const newChildCurrentParent = newChild.parent; if (newChildCurrentParent && (newChild.previousSibling || newChild.nextSibling)) { - vnode_remove(journal, newChildCurrentParent, newChild, false); + vnode_remove(newChildCurrentParent as ElementVNode | VirtualVNode, newChild, false); } // link newChild into the previous/next list @@ -323,22 +318,16 @@ function renderStyles(getStyles: () => Record) { }); } -export async function rerenderComponent(element: HTMLElement, flush?: boolean) { +export async function rerenderComponent(element: HTMLElement) { const container = _getDomContainer(element); const vElement = vnode_locate(container.rootVNode, element); const host = getHostVNode(vElement) as HostElement; - const qrl = container.getHostProp>>(host, OnRenderProp)!; - const props = container.getHostProp(host, ELEMENT_PROPS); - container.$scheduler$(ChoreType.COMPONENT, host, qrl, props); - if (flush) { - // Note that this can deadlock - await getTestPlatform().flush(); - } + markVNodeDirty(container, host, ChoreBits.COMPONENT); } function getHostVNode(vElement: _VNode | null) { while (vElement != null) { - if (vElement.getAttr(OnRenderProp) != null) { + if (vnode_getProp(vElement, OnRenderProp, null) != null) { return vElement as _VirtualVNode; } vElement = vElement.parent; diff --git a/packages/qwik/src/testing/util.ts b/packages/qwik/src/testing/util.ts index 42b55ef5ed3..057f92abf0e 100644 --- a/packages/qwik/src/testing/util.ts +++ b/packages/qwik/src/testing/util.ts @@ -1,6 +1,5 @@ import { normalize } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { ChoreType } from '../core/shared/util-chore-type'; import type { Container } from '../core/shared/types'; /** @public */ @@ -104,5 +103,5 @@ export const platformGlobal: { document: Document | undefined } = (__globalThis * @public */ export async function waitForDrain(container: Container) { - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; + await container.$renderPromise$; } diff --git a/packages/qwik/src/testing/vdom-diff.unit-util.ts b/packages/qwik/src/testing/vdom-diff.unit-util.ts index afb1e6bfb62..91863fdeef1 100644 --- a/packages/qwik/src/testing/vdom-diff.unit-util.ts +++ b/packages/qwik/src/testing/vdom-diff.unit-util.ts @@ -14,12 +14,12 @@ import type { } from '@qwik.dev/core/internal'; import { expect } from 'vitest'; import { - vnode_applyJournal, vnode_getAttrKeys, vnode_getElementName, vnode_getFirstChild, vnode_getNode, vnode_getNodeTypeName, + vnode_getProp, vnode_getText, vnode_insertBefore, vnode_isElementVNode, @@ -28,7 +28,8 @@ import { vnode_newText, vnode_newUnMaterializedElement, vnode_newVirtual, - type VNodeJournal, + vnode_setAttr, + vnode_setProp, } from '../core/client/vnode'; import { format } from 'prettier'; @@ -50,7 +51,9 @@ import { HANDLER_PREFIX } from '../core/client/vnode-diff'; import { prettyJSX } from './jsx'; import { isElement, prettyHtml } from './html'; import { QContainerValue } from '../core/shared/types'; -import type { ElementVNode, VirtualVNode, VNode } from '../core/client/vnode-impl'; +import type { VNode } from '../core/shared/vnode/vnode'; +import type { ElementVNode } from '../core/shared/vnode/element-vnode'; +import type { VirtualVNode } from '../core/shared/vnode/virtual-vnode'; expect.extend({ toMatchVDOM( @@ -189,8 +192,8 @@ function diffJsxVNode( // we need this, because Domino lowercases all attributes for `element.attributes` const propLowerCased = prop.toLowerCase(); let receivedValue = - received.getAttr(prop) || - received.getAttr(propLowerCased) || + vnode_getProp(received, prop, null) || + vnode_getProp(received, propLowerCased, null) || receivedElement?.getAttribute(prop) || receivedElement?.getAttribute(propLowerCased); let expectedValue = @@ -393,9 +396,9 @@ function shouldSkip(vNode: _VNode | null) { const tag = vnode_getElementName(vNode); if ( tag === 'script' && - (vNode.getAttr('type') === 'qwik/vnode' || - vNode.getAttr('type') === 'x-qwik/vnode' || - vNode.getAttr('type') === 'qwik/state') + (vnode_getProp(vNode, 'type', null) === 'qwik/vnode' || + vnode_getProp(vNode, 'type', null) === 'x-qwik/vnode' || + vnode_getProp(vNode, 'type', null) === 'qwik/state') ) { return true; } @@ -452,7 +455,6 @@ export function vnode_fromJSX(jsx: JSXOutput) { const container: ClientContainer = _getDomContainer(doc.body); const vBody = vnode_newUnMaterializedElement(doc.body); let vParent: _ElementVNode | _VirtualVNode = vBody; - const journal: VNodeJournal = container.$journal$; walkJSX(jsx, { enter: (jsx) => { const type = jsx.type; @@ -469,7 +471,7 @@ export function vnode_fromJSX(jsx: JSXOutput) { throw new Error('Unknown type:' + type); } - vnode_insertBefore(journal, vParent, child, null); + vnode_insertBefore(vParent, child, null); const props = jsx.varProps; for (const key in props) { if (Object.prototype.hasOwnProperty.call(props, key)) { @@ -477,14 +479,14 @@ export function vnode_fromJSX(jsx: JSXOutput) { continue; } if (key.startsWith(HANDLER_PREFIX) || isHtmlAttributeAnEventName(key)) { - child.setProp(key, props[key]); + vnode_setProp(child, key, props[key]); } else { - child.setAttr(key, String(props[key]), journal); + vnode_setAttr(child, key, String(props[key])); } } } if (jsx.key != null) { - child.setAttr(ELEMENT_KEY, String(jsx.key), journal); + vnode_setAttr(child, ELEMENT_KEY, String(jsx.key)); } vParent = child as ElementVNode | VirtualVNode; }, @@ -493,14 +495,12 @@ export function vnode_fromJSX(jsx: JSXOutput) { }, text: (value) => { vnode_insertBefore( - journal, vParent, vnode_newText(doc.createTextNode(String(value)), String(value)), null ); }, }); - vnode_applyJournal(journal); return { vParent, vNode: vnode_getFirstChild(vParent), document: doc, container }; } function constPropsFromElement(element: Element) { From c366b10c93606f6e33a3570caa0dae92a823fb74 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 16:38:34 +0100 Subject: [PATCH 06/17] more logging --- packages/qwik/src/core/client/vnode.ts | 10 ++++++++++ packages/qwik/src/core/debug.ts | 4 ++-- packages/qwik/src/core/shared/cursor/cursor-walker.ts | 4 ++++ packages/qwik/src/core/shared/vnode/vnode-dirty.ts | 3 +-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index 076bfd119f7..608128614eb 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -173,6 +173,7 @@ 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'; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1801,6 +1802,15 @@ 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_getProp(vnode!, key, null); diff --git a/packages/qwik/src/core/debug.ts b/packages/qwik/src/core/debug.ts index b67ae7b5225..6853f300e31 100644 --- a/packages/qwik/src/core/debug.ts +++ b/packages/qwik/src/core/debug.ts @@ -35,7 +35,7 @@ export function qwikDebugToString(value: any): any { stringifyPath.push(value); if (Array.isArray(value)) { if (vnode_isVNode(value)) { - return '(' + vnode_getProp(value, DEBUG_TYPE, null) + ')'; + return '(' + (vnode_getProp(value, DEBUG_TYPE, null) || 'vnode') + ')'; } else { return value.map(qwikDebugToString); } @@ -52,7 +52,7 @@ export function qwikDebugToString(value: any): any { } else if (isJSXNode(value)) { return jsxToString(value); } else if (vnode_isVNode(value)) { - return '(' + vnode_getProp(value, DEBUG_TYPE, null) + ')'; + return '(' + (vnode_getProp(value, DEBUG_TYPE, null) || 'vnode') + ')'; } } finally { stringifyPath.pop(); diff --git a/packages/qwik/src/core/shared/cursor/cursor-walker.ts b/packages/qwik/src/core/shared/cursor/cursor-walker.ts index 6616b6f88c5..a72ffea6b03 100644 --- a/packages/qwik/src/core/shared/cursor/cursor-walker.ts +++ b/packages/qwik/src/core/shared/cursor/cursor-walker.ts @@ -35,6 +35,8 @@ import { VNodeFlags } from '../../client/types'; import { isPromise } from '../utils/promises'; import type { ValueOrPromise } from '../utils/types'; +const DEBUG = true; + const nextTick = createNextTick(processCursorQueue); let isNextTickScheduled = false; @@ -138,6 +140,7 @@ export function walkCursor(cursor: Cursor, options: WalkOptions): void { let count = 0; while ((currentVNode = getCursorPosition(cursor))) { + DEBUG && console.warn('walkCursor', currentVNode.toString()); if (count++ > 100) { throw new Error('Infinite loop detected in cursor walker'); } @@ -188,6 +191,7 @@ export function walkCursor(cursor: Cursor, options: WalkOptions): void { // Handle blocking promise if (result && isPromise(result)) { + DEBUG && console.warn('walkCursor: blocking promise', currentVNode.toString()); // Store promise on cursor and pause setVNodePromise(cursor, result); // pauseCursor(cursor, currentVNode); diff --git a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts index 0e1919304c5..73881ab4eb3 100644 --- a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts +++ b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts @@ -1,4 +1,3 @@ -import { getDomContainer } from '../../client/dom-container'; import { addCursor, findCursor } from '../cursor/cursor'; import { getCursorPosition, setCursorPosition } from '../cursor/cursor-props'; import { findContainerForVNode } from '../cursor/cursor-walker'; @@ -52,7 +51,7 @@ export function markVNodeDirty(container: Container | null, vNode: VNode, bits: try { container = findContainerForVNode(vNode)!; } catch { - console.error('markVNodeDirty: unable to find container for', vNode); + console.error('markVNodeDirty: unable to find container for', vNode.toString()); return; } } From a1f731483b23f3f8c36826edff690cdecb8c80b4 Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 26 Nov 2025 20:57:23 +0100 Subject: [PATCH 07/17] feat(cursors): pass container to vnode functions --- .../qwik/src/core/client/dom-container.ts | 6 +- packages/qwik/src/core/client/vnode-diff.ts | 35 ++++-- .../qwik/src/core/client/vnode-namespace.ts | 6 +- packages/qwik/src/core/client/vnode.ts | 106 +++++++++++------- .../src/core/shared/cursor/cursor-props.ts | 34 +++++- .../src/core/shared/cursor/cursor-walker.ts | 23 +--- .../qwik/src/core/shared/cursor/cursor.ts | 3 +- .../qwik/src/core/shared/vnode/vnode-dirty.ts | 15 +-- 8 files changed, 132 insertions(+), 96 deletions(-) diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 9483e3f9430..7c938aebb88 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -73,11 +73,7 @@ export function getDomContainer(element: Element): 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 */ diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index 9a3a6f492e8..19fe103e2c0 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -498,6 +498,7 @@ export const vnode_diff = ( if (vProjectedNode == null) { // Nothing to project, so render content of the slot. vnode_insertBefore( + container, vParent as ElementVNode | VirtualVNode, (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() @@ -512,6 +513,7 @@ export const vnode_diff = ( } else { // move from q:template to the target node vnode_insertBefore( + container, vParent as ElementVNode | VirtualVNode, (vNewNode = vProjectedNode), vCurrent && getInsertBefore() @@ -544,7 +546,7 @@ export const vnode_diff = ( continue; } cleanup(container, vNode); - vnode_remove(vParent, vNode, true); + vnode_remove(container, vParent, vNode, true); } vSideBuffer.clear(); vSideBuffer = null; @@ -589,7 +591,7 @@ export const vnode_diff = ( cleanup(container, vChild); vChild = vChild.nextSibling as VNode | null; } - vnode_truncate(vCurrent as ElementVNode | VirtualVNode, vFirstChild); + vnode_truncate(container, vCurrent as ElementVNode | VirtualVNode, vFirstChild); } } @@ -604,7 +606,7 @@ export const vnode_diff = ( cleanup(container, toRemove); // 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(vParent, toRemove, true); + vnode_remove(container, vParent, toRemove, true); } } } @@ -615,7 +617,7 @@ export const vnode_diff = ( cleanup(container, vCurrent); const toRemove = vCurrent; advanceToNextSibling(); - vnode_remove(vParent, toRemove, true); + vnode_remove(container, vParent, toRemove, true); } } @@ -666,7 +668,7 @@ export const vnode_diff = ( vnode_setProp(vNewNode!, HANDLER_PREFIX + ':' + scopedEvent, value); if (scope) { // window and document need attrs so qwik loader can find them - vnode_setAttr(vNewNode!, key, ''); + vnode_setAttr(container, vNewNode!, key, ''); } // register an event for qwik loader (window/document prefixed with '-') registerQwikLoaderEvent(loaderScopedEvent); @@ -744,7 +746,7 @@ export const vnode_diff = ( } } - vnode_insertBefore(vParent as ElementVNode, vNewNode as ElementVNode, vCurrent); + vnode_insertBefore(container, vParent as ElementVNode, vNewNode as ElementVNode, vCurrent); return needsQDispatchEventPatch; } @@ -829,7 +831,7 @@ export const vnode_diff = ( const setAttribute = (vnode: ElementVNode, key: string, value: any) => { const serializedValue = value != null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null; - vnode_setAttr(vnode, key, serializedValue); + vnode_setAttr(container, vnode, key, serializedValue); }; const record = (key: string, value: any) => { @@ -1092,7 +1094,12 @@ export const vnode_diff = ( } } } - vnode_insertBefore(parentForInsert as ElementVNode | VirtualVNode, buffered, vCurrent); + vnode_insertBefore( + container, + parentForInsert as ElementVNode | VirtualVNode, + buffered, + vCurrent + ); vCurrent = buffered; vNewNode = null; return; @@ -1117,6 +1124,7 @@ export const vnode_diff = ( const createNew = () => { vnode_insertBefore( + container, vParent as VirtualVNode, (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() @@ -1259,6 +1267,7 @@ export const vnode_diff = ( clearAllEffects(container, host); } vnode_insertBefore( + container, vParent as VirtualVNode, (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() @@ -1272,6 +1281,7 @@ export const vnode_diff = ( function insertNewInlineComponent() { vnode_insertBefore( + container, vParent as VirtualVNode, (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() @@ -1289,13 +1299,14 @@ export const vnode_diff = ( const type = vnode_getType(vCurrent); if (type === 3 /* Text */) { if (text !== vnode_getText(vCurrent as TextVNode)) { - vnode_setText(vCurrent as TextVNode, text); + vnode_setText(container, vCurrent as TextVNode, text); return; } return; } } vnode_insertBefore( + container, vParent as VirtualVNode, (vNewNode = vnode_newText(container.document.createTextNode(text), text)), vCurrent @@ -1542,7 +1553,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) { projectionChild = projectionChild.nextSibling as VNode | null; } - cleanupStaleUnclaimedProjection(projection); + cleanupStaleUnclaimedProjection(container, projection); } } } @@ -1615,7 +1626,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) { } while (true as boolean); } -function cleanupStaleUnclaimedProjection(projection: VNode) { +function cleanupStaleUnclaimedProjection(container: ClientContainer, projection: VNode) { // we are removing a node where the projection would go after slot render. // This is not needed, so we need to cleanup still unclaimed projection const projectionParent = projection.parent; @@ -1626,7 +1637,7 @@ function cleanupStaleUnclaimedProjection(projection: VNode) { vnode_getElementName(projectionParent as ElementVNode) === QTemplate ) { // if parent is the q:template element then projection is still unclaimed - remove it - vnode_remove(projectionParent as ElementVNode | VirtualVNode, projection, true); + vnode_remove(container, projectionParent as ElementVNode | VirtualVNode, projection, true); } } } diff --git a/packages/qwik/src/core/client/vnode-namespace.ts b/packages/qwik/src/core/client/vnode-namespace.ts index 027ca217be9..d7426fff560 100644 --- a/packages/qwik/src/core/client/vnode-namespace.ts +++ b/packages/qwik/src/core/client/vnode-namespace.ts @@ -23,6 +23,7 @@ import { 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 { Container } from '../shared/types'; export const isForeignObjectElement = (elementName: string) => { return isDev ? elementName.toLowerCase() === 'foreignobject' : elementName === 'foreignObject'; @@ -51,6 +52,7 @@ export const vnode_getElementNamespaceFlags = (element: Element) => { }; export function vnode_getDomChildrenWithCorrectNamespacesToInsert( + container: Container, domParentVNode: ElementVNode, newChild: VNode ): (ElementVNode | TextVNode)[] { @@ -62,11 +64,11 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert( 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(newChild, true); + domChildren = vnode_getDOMChildNodes(container, 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 - const children = vnode_getDOMChildNodes(newChild, true); + const children = vnode_getDOMChildNodes(container, newChild, true); for (let i = 0; i < children.length; i++) { const childVNode = children[i]; diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index 608128614eb..3918f4dd39f 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -126,6 +126,7 @@ import { QContainerValue, VirtualType, VirtualTypeName, + type Container, type QElement, } from '../shared/types'; import { isText } from '../shared/utils/element'; @@ -165,7 +166,6 @@ import { vnode_getElementNamespaceFlags, } from './vnode-namespace'; import { mergeMaps } from '../shared/utils/maps'; -import { _EFFECT_BACK_REF } from '../reactive-primitives/types'; import { EventNameHtmlScope } from '../shared/utils/event-names'; import { VNode } from '../shared/vnode/vnode'; import { ElementVNode } from '../shared/vnode/element-vnode'; @@ -174,6 +174,7 @@ 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'; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -408,10 +409,15 @@ export const vnode_setProp = (vNode: VNode, key: string, value: unknown) => { } }; -export const vnode_setAttr = (vNode: VNode, key: string, value: string | null | boolean) => { +export const vnode_setAttr = ( + container: Container, + vNode: VNode, + key: string, + value: string | null | boolean +) => { if (vnode_isElementVNode(vNode)) { vnode_setProp(vNode, key, value); - addVNodeOperation(vNode, { + addVNodeOperation(container, vNode, { operationType: VNodeOperationType.None, attrs: { [key]: value, @@ -503,16 +509,19 @@ export function vnode_walkVNode( } export function vnode_getDOMChildNodes( + container: Container, root: VNode, isVNode: true, childNodes?: (ElementVNode | TextVNode)[] ): (ElementVNode | TextVNode)[]; export function vnode_getDOMChildNodes( + container: Container, root: VNode, isVNode?: false, childNodes?: (Element | Text)[] ): (Element | Text)[]; export function vnode_getDOMChildNodes( + container: Container, root: VNode, isVNode: boolean = false, childNodes: (ElementVNode | TextVNode | Element | Text)[] = [] @@ -524,7 +533,7 @@ export function vnode_getDOMChildNodes( * we would return a single text node which represents many actual text nodes, or removing a * single text node would remove many text nodes. */ - vnode_ensureTextInflated(root); + vnode_ensureTextInflated(container, root); } childNodes.push(isVNode ? root : vnode_getNode(root)!); return childNodes; @@ -539,12 +548,12 @@ export function vnode_getDOMChildNodes( * we would return a single text node which represents many actual text nodes, or removing a * single text node would remove many text nodes. */ - vnode_ensureTextInflated(vNode); + vnode_ensureTextInflated(container, vNode); childNodes.push(isVNode ? vNode : vnode_getNode(vNode)!); } else { isVNode - ? vnode_getDOMChildNodes(vNode, true, childNodes as (ElementVNode | TextVNode)[]) - : vnode_getDOMChildNodes(vNode, false, childNodes as (Element | Text)[]); + ? vnode_getDOMChildNodes(container, vNode, true, childNodes as (ElementVNode | TextVNode)[]) + : vnode_getDOMChildNodes(container, vNode, false, childNodes as (Element | Text)[]); } vNode = vNode.nextSibling as VNode | null; } @@ -642,13 +651,13 @@ const vnode_getDomSibling = ( return null; }; -const vnode_ensureInflatedIfText = (vNode: VNode): void => { +const vnode_ensureInflatedIfText = (container: Container, vNode: VNode): void => { if (vnode_isTextVNode(vNode)) { - vnode_ensureTextInflated(vNode); + vnode_ensureTextInflated(container, vNode); } }; -const vnode_ensureTextInflated = (vnode: TextVNode) => { +const vnode_ensureTextInflated = (container: Container, vnode: TextVNode) => { const textVNode = ensureTextVNode(vnode); const flags = textVNode.flags; if ((flags & VNodeFlags.Inflated) === 0) { @@ -673,7 +682,7 @@ const vnode_ensureTextInflated = (vnode: TextVNode) => { lastPreviousTextNode = textNode; subCursor.node = textNode; subCursor.flags |= VNodeFlags.Inflated; - addVNodeOperation(subCursor, { + addVNodeOperation(container, subCursor, { operationType: VNodeOperationType.InsertOrMove, parent: parentNode, target: lastPreviousTextNode, @@ -688,12 +697,12 @@ const vnode_ensureTextInflated = (vnode: TextVNode) => { const isLastNode = next ? !vnode_isTextVNode(next) : true; if ((subCursor.flags & VNodeFlags.Inflated) === 0) { if (isLastNode && sharedTextNode) { - addVNodeOperation(subCursor, { + addVNodeOperation(container, subCursor, { operationType: VNodeOperationType.SetText, }); } else { const textNode = doc.createTextNode(subCursor.text!); - addVNodeOperation(subCursor, { + addVNodeOperation(container, subCursor, { operationType: VNodeOperationType.InsertOrMove, parent: parentNode, target: insertBeforeNode, @@ -836,7 +845,12 @@ const indexOfAlphanumeric = (id: string, length: number): number => { return length; }; -export const vnode_createErrorDiv = (document: Document, host: VNode, err: Error) => { +export const vnode_createErrorDiv = ( + container: Container, + document: Document, + host: VNode, + err: Error +) => { const errorDiv = document.createElement('errored-host'); if (err && err instanceof Error) { (errorDiv as any).props = { error: err }; @@ -845,8 +859,8 @@ export const vnode_createErrorDiv = (document: Document, host: VNode, err: Error const vErrorDiv = vnode_newElement(errorDiv, 'errored-host'); - vnode_getDOMChildNodes(host, true).forEach((child) => { - vnode_insertBefore(vErrorDiv, child, null); + vnode_getDOMChildNodes(container, host, true).forEach((child) => { + vnode_insertBefore(container, vErrorDiv, child, null); }); return vErrorDiv; }; @@ -1042,6 +1056,7 @@ export const vnode_applyJournal = (journal: VNodeJournal) => { ////////////////////////////////////////////////////////////////////////////////////////////////////// export const vnode_insertBefore = ( + container: Container, parent: ElementVNode | VirtualVNode, newChild: VNode, insertBefore: VNode | null @@ -1087,7 +1102,11 @@ export const vnode_insertBefore = ( const parentNode = domParentVNode && domParentVNode.node; let domChildren: (ElementVNode | TextVNode)[] | null = null; if (domParentVNode) { - domChildren = vnode_getDomChildrenWithCorrectNamespacesToInsert(domParentVNode, newChild); + domChildren = vnode_getDomChildrenWithCorrectNamespacesToInsert( + container, + domParentVNode, + newChild + ); } /** @@ -1131,7 +1150,7 @@ export const vnode_insertBefore = ( newChildCurrentParent && (newChild.previousSibling || newChild.nextSibling || newChildCurrentParent !== parent) ) { - vnode_remove(newChildCurrentParent, newChild, false); + vnode_remove(container, newChildCurrentParent, newChild, false); } const parentIsDeleted = parent.flags & VNodeFlags.Deleted; @@ -1153,7 +1172,17 @@ export const vnode_insertBefore = ( } else { adjustedInsertBefore = insertBefore; } - adjustedInsertBefore && vnode_ensureInflatedIfText(adjustedInsertBefore); + adjustedInsertBefore && vnode_ensureInflatedIfText(container, adjustedInsertBefore); + + if (domChildren && domChildren.length) { + for (const child of domChildren) { + addVNodeOperation(container, child, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode!, + target: vnode_getNode(adjustedInsertBefore), + }); + } + } } // link newChild into the previous/next list @@ -1175,17 +1204,6 @@ export const vnode_insertBefore = ( if (parentIsDeleted) { // if the parent is deleted, then the new child is also deleted newChild.flags |= VNodeFlags.Deleted; - } else { - // Here we know the insertBefore node - if (domChildren && domChildren.length) { - for (const child of domChildren) { - addVNodeOperation(child, { - operationType: VNodeOperationType.InsertOrMove, - parent: parentNode!, - target: vnode_getNode(adjustedInsertBefore), - }); - } - } } }; @@ -1205,13 +1223,14 @@ export const vnode_getDomParentVNode = ( }; export const vnode_remove = ( + container: Container, vParent: ElementVNode | VirtualVNode, vToRemove: VNode, removeDOM: boolean ) => { assertEqual(vParent, vToRemove.parent, 'Parent mismatch.'); if (vnode_isTextVNode(vToRemove)) { - vnode_ensureTextInflated(vToRemove); + vnode_ensureTextInflated(container, vToRemove); } if (removeDOM) { @@ -1221,11 +1240,11 @@ export const vnode_remove = ( // ignore children, as they are inserted via innerHTML return; } - const children = vnode_getDOMChildNodes(vToRemove, true); + const children = vnode_getDOMChildNodes(container, vToRemove, true); //&& //journal.push(VNodeJournalOpCode.Remove, domParent, ...children); if (domParent && children.length) { for (const child of children) { - addVNodeOperation(child, { + addVNodeOperation(container, child, { operationType: VNodeOperationType.Delete, }); } @@ -1249,6 +1268,7 @@ export const vnode_remove = ( }; export const vnode_queryDomNodes = ( + container: Container, vNode: VNode, selector: string, cb: (element: Element) => void @@ -1263,25 +1283,29 @@ export const vnode_queryDomNodes = ( } else { let child = vnode_getFirstChild(vNode); while (child) { - vnode_queryDomNodes(child, selector, cb); + vnode_queryDomNodes(container, child, selector, cb); child = child.nextSibling as VNode | null; } } }; -export const vnode_truncate = (vParent: ElementVNode | VirtualVNode, vDelete: VNode) => { +export const vnode_truncate = ( + container: Container, + vParent: ElementVNode | VirtualVNode, + vDelete: VNode +) => { assertDefined(vDelete, 'Missing vDelete.'); const parent = vnode_getDomParent(vParent); if (parent) { if (vnode_isElementVNode(vParent)) { - addVNodeOperation(vParent, { + addVNodeOperation(container, vParent, { operationType: VNodeOperationType.RemoveAllChildren, }); } else { - const children = vnode_getDOMChildNodes(vParent, true); + const children = vnode_getDOMChildNodes(container, vParent, true); if (children.length) { for (const child of children) { - addVNodeOperation(child, { + addVNodeOperation(container, child, { operationType: VNodeOperationType.Delete, }); } @@ -1319,10 +1343,10 @@ export const vnode_getText = (textVNode: TextVNode): string => { return text; }; -export const vnode_setText = (textVNode: TextVNode, text: string) => { - vnode_ensureTextInflated(textVNode); +export const vnode_setText = (container: Container, textVNode: TextVNode, text: string) => { + vnode_ensureTextInflated(container, textVNode); textVNode.text = text; - addVNodeOperation(textVNode, { + addVNodeOperation(container, textVNode, { operationType: VNodeOperationType.SetText, }); }; diff --git a/packages/qwik/src/core/shared/cursor/cursor-props.ts b/packages/qwik/src/core/shared/cursor/cursor-props.ts index 15b5409e4d7..8ffb1b6f0cb 100644 --- a/packages/qwik/src/core/shared/cursor/cursor-props.ts +++ b/packages/qwik/src/core/shared/cursor/cursor-props.ts @@ -3,16 +3,18 @@ import type { VNode } from '../vnode/vnode'; import type { Props } from '../jsx/jsx-runtime'; import { isCursor } from './cursor'; import { removeCursorFromQueue } from './cursor-queue'; +import type { Container } from '../types'; /** * Keys used to store cursor-related data in vNode props. These are internal properties that should * not conflict with user props. */ -const CURSOR_PRIORITY_KEY = 'q:priority'; -const CURSOR_POSITION_KEY = 'q:position'; -const CURSOR_CHILD_KEY = 'q:childIndex'; -const VNODE_PROMISE_KEY = 'q:promise'; -const CURSOR_EXTRA_PROMISES_KEY = 'q:extraPromises'; +const CURSOR_PRIORITY_KEY = ':priority'; +const CURSOR_POSITION_KEY = ':position'; +const CURSOR_CHILD_KEY = ':childIndex'; +const CURSOR_CONTAINER_KEY = ':cursorContainer'; +const VNODE_PROMISE_KEY = ':promise'; +const CURSOR_EXTRA_PROMISES_KEY = ':extraPromises'; /** * Gets the priority of a cursor vNode. @@ -134,3 +136,25 @@ export function setExtraPromises(vNode: VNode, extraPromises: Promise[] | const props = (vNode.props ||= {}); props[CURSOR_EXTRA_PROMISES_KEY] = extraPromises; } + +/** + * Sets the cursor container on a vNode. + * + * @param vNode - The vNode + * @param container - The container to set + */ +export function setCursorContainer(vNode: VNode, container: Container): void { + const props = (vNode.props ||= {}); + props[CURSOR_CONTAINER_KEY] = container; +} + +/** + * Gets the cursor container from a vNode. + * + * @param vNode - The vNode + * @returns The container, or null if none or not a cursor + */ +export function getCursorContainer(vNode: VNode): Container | null { + const props = vNode.props; + return (props?.[CURSOR_CONTAINER_KEY] as Container | null) ?? null; +} diff --git a/packages/qwik/src/core/shared/cursor/cursor-walker.ts b/packages/qwik/src/core/shared/cursor/cursor-walker.ts index a72ffea6b03..ee5164e492b 100644 --- a/packages/qwik/src/core/shared/cursor/cursor-walker.ts +++ b/packages/qwik/src/core/shared/cursor/cursor-walker.ts @@ -23,6 +23,7 @@ import { setVNodePromise, setNextChildIndex, getNextChildIndex, + getCursorContainer, } from './cursor-props'; import { ChoreBits } from '../vnode/enums/chore-bits.enum'; import { getHighestPriorityCursor, removeCursorFromQueue } from './cursor-queue'; @@ -34,6 +35,7 @@ import type { Container } from '../types'; import { VNodeFlags } from '../../client/types'; import { isPromise } from '../utils/promises'; import type { ValueOrPromise } from '../utils/types'; +import { assertDefined } from '../error/assert'; const DEBUG = true; @@ -75,22 +77,6 @@ export function processCursorQueue( } } -export function findContainerForVNode(vNode: VNode): Container { - let element: Element; - if (vnode_isElementVNode(vNode)) { - element = vNode.node; - } else { - let parent = vNode.parent; - while (parent) { - if (vnode_isElementVNode(parent)) { - element = parent.node; - break; - } - parent = parent.parent; - } - } - return getDomContainer(element!); -} let globalCount = 0; /** @@ -134,7 +120,8 @@ export function walkCursor(cursor: Cursor, options: WalkOptions): void { throw new Error('Infinite loop detected in cursor walker'); } - const container = findContainerForVNode(cursor); + const container = getCursorContainer(cursor); + assertDefined(container, 'Cursor container not found'); // Get starting position (resume from last position or start at root) let currentVNode: VNode | null = null; @@ -237,7 +224,7 @@ function getNextVNode(vNode: VNode): VNode | null { while (count-- > 0) { const nextVNode = dirtyChildren[index]; if (nextVNode.dirty & ChoreBits.DIRTY_MASK) { - setNextChildIndex(parent, index + 1); + setNextChildIndex(parent, (index + 1) % len); return nextVNode; } index++; diff --git a/packages/qwik/src/core/shared/cursor/cursor.ts b/packages/qwik/src/core/shared/cursor/cursor.ts index 2bd29965520..966d71ab511 100644 --- a/packages/qwik/src/core/shared/cursor/cursor.ts +++ b/packages/qwik/src/core/shared/cursor/cursor.ts @@ -1,7 +1,7 @@ import { VNodeFlags } from '../../client/types'; import type { Container } from '../types'; import type { VNode } from '../vnode/vnode'; -import { setCursorPriority, setCursorPosition } from './cursor-props'; +import { setCursorPriority, setCursorPosition, setCursorContainer } from './cursor-props'; import { addCursorToQueue } from './cursor-queue'; import { triggerCursors } from './cursor-walker'; @@ -24,6 +24,7 @@ export type Cursor = VNode; export function addCursor(container: Container, root: VNode, priority: number): Cursor { setCursorPriority(root, priority); setCursorPosition(root, root); + setCursorContainer(root, container); const cursor = root as Cursor; cursor.flags |= VNodeFlags.Cursor; diff --git a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts index 73881ab4eb3..98ff2b6ae94 100644 --- a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts +++ b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts @@ -1,6 +1,5 @@ import { addCursor, findCursor } from '../cursor/cursor'; import { getCursorPosition, setCursorPosition } from '../cursor/cursor-props'; -import { findContainerForVNode } from '../cursor/cursor-walker'; import type { Container } from '../types'; import type { ElementVNode } from './element-vnode'; import { ChoreBits } from './enums/chore-bits.enum'; @@ -9,8 +8,7 @@ import type { VNodeOperation } from './types/dom-vnode-operation'; import type { VirtualVNode } from './virtual-vnode'; import type { VNode } from './vnode'; -export function propagateDirty(vNode: VNode, bits: ChoreBits): void {} -export function markVNodeDirty(container: Container | null, vNode: VNode, bits: ChoreBits): void { +export function markVNodeDirty(container: Container, vNode: VNode, bits: ChoreBits): void { const prevDirty = vNode.dirty; vNode.dirty |= bits; const isRealDirty = bits & ChoreBits.DIRTY_MASK; @@ -47,22 +45,15 @@ export function markVNodeDirty(container: Container | null, vNode: VNode, bits: } } } else { - if (!container) { - try { - container = findContainerForVNode(vNode)!; - } catch { - console.error('markVNodeDirty: unable to find container for', vNode.toString()); - return; - } - } addCursor(container, vNode, 0); } } export function addVNodeOperation( + container: Container, vNode: ElementVNode | TextVNode | VirtualVNode, operation: VNodeOperation ): void { vNode.operation = operation; - markVNodeDirty(null, vNode, ChoreBits.OPERATION); + markVNodeDirty(container, vNode, ChoreBits.OPERATION); } From 6b0a6321124986cea8d686cc8265096fa15a2dc0 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Fri, 28 Nov 2025 15:24:03 +0100 Subject: [PATCH 08/17] chore(types): add event handler type tests --- .../shared/jsx/types/jsx-polymorphic.unit.tsx | 70 +++++++++++++++++ .../core/shared/jsx/types/jsx-types.unit.tsx | 76 +++---------------- 2 files changed, 82 insertions(+), 64 deletions(-) create mode 100644 packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx 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>(); + <>