From 0d70d3d0490a751a439eb2d4b12de6ed6a2f382b Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Wed, 26 Nov 2025 15:47:11 +0100 Subject: [PATCH 01/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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>(); + <>
-
World
+ {/* TODO: q:container is const and div is reused, is it ok? */} +
World
World
@@ -2885,7 +2886,7 @@ describe.each([
-
World
+
World
World
From 4b5eca2a2bace74fe30bf42805fa334853eddb08 Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 12 Dec 2025 23:10:10 +0100 Subject: [PATCH 29/38] feat(cursors): fix catching error for runQrl --- packages/qwik/src/core/client/run-qrl.ts | 18 +++++- packages/qwik/src/core/client/vnode.ts | 4 +- .../reactive-primitives/impl/store.unit.tsx | 16 +++--- .../src/core/shared/serdes/inflate.unit.ts | 56 ++++++++++++++----- .../qwik/src/core/shared/utils/promises.ts | 15 +++++ .../qwik/src/core/tests/container.spec.tsx | 40 +++++++------ .../qwik/src/core/tests/render-api.spec.tsx | 2 +- .../qwik/src/server/ssr-container.spec.ts | 6 +- packages/qwik/src/testing/element-fixture.ts | 1 + 9 files changed, 111 insertions(+), 47 deletions(-) diff --git a/packages/qwik/src/core/client/run-qrl.ts b/packages/qwik/src/core/client/run-qrl.ts index 90d370f8a3a..3e4985caac0 100644 --- a/packages/qwik/src/core/client/run-qrl.ts +++ b/packages/qwik/src/core/client/run-qrl.ts @@ -1,8 +1,10 @@ import type { QRLInternal } from '../shared/qrl/qrl-class'; -import { retryOnPromise } from '../shared/utils/promises'; +import { catchError, retryOnPromise, safeCall } from '../shared/utils/promises'; import type { ValueOrPromise } from '../shared/utils/types'; +import type { ElementVNode } from '../shared/vnode/element-vnode'; import { getInvokeContext } from '../use/use-core'; import { useLexicalScope } from '../use/use-lexical-scope.public'; +import { getDomContainer } from './dom-container'; import { VNodeFlags } from './types'; /** @@ -18,7 +20,19 @@ export const _run = (...args: unknown[]): ValueOrPromise => { if (hostElement) { return retryOnPromise(() => { if (!(hostElement.flags & VNodeFlags.Deleted)) { - return runQrl(...args); + return catchError( + () => runQrl(...args), + (err) => { + const container = (context.$container$ ||= getDomContainer( + (hostElement as ElementVNode).node + )); + if (container) { + container.handleError(err, hostElement); + } else { + throw err; + } + } + ); } }); } diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index e8a904fc676..b88e18d1e06 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -228,7 +228,7 @@ export const vnode_newSharedText = ( textContent: string ): TextVNode => { sharedTextNode && - assertEqual(fastNodeType(sharedTextNode), 3 /* TEXT_NODE */, 'Expecting element node.'); + assertEqual(fastNodeType(sharedTextNode), 3 /* TEXT_NODE */, 'Expecting text node.'); const vnode: TextVNode = new TextVNode( VNodeFlags.Text | (-1 << VNodeFlagsIndex.shift), // Flag null, // Parent @@ -251,7 +251,7 @@ export const vnode_newText = (textNode: Text, textContent: string | undefined): textNode, // TextNode textContent // Text Content ); - assertEqual(fastNodeType(textNode), 3 /* TEXT_NODE */, 'Expecting element node.'); + assertEqual(fastNodeType(textNode), 3 /* TEXT_NODE */, 'Expecting text node.'); assertFalse(vnode_isElementVNode(vnode), 'Incorrect format of TextVNode.'); assertTrue(vnode_isTextVNode(vnode), 'Incorrect format of TextVNode.'); assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of TextVNode.'); diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx b/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx index e4b1f2a292b..a071efb28c2 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx +++ b/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx @@ -1,28 +1,29 @@ import { getDomContainer, implicit$FirstArg, type QRL } from '@qwik.dev/core'; -import { createDocument, getTestPlatform } from '@qwik.dev/core/testing'; +import { createDocument } from '@qwik.dev/core/testing'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { Container, HostElement } from '../../shared/types'; import { getOrCreateStore, isStore } from './store'; import { EffectProperty, StoreFlags } from '../types'; import { invoke } from '../../use/use-core'; import { newInvokeContext } from '../../use/use-core'; -import { ChoreType } from '../../shared/util-chore-type'; import type { QRLInternal } from '../../shared/qrl/qrl-class'; import { Task } from '../../use/use-task'; import { getSubscriber } from '../subscriber'; +import { vnode_newVirtual, vnode_setProp } from '../../client/vnode'; +import { ELEMENT_SEQ } from 'packages/qwik/src/server/qwik-copy'; describe('v2/store', () => { const log: any[] = []; let container: Container = null!; + let document: Document = null!; beforeEach(() => { log.length = 0; - const document = createDocument({ html: '' }); + document = createDocument({ html: '' }); container = getDomContainer(document.body); }); afterEach(async () => { - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; - await getTestPlatform().flush(); + await container.$renderPromise$; container = null!; }); @@ -61,13 +62,14 @@ describe('v2/store', () => { } async function flushSignals() { - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; + await container.$renderPromise$; } function effectQrl(fnQrl: QRL<() => void>) { const qrl = fnQrl as QRLInternal<() => void>; - const element: HostElement = null!; + const element: HostElement = vnode_newVirtual(); const task = new Task(0, 0, element, fnQrl as QRLInternal, undefined, null); + vnode_setProp(element, ELEMENT_SEQ, [task]); if (!qrl.resolved) { throw qrl.resolve(); } else { diff --git a/packages/qwik/src/core/shared/serdes/inflate.unit.ts b/packages/qwik/src/core/shared/serdes/inflate.unit.ts index c8719b4263c..38e5c118bf2 100644 --- a/packages/qwik/src/core/shared/serdes/inflate.unit.ts +++ b/packages/qwik/src/core/shared/serdes/inflate.unit.ts @@ -2,8 +2,11 @@ import { describe, expect, it } from 'vitest'; import { NEEDS_COMPUTATION, EffectProperty } from '../../reactive-primitives/types'; import { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl'; import { VNodeFlags } from '../../client/types'; -import { ElementVNode, VirtualVNode } from '../../client/vnode-impl'; import { inflateWrappedSignalValue } from './inflate'; +import { vnode_newElement, vnode_newText, vnode_setProp } from '../../client/vnode'; +import { ElementVNode } from '../vnode/element-vnode'; +import { TextVNode } from '../vnode/text-vnode'; +import { VirtualVNode } from '../vnode/virtual-vnode'; describe('inflateWrappedSignalValue', () => { it('should read value from class attribute', () => { @@ -20,16 +23,18 @@ describe('inflateWrappedSignalValue', () => { } as any; const vnode = new ElementVNode( + null, VNodeFlags.Element, - null, // parent - null, // previousSibling - null, // nextSibling - null, // firstChild - null, // lastChild + null, + null, + null, + null, + null, + null, element, 'div' ); - vnode.setAttr('class', 'active', null); + vnode_setProp(vnode, 'class', 'active'); signal.$hostElement$ = vnode; @@ -52,16 +57,18 @@ describe('inflateWrappedSignalValue', () => { } as any; const vnode = new ElementVNode( + null, VNodeFlags.Element, null, null, null, null, null, + null, element, 'div' ); - vnode.setAttr('data-state', 'initial', null); + vnode_setProp(vnode, 'data-state', 'initial'); signal.$hostElement$ = vnode; signal.$effects$ = new Set([[vnode, 'data-state', null, null]] as any); @@ -80,12 +87,14 @@ describe('inflateWrappedSignalValue', () => { } as any; const vnode = new ElementVNode( + null, VNodeFlags.Element, null, null, null, null, null, + null, element, 'div' ); @@ -107,12 +116,14 @@ describe('inflateWrappedSignalValue', () => { } as any; const vnode = new ElementVNode( + null, VNodeFlags.Element, null, null, null, null, null, + null, element, 'div' ); @@ -137,17 +148,19 @@ describe('inflateWrappedSignalValue', () => { } as unknown as HTMLElement; const vnode = new ElementVNode( + null, VNodeFlags.Element, null, null, null, null, null, + null, element, 'div' ); - vnode.setAttr('first-attr', 'first-value', null); - vnode.setAttr('second-attr', 'second-value', null); + vnode_setProp(vnode, 'first-attr', 'first-value'); + vnode_setProp(vnode, 'second-attr', 'second-value'); signal.$hostElement$ = vnode; signal.$effects$ = new Set([ @@ -165,9 +178,18 @@ describe('inflateWrappedSignalValue', () => { signal.$untrackedValue$ = NEEDS_COMPUTATION; const textNode = { nodeValue: 'hello' } as any; - const textVNode = { flags: VNodeFlags.Text, textNode } as any; + const textVNode = new TextVNode(VNodeFlags.Text, null, null, null, null, textNode, 'hello'); - const vnode = new VirtualVNode(VNodeFlags.Virtual, null, null, null, textVNode, textVNode); + const vnode = new VirtualVNode( + null, + VNodeFlags.Virtual, + null, + null, + null, + null, + textVNode, + textVNode + ); signal.$hostElement$ = vnode; // No attribute effects, only VNODE effect @@ -191,16 +213,20 @@ describe('inflateWrappedSignalValue', () => { } as any; const vnode = new ElementVNode( + null, VNodeFlags.Element, null, null, null, - textVNode, - textVNode, + null, + null, + null, element, 'div' ); - vnode.setAttr('data-value', 'attr-value', null); + vnode_setProp(vnode, 'data-value', 'attr-value'); + vnode.firstChild = textVNode; + vnode.lastChild = textVNode; signal.$hostElement$ = vnode; signal.$effects$ = new Set([[vnode, 'data-value', null, null]] as any); diff --git a/packages/qwik/src/core/shared/utils/promises.ts b/packages/qwik/src/core/shared/utils/promises.ts index e1d632f1e0d..25183a6e636 100644 --- a/packages/qwik/src/core/shared/utils/promises.ts +++ b/packages/qwik/src/core/shared/utils/promises.ts @@ -26,6 +26,21 @@ export const safeCall = ( } }; +export const catchError = ( + call: () => ValueOrPromise, + rejectFn: { f(reason: any): ValueOrPromise }['f'] +): ValueOrPromise => { + try { + const result = call(); + if (isPromise(result)) { + return result.catch(rejectFn); + } + return result; + } catch (e) { + return rejectFn(e); + } +}; + export const maybeThen = ( valueOrPromise: ValueOrPromise, thenFn: (arg: Awaited) => ValueOrPromise diff --git a/packages/qwik/src/core/tests/container.spec.tsx b/packages/qwik/src/core/tests/container.spec.tsx index 05561a87650..13787517252 100644 --- a/packages/qwik/src/core/tests/container.spec.tsx +++ b/packages/qwik/src/core/tests/container.spec.tsx @@ -6,7 +6,12 @@ import { SsrNode } from '../../server/ssr-node'; import { createDocument } from '../../testing/document'; import { getDomContainer } from '../client/dom-container'; import type { ClientContainer } from '../client/types'; -import { vnode_getFirstChild, vnode_getProp, vnode_getText } from '../client/vnode'; +import { + vnode_ensureElementInflated, + vnode_getFirstChild, + vnode_getProp, + vnode_getText, +} from '../client/vnode'; import { createComputed$, createSignal } from '../reactive-primitives/signal.public'; import { SignalFlags } from '../reactive-primitives/types'; import { SERIALIZABLE_STATE, component$ } from '../shared/component.public'; @@ -114,25 +119,26 @@ describe('serializer v2', () => { // doesn't use the vnode so not serialized it('should retrieve element', async () => { const clientContainer = await withContainer((ssr) => { - ssr.openElement('div', ['id', 'parent']); + ssr.openElement('div', null, ['id', 'parent']); ssr.textNode('Hello'); - ssr.openElement('span', ['id', 'myId']); + ssr.openElement('span', null, ['id', 'myId']); const node = ssr.getOrCreateLastNode(); ssr.addRoot({ someProp: node }); ssr.textNode('Hello'); - ssr.openElement('b', ['id', 'child']); + ssr.openElement('b', null, ['id', 'child']); ssr.closeElement(); ssr.closeElement(); ssr.closeElement(); }); const vnodeSpan: VNode = await clientContainer.$getObjectById$(0).someProp; + vnode_ensureElementInflated(vnodeSpan); expect(vnode_getProp(vnodeSpan, 'id', null)).toBe('myId'); }); it('should retrieve text node', async () => { const clientContainer = await withContainer((ssr) => { - ssr.openElement('div', ['id', 'parent']); + ssr.openElement('div', null, ['id', 'parent']); ssr.textNode('Hello'); - ssr.openElement('span', ['id', 'div']); + ssr.openElement('span', null, ['id', 'myId']); ssr.textNode('Greetings'); ssr.textNode(' '); ssr.textNode('World'); @@ -140,7 +146,7 @@ describe('serializer v2', () => { expect(node.id).toBe('2C'); ssr.textNode('!'); ssr.addRoot({ someProp: node }); - ssr.openElement('b', ['id', 'child']); + ssr.openElement('b', null, ['id', 'child']); ssr.closeElement(); ssr.closeElement(); ssr.closeElement(); @@ -150,9 +156,9 @@ describe('serializer v2', () => { }); it('should retrieve text node in Fragments', async () => { const clientContainer = await withContainer((ssr) => { - ssr.openElement('div', ['id', 'parent']); + ssr.openElement('div', null, ['id', 'parent']); ssr.textNode('Hello'); - ssr.openElement('span', ['id', 'div']); // 2 + ssr.openElement('span', null, ['id', 'div']); // 2 ssr.textNode('Greetings'); // 2A ssr.textNode(' '); // 2B ssr.openFragment([]); // 2C @@ -161,7 +167,7 @@ describe('serializer v2', () => { expect(node.id).toBe('2CA'); ssr.textNode('!'); ssr.addRoot({ someProp: node }); - ssr.openElement('b', ['id', 'child']); + ssr.openElement('b', null, ['id', 'child']); ssr.closeElement(); ssr.closeFragment(); ssr.closeElement(); @@ -520,11 +526,11 @@ describe('serializer v2', () => { await expect(() => withContainer( (ssr) => { - ssr.openElement('body', [], null, filePath); - ssr.openElement('p', [], null, filePath); + ssr.openElement('body', null, [], null, filePath); + ssr.openElement('p', null, [], null, filePath); ssr.openFragment([]); - ssr.openElement('b', [], null, filePath); - ssr.openElement('div', [], null, filePath); + ssr.openElement('b', null, [], null, filePath); + ssr.openElement('div', null, [], null, filePath); }, { containerTag: 'html' } ) @@ -547,9 +553,9 @@ describe('serializer v2', () => { const filePath = '/some/path/test-file.tsx'; await expect(() => withContainer((ssr) => { - ssr.openElement('img', [], null, filePath); + ssr.openElement('img', null, [], null, filePath); ssr.openFragment([]); - ssr.openElement('div', [], null, filePath); + ssr.openElement('div', null, [], null, filePath); }) ).rejects.toThrowError( [ @@ -600,10 +606,10 @@ async function toHTML(jsx: JSXOutput): Promise { } ssrContainer.openElement( jsx.type, + jsx.key, toSsrAttrs(jsx.varProps, { serializationCtx: ssrContainer.serializationCtx, styleScopedId: null, - key: jsx.key, }), toSsrAttrs(jsx.constProps, { serializationCtx: ssrContainer.serializationCtx, diff --git a/packages/qwik/src/core/tests/render-api.spec.tsx b/packages/qwik/src/core/tests/render-api.spec.tsx index bb1b3169aaf..77d8aadd9c9 100644 --- a/packages/qwik/src/core/tests/render-api.spec.tsx +++ b/packages/qwik/src/core/tests/render-api.spec.tsx @@ -1045,7 +1045,7 @@ describe('render api', () => { streaming, }); // This can change when the size of the output changes - expect(stream.write).toHaveBeenCalledTimes(5); + expect(stream.write).toHaveBeenCalledTimes(4); }); }); }); diff --git a/packages/qwik/src/server/ssr-container.spec.ts b/packages/qwik/src/server/ssr-container.spec.ts index 8ce1d71f2c5..24f57b49226 100644 --- a/packages/qwik/src/server/ssr-container.spec.ts +++ b/packages/qwik/src/server/ssr-container.spec.ts @@ -31,15 +31,15 @@ describe('SSR Container', () => { container.openContainer(); // Add a large content to exceed 30KB while opening the next element const largeContent = 'x'.repeat(30 * 1024); - container.openElement('div', null); + container.openElement('div', null, null); container.textNode(largeContent); await container.closeElement(); // Add a style element with QStyle attribute - container.openElement('style', [QStyle, 'my-style-id']); + container.openElement('style', null, [QStyle, 'my-style-id']); container.write('.my-class { color: red; }'); await container.closeElement(); // Add another regular elementm - container.openElement('div', null); + container.openElement('div', null, null); await container.closeElement(); await container.closeContainer(); diff --git a/packages/qwik/src/testing/element-fixture.ts b/packages/qwik/src/testing/element-fixture.ts index 1ef88cda2e9..d1735da1c49 100644 --- a/packages/qwik/src/testing/element-fixture.ts +++ b/packages/qwik/src/testing/element-fixture.ts @@ -204,5 +204,6 @@ export function cleanupAttrs(innerHTML: string | undefined): any { return innerHTML ?.replaceAll(/ q:key="[^"]+"/g, '') .replaceAll(/ :=""/g, '') + .replaceAll(/ :="[^"]+"/g, '') .replaceAll(/ on:\w+="[^"]+"/g, ''); } From 780af824e9cbe209a03d455d4e0be4731ca574c8 Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 13 Dec 2025 09:53:11 +0100 Subject: [PATCH 30/38] feat(cursors): fix hoisting styles --- .../qwik/src/core/client/dom-container.ts | 2 +- .../reactive-primitives/impl/signal.unit.tsx | 31 ++++++++++--------- .../qwik/src/core/tests/use-task.spec.tsx | 6 +++- .../src/core/tests/use-visible-task.spec.tsx | 9 +++++- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 4bd33a56c0d..83548b84936 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -110,7 +110,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer { } 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)!; @@ -132,6 +131,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer { preprocessState(this.$rawStateData$, this); this.$stateData$ = wrapDeserializerProxy(this, this.$rawStateData$) as unknown[]; } + this.$hoistStyles$(); if (!qTest && element.isConnected) { element.dispatchEvent(new CustomEvent('qresume', { bubbles: true })); } diff --git a/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx b/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx index f8e4fd0c6af..231b97e2ee7 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx +++ b/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx @@ -27,6 +27,8 @@ import { type Signal, } from '../signal.public'; import { getSubscriber } from '../subscriber'; +import { vnode_newVirtual, vnode_setProp } from '../../client/vnode'; +import { ELEMENT_SEQ } from '../../shared/utils/markers'; class Foo { constructor(public val: number = 0) {} @@ -118,8 +120,7 @@ describe('signal', () => { afterEach(async () => { delayMap.clear(); - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; - await getTestPlatform().flush(); + await container.$renderPromise$; container = null!; }); @@ -189,9 +190,9 @@ describe('signal', () => { await withContainer(async () => { const a = createSignal(true) as InternalSignal; const b = createSignal(true) as InternalSignal; - await retryOnPromise(async () => { - let signal!: InternalReadonlySignal; - effect$(() => { + let signal!: InternalReadonlySignal; + await retryOnPromise(() => + effect$(async () => { signal = signal || createComputedQrl( @@ -201,14 +202,13 @@ describe('signal', () => { }) ) ); - log.push(signal.value); // causes subscription - }); - expect(log).toEqual([true]); - a.value = !a.untrackedValue; - await flushSignals(); - b.value = !b.untrackedValue; - }); - await flushSignals(); + const signalValue = await retryOnPromise(() => signal.value); + log.push(signalValue); // causes subscription + }) + ); + expect(log).toEqual([true]); + a.value = !a.untrackedValue; + b.value = !b.untrackedValue; expect(log).toEqual([true, false]); }); }); @@ -264,7 +264,7 @@ describe('signal', () => { } async function flushSignals() { - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; + await container.$renderPromise$; } /** Simulates the QRLs being lazy loaded once per test. */ @@ -286,8 +286,9 @@ describe('signal', () => { function effectQrl(fnQrl: QRL<() => void>) { const qrl = fnQrl as QRLInternal<() => void>; - const element: HostElement = null!; + const element: HostElement = vnode_newVirtual(); const task = new Task(0, 0, element, fnQrl as QRLInternal, undefined, null); + vnode_setProp(element, ELEMENT_SEQ, [task]); if (!qrl.resolved) { throw qrl.resolve(); } else { diff --git a/packages/qwik/src/core/tests/use-task.spec.tsx b/packages/qwik/src/core/tests/use-task.spec.tsx index 6b9e3b8c196..809e8a46c7b 100644 --- a/packages/qwik/src/core/tests/use-task.spec.tsx +++ b/packages/qwik/src/core/tests/use-task.spec.tsx @@ -391,7 +391,11 @@ describe.each([ }); const { vNode, document } = await render(, { debug }); - expect((globalThis as any).log).toEqual(['quadruple', 'double', 'Counter', 'quadruple']); + if (render === ssrRenderToDom) { + expect((globalThis as any).log).toEqual(['quadruple', 'double', 'quadruple', 'Counter']); + } else { + expect((globalThis as any).log).toEqual(['quadruple', 'double', 'Counter', 'quadruple']); + } expect(vNode).toMatchVDOM(
- +
redraw.value)} data={state.data} />
\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nelement\n\n\n\n\nElement \\| VNode\n\n\n\n\n\n
\n\n**Returns:**\n\nIClientContainer", + "content": "```typescript\nexport declare function getDomContainer(element: Element): IClientContainer;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nelement\n\n\n\n\nElement\n\n\n\n\n\n
\n\n**Returns:**\n\nIClientContainer", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/client/dom-container.ts", "mdFile": "core.getdomcontainer.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index e2e25d509a0..6a074c387e5 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -1458,9 +1458,7 @@ export type FunctionComponent

= { ## getDomContainer ```typescript -export declare function getDomContainer( - element: Element | VNode, -): IClientContainer; +export declare function getDomContainer(element: Element): IClientContainer; ```
@@ -1482,7 +1480,7 @@ element -Element \| VNode +Element diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index 4c4a72bcfad..063865c7534 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -765,7 +765,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => { } }; _waitNextPage().then(() => { - const container = _getQContainerElement(elm as _ElementVNode)!; + const container = _getQContainerElement(elm as Element)!; container.setAttribute(Q_ROUTE, routeName); const scrollState = currentScrollState(scroller); saveScrollHistory(scrollState); diff --git a/packages/qwik/src/core/client/chore-array.ts b/packages/qwik/src/core/client/chore-array.ts deleted file mode 100644 index b2841344d88..00000000000 --- a/packages/qwik/src/core/client/chore-array.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl'; -import { StoreHandler } from '../reactive-primitives/impl/store'; -import { assertFalse } from '../shared/error/assert'; -import { PropsProxyHandler } from '../shared/jsx/props-proxy'; -import { isQrl } from '../shared/qrl/qrl-utils'; -import type { Chore } from '../shared/scheduler'; -import { - ssrNodeDocumentPosition, - vnode_documentPosition, -} from '../shared/scheduler-document-position'; -import { ChoreType } from '../shared/util-chore-type'; -import type { ISsrNode } from '../ssr/ssr-types'; -import { vnode_isVNode } from './vnode'; - -export class ChoreArray extends Array { - add(value: Chore): number { - /// We need to ensure that the `queue` is sorted by priority. - /// 1. Find a place where to insert into. - const idx = sortedFindIndex(this, value); - - if (idx < 0) { - /// 2. Insert the chore into the queue. - this.splice(~idx, 0, value); - return idx; - } - - const existing = this[idx]; - /** - * When a derived signal is updated we need to run vnode_diff. However the signal can update - * multiple times during component execution. For this reason it is necessary for us to update - * the chore with the latest result of the signal. - */ - if (existing.$payload$ !== value.$payload$) { - existing.$payload$ = value.$payload$; - } - return idx; - } - - delete(value: Chore) { - const idx = this.indexOf(value); - if (idx >= 0) { - this.splice(idx, 1); - } - return idx; - } -} - -export function sortedFindIndex(sortedArray: Chore[], value: Chore): number { - /// We need to ensure that the `queue` is sorted by priority. - /// 1. Find a place where to insert into. - let bottom = 0; - let top = sortedArray.length; - while (bottom < top) { - const middle = bottom + ((top - bottom) >> 1); - const midChore = sortedArray[middle]; - const comp = choreComparator(value, midChore); - if (comp < 0) { - top = middle; - } else if (comp > 0) { - bottom = middle + 1; - } else { - // We already have the host in the queue. - return middle; - } - } - return ~bottom; -} - -/** - * Compares two chores to determine their execution order in the scheduler's queue. - * - * @param a - The first chore to compare - * @param b - The second chore to compare - * @returns A number indicating the relative order of the chores. A negative number means `a` runs - * before `b`. - */ -export function choreComparator(a: Chore, b: Chore): number { - const macroTypeDiff = (a.$type$ & ChoreType.MACRO) - (b.$type$ & ChoreType.MACRO); - if (macroTypeDiff !== 0) { - return macroTypeDiff; - } - - const aHost = a.$host$; - const bHost = b.$host$; - - if (aHost !== bHost && aHost !== null && bHost !== null) { - if (vnode_isVNode(aHost) && vnode_isVNode(bHost)) { - // we are running on the client. - const hostDiff = vnode_documentPosition(aHost, bHost); - if (hostDiff !== 0) { - return hostDiff; - } - } else { - assertFalse(vnode_isVNode(aHost), 'expected aHost to be SSRNode but it is a VNode'); - assertFalse(vnode_isVNode(bHost), 'expected bHost to be SSRNode but it is a VNode'); - const hostDiff = ssrNodeDocumentPosition(aHost as ISsrNode, bHost as ISsrNode); - if (hostDiff !== 0) { - return hostDiff; - } - } - } - - const microTypeDiff = (a.$type$ & ChoreType.MICRO) - (b.$type$ & ChoreType.MICRO); - if (microTypeDiff !== 0) { - return microTypeDiff; - } - // types are the same - - const idxDiff = toNumber(a.$idx$) - toNumber(b.$idx$); - if (idxDiff !== 0) { - return idxDiff; - } - - // If the host is the same (or missing), and the type is the same, we need to compare the target. - if (a.$target$ !== b.$target$) { - if (isQrl(a.$target$) && isQrl(b.$target$) && a.$target$.$hash$ === b.$target$.$hash$) { - return 0; - } - // 1 means that we are going to process chores as FIFO - return 1; - } - - // ensure that the effect chores are scheduled for the same target - // TODO: can we do this better? - if ( - a.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS && - b.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS && - ((a.$target$ instanceof StoreHandler && b.$target$ instanceof StoreHandler) || - (a.$target$ instanceof PropsProxyHandler && b.$target$ instanceof PropsProxyHandler) || - (a.$target$ instanceof AsyncComputedSignalImpl && - b.$target$ instanceof AsyncComputedSignalImpl)) && - a.$payload$ !== b.$payload$ - ) { - return 1; - } - - // The chores are the same and will run only once - return 0; -} - -function toNumber(value: number | string): number { - return typeof value === 'number' ? value : -1; -} diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 83548b84936..dbb8df06d12 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -57,7 +57,7 @@ import { vnode_newUnMaterializedElement, vnode_setProp, type VNodeJournal, -} from './vnode'; +} from './vnode-utils'; import type { ElementVNode } from '../shared/vnode/element-vnode'; import type { VNode } from '../shared/vnode/vnode'; import type { VirtualVNode } from '../shared/vnode/virtual-vnode'; diff --git a/packages/qwik/src/core/client/dom-render.ts b/packages/qwik/src/core/client/dom-render.ts index 3ed59f2da27..65404350502 100644 --- a/packages/qwik/src/core/client/dom-render.ts +++ b/packages/qwik/src/core/client/dom-render.ts @@ -7,7 +7,7 @@ import { QContainerAttr } from '../shared/utils/markers'; import type { RenderOptions, RenderResult } from './types'; import { qDev } from '../shared/utils/qdev'; import { QError, qError } from '../shared/error/error'; -import { vnode_setProp } from './vnode'; +import { vnode_setProp } from './vnode-utils'; import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; import { NODE_DIFF_DATA_KEY } from '../shared/cursor/cursor-props'; diff --git a/packages/qwik/src/core/client/process-vnode-data.unit.tsx b/packages/qwik/src/core/client/process-vnode-data.unit.tsx index f33fad6dc39..d2d72a1cf18 100644 --- a/packages/qwik/src/core/client/process-vnode-data.unit.tsx +++ b/packages/qwik/src/core/client/process-vnode-data.unit.tsx @@ -7,7 +7,7 @@ import { processVNodeData } from './process-vnode-data'; import type { ClientContainer } from './types'; import { QContainerValue } from '../shared/types'; import { QContainerAttr } from '../shared/utils/markers'; -import { vnode_getFirstChild } from './vnode'; +import { vnode_getFirstChild } from './vnode-utils'; import { Fragment } from '@qwik.dev/core'; describe('processVnodeData', () => { diff --git a/packages/qwik/src/core/client/run-qrl.ts b/packages/qwik/src/core/client/run-qrl.ts index 3e4985caac0..96199380d31 100644 --- a/packages/qwik/src/core/client/run-qrl.ts +++ b/packages/qwik/src/core/client/run-qrl.ts @@ -1,5 +1,5 @@ import type { QRLInternal } from '../shared/qrl/qrl-class'; -import { catchError, retryOnPromise, safeCall } from '../shared/utils/promises'; +import { catchError, retryOnPromise } from '../shared/utils/promises'; import type { ValueOrPromise } from '../shared/utils/types'; import type { ElementVNode } from '../shared/vnode/element-vnode'; import { getInvokeContext } from '../use/use-core'; diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index 7d39c00227a..0f5d3d3cc07 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -85,7 +85,7 @@ import { vnode_truncate, vnode_walkVNode, type VNodeJournal, -} from './vnode'; +} from './vnode-utils'; import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace'; import { cleanupDestroyable } from '../use/utils/destroyable'; import { SignalImpl } from '../reactive-primitives/impl/signal-impl'; diff --git a/packages/qwik/src/core/client/vnode-diff.unit.tsx b/packages/qwik/src/core/client/vnode-diff.unit.tsx index 5c83c61e9f0..99e54b66e4d 100644 --- a/packages/qwik/src/core/client/vnode-diff.unit.tsx +++ b/packages/qwik/src/core/client/vnode-diff.unit.tsx @@ -23,7 +23,12 @@ import { StoreFlags } from '../reactive-primitives/types'; import { QError, qError } from '../shared/error/error'; import type { QElement } from '../shared/types'; import { VNodeFlags } from './types'; -import { vnode_getFirstChild, vnode_getNode, vnode_setProp, type VNodeJournal } from './vnode'; +import { + vnode_getFirstChild, + vnode_getNode, + vnode_setProp, + type VNodeJournal, +} from './vnode-utils'; import { vnode_diff } from './vnode-diff'; import { _flushJournal } from '../shared/cursor/cursor-flush'; import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; diff --git a/packages/qwik/src/core/client/vnode-impl.ts b/packages/qwik/src/core/client/vnode-impl.ts deleted file mode 100644 index 79b0e20f28b..00000000000 --- a/packages/qwik/src/core/client/vnode-impl.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { VNodeFlags } from './types'; -import { mapApp_findIndx, mapArray_get } from './util-mapArray'; -import { - vnode_ensureElementInflated, - vnode_toString, - VNodeJournalOpCode, - type VNodeJournal, -} from './vnode'; -import type { ChoreArray } from './chore-array'; -import { 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 { - props: unknown[] | null = null; - slotParent: VNode | null = null; - // scheduled chores for this vnode - chores: ChoreArray | null = null; - // blocked chores for this vnode - blockedChores: ChoreArray | null = null; - - constructor( - public flags: VNodeFlags, - public parent: ElementVNode | VirtualVNode | null, - public previousSibling: VNode | null | undefined, - public nextSibling: VNode | null | undefined - ) { - super(); - } - - getProp(key: string, getObject: ((id: string) => any) | null): T | null { - const type = this.flags; - if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { - type & VNodeFlags.Element && vnode_ensureElementInflated(this); - this.props ||= []; - const idx = mapApp_findIndx(this.props as any, key, 0); - if (idx >= 0) { - let value = this.props[idx + 1] as any; - if (typeof value === 'string' && getObject) { - this.props[idx + 1] = value = getObject(value); - } - return value; - } - } - return null; - } - - setProp(key: string, value: any) { - this.props ||= []; - const idx = mapApp_findIndx(this.props, key, 0); - if (idx >= 0) { - this.props[idx + 1] = value as any; - } else if (value != null) { - this.props.splice(idx ^ -1, 0, key, value as any); - } - } - - getAttr(key: string): string | null { - if ((this.flags & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { - vnode_ensureElementInflated(this); - this.props ||= []; - return mapArray_get(this.props, key, 0) as string | null; - } - return null; - } - - setAttr(key: string, value: string | null | boolean, journal: VNodeJournal | null) { - const type = this.flags; - if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { - vnode_ensureElementInflated(this); - this.props ||= []; - const idx = mapApp_findIndx(this.props, key, 0); - - if (idx >= 0) { - if (this.props[idx + 1] != value && this instanceof ElementVNode) { - // Values are different, update DOM - journal && journal.push(VNodeJournalOpCode.SetAttribute, this.element, key, value); - } - if (value == null) { - this.props.splice(idx, 2); - } else { - this.props[idx + 1] = value; - } - } else if (value != null) { - this.props.splice(idx ^ -1, 0, key, value); - if (this instanceof ElementVNode) { - // New value, update DOM - journal && journal.push(VNodeJournalOpCode.SetAttribute, this.element, key, value); - } - } - } - } - - toString(): string { - if (isDev) { - return vnode_toString.call(this); - } - return String(this); - } -} - -/** @internal */ -export class TextVNode extends VNode { - constructor( - flags: VNodeFlags, - parent: ElementVNode | VirtualVNode | null, - previousSibling: VNode | null | undefined, - nextSibling: VNode | null | undefined, - public textNode: Text | null, - public text: string | undefined - ) { - super(flags, parent, previousSibling, nextSibling); - } -} - -/** @internal */ -export class VirtualVNode extends VNode { - constructor( - flags: VNodeFlags, - parent: ElementVNode | VirtualVNode | null, - previousSibling: VNode | null | undefined, - nextSibling: VNode | null | undefined, - public firstChild: VNode | null | undefined, - public lastChild: VNode | null | undefined - ) { - super(flags, parent, previousSibling, nextSibling); - } -} - -/** @internal */ -export class ElementVNode extends VNode { - constructor( - flags: VNodeFlags, - parent: ElementVNode | VirtualVNode | null, - previousSibling: VNode | null | undefined, - nextSibling: VNode | null | undefined, - public firstChild: VNode | null | undefined, - public lastChild: VNode | null | undefined, - public element: QElement, - public elementName: string | undefined - ) { - super(flags, parent, previousSibling, nextSibling); - } -} diff --git a/packages/qwik/src/core/client/vnode-namespace.ts b/packages/qwik/src/core/client/vnode-namespace.ts index 76d52c3e732..81933c34406 100644 --- a/packages/qwik/src/core/client/vnode-namespace.ts +++ b/packages/qwik/src/core/client/vnode-namespace.ts @@ -20,11 +20,10 @@ import { vnode_isElementVNode, vnode_isTextVNode, type VNodeJournal, -} from './vnode'; +} from './vnode-utils'; import type { ElementVNode } from '../shared/vnode/element-vnode'; import type { VNode } from '../shared/vnode/vnode'; import type { TextVNode } from '../shared/vnode/text-vnode'; -import type { Container } from '../shared/types'; export const isForeignObjectElement = (elementName: string) => { return isDev ? elementName.toLowerCase() === 'foreignobject' : elementName === 'foreignObject'; diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode-utils.ts similarity index 100% rename from packages/qwik/src/core/client/vnode.ts rename to packages/qwik/src/core/client/vnode-utils.ts diff --git a/packages/qwik/src/core/client/vnode.unit.tsx b/packages/qwik/src/core/client/vnode.unit.tsx index b7450aa2beb..9b0d6dc295d 100644 --- a/packages/qwik/src/core/client/vnode.unit.tsx +++ b/packages/qwik/src/core/client/vnode.unit.tsx @@ -20,7 +20,7 @@ import { vnode_setText, vnode_walkVNode, type VNodeJournal, -} from './vnode'; +} from './vnode-utils'; import type { ElementVNode } from '../shared/vnode/element-vnode'; import type { VNode } from '../shared/vnode/vnode'; import type { TextVNode } from '../shared/vnode/text-vnode'; diff --git a/packages/qwik/src/core/debug.ts b/packages/qwik/src/core/debug.ts index 3d273a72a97..b17ac760b3a 100644 --- a/packages/qwik/src/core/debug.ts +++ b/packages/qwik/src/core/debug.ts @@ -1,6 +1,6 @@ import { isSignal } from './reactive-primitives/utils'; // ^ keep this first to avoid circular dependency breaking class extend -import { vnode_getProp, vnode_isVNode } from './client/vnode'; +import { vnode_getProp, vnode_isVNode } from './client/vnode-utils'; import { ComputedSignalImpl } from './reactive-primitives/impl/computed-signal-impl'; import { isStore } from './reactive-primitives/impl/store'; import { WrappedSignalImpl } from './reactive-primitives/impl/wrapped-signal-impl'; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 707ae88cd44..7fd4bcb07ac 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -24,7 +24,7 @@ export { vnode_isTextVNode as _vnode_isTextVNode, vnode_isVirtualVNode as _vnode_isVirtualVNode, vnode_toString as _vnode_toString, -} from './client/vnode'; +} from './client/vnode-utils'; export type { VNode as _VNode } from './shared/vnode/vnode'; export type { ElementVNode as _ElementVNode } from './shared/vnode/element-vnode'; export type { TextVNode as _TextVNode } from './shared/vnode/text-vnode'; diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 536b668d225..8cfa094b8e0 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -41,14 +41,8 @@ export type ClassList = string | undefined | null | false | Record | null; - // Warning: (ae-forgotten-export) The symbol "VNodeJournal" needs to be exported by the entry point index.d.ts - // - // (undocumented) - $journal$: VNodeJournal; // (undocumented) $locale$: string; // (undocumented) @@ -219,16 +213,15 @@ class DomContainer extends _SharedContainer implements ClientContainer { $forwardRefs$: Array | null; // (undocumented) $getObjectById$: (id: number | string) => unknown; + $hoistStyles$(): void; // (undocumented) $instanceHash$: string; // (undocumented) - $journal$: VNodeJournal; - // (undocumented) $qFuncs$: Array<(...args: unknown[]) => unknown>; // (undocumented) $rawStateData$: unknown[]; // (undocumented) - $setRawState$(id: number, vParent: _ElementVNode | _VirtualVNode): void; + $setRawState$(id: number, vParent: _VNode): void; // Warning: (ae-forgotten-export) The symbol "ObjToProxyMap" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -280,17 +273,18 @@ export const _EFFECT_BACK_REF: unique symbol; // @internal (undocumented) export class _ElementVNode extends _VNode { - constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined, element: QElement, elementName: string | undefined); - // Warning: (ae-forgotten-export) The symbol "QElement" needs to be exported by the entry point index.d.ts - // - // (undocumented) - element: QElement; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts + constructor(key: string | null, flags: _VNodeFlags, parent: _VNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined, node: Element, elementName: string | undefined); // (undocumented) elementName: string | undefined; // (undocumented) firstChild: _VNode | null | undefined; // (undocumented) + key: string | null; + // (undocumented) lastChild: _VNode | null | undefined; + // (undocumented) + node: Element; } // @internal (undocumented) @@ -315,6 +309,12 @@ export type EventHandler = { // @internal (undocumented) export const eventQrl: (qrl: QRL) => QRL; +// Warning: (ae-forgotten-export) The symbol "SSRContainer" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ISsrNode" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export function _executeSsrChores(container: SSRContainer, ssrNode: ISsrNode): ValueOrPromise; + // Warning: (ae-forgotten-export) The symbol "WrappedSignalImpl" needs to be exported by the entry point index.d.ts // // @internal (undocumented) @@ -337,7 +337,6 @@ export type FunctionComponent

= { }['renderFn']; // Warning: (ae-forgotten-export) The symbol "PropsProxy" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @internal export const _getConstProps: (props: PropsProxy | Record | null | undefined) => Props | null; @@ -351,11 +350,10 @@ export const _getContextElement: () => unknown; // @internal (undocumented) export const _getContextEvent: () => unknown; -// Warning: (ae-incompatible-release-tags) The symbol "getDomContainer" is marked as @public, but its signature references "_VNode" which is marked as @internal // Warning: (ae-incompatible-release-tags) The symbol "getDomContainer" is marked as @public, but its signature references "ClientContainer" which is marked as @internal // // @public (undocumented) -function getDomContainer(element: Element | _VNode): ClientContainer; +function getDomContainer(element: Element): ClientContainer; export { getDomContainer as _getDomContainer } export { getDomContainer } @@ -366,7 +364,7 @@ export function getLocale(defaultLocale?: string): string; export const getPlatform: () => CorePlatform; // @internal (undocumented) -export function _getQContainerElement(element: Element | _VNode): Element | null; +export function _getQContainerElement(element: Element): Element | null; // @internal export const _getVarProps: (props: PropsProxy | Record | null | undefined) => Props | null; @@ -417,8 +415,6 @@ export const isSignal: (value: any) => value is Signal; // // @internal (undocumented) export interface ISsrComponentFrame { - // Warning: (ae-forgotten-export) The symbol "ISsrNode" needs to be exported by the entry point index.d.ts - // // (undocumented) componentNode: ISsrNode; // (undocumented) @@ -455,7 +451,7 @@ export const _isTask: (value: any) => value is Task; // @public export const jsx: >(type: T, props: T extends FunctionComponent ? PROPS : Props, key?: string | number | null, _isStatic?: boolean, dev?: JsxDevOpts) => JSXNode; -// @internal (undocumented) +// @internal @deprecated (undocumented) export const _jsxBranch: (input?: T) => T | undefined; // @internal @deprecated (undocumented) @@ -930,24 +926,24 @@ export abstract class _SharedContainer implements Container { // (undocumented) $currentUniqueId$: number; // (undocumented) - $flushEpoch$: number; + $cursorCount$: number; // (undocumented) readonly $getObjectById$: (id: number | string) => any; // (undocumented) $instanceHash$: string | null; // (undocumented) readonly $locale$: string; - // Warning: (ae-forgotten-export) The symbol "Scheduler" needs to be exported by the entry point index.d.ts - // // (undocumented) - readonly $scheduler$: Scheduler; + $renderPromise$: Promise | null; + // (undocumented) + $resolveRenderPromise$: (() => void) | null; // (undocumented) $serverData$: Record; // (undocumented) readonly $storeProxyMap$: ObjToProxyMap; // (undocumented) readonly $version$: string; - constructor(journalFlush: () => void, serverData: Record, locale: string); + constructor(serverData: Record, locale: string); // (undocumented) abstract ensureProjectionResolved(host: HostElement): void; // (undocumented) @@ -1669,11 +1665,11 @@ export interface TaskOptions { // @internal (undocumented) export class _TextVNode extends _VNode { - constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, textNode: Text | null, text: string | undefined); + constructor(flags: _VNodeFlags, parent: _VNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null, node: Text | null, text: string | undefined); // (undocumented) - text: string | undefined; + node: Text | null; // (undocumented) - textNode: Text | null; + text: string | undefined; } // @public @@ -1840,10 +1836,12 @@ export const version: string; // @internal (undocumented) export class _VirtualVNode extends _VNode { - constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined); + constructor(key: string | null, flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null, firstChild: _VNode | null | undefined, lastChild: _VNode | null | undefined); // (undocumented) firstChild: _VNode | null | undefined; // (undocumented) + key: string | null; + // (undocumented) lastChild: _VNode | null | undefined; } @@ -1854,31 +1852,25 @@ export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | ' // // @internal (undocumented) export abstract class _VNode extends BackRef { - constructor(flags: _VNodeFlags, parent: _ElementVNode | _VirtualVNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined); - // (undocumented) - blockedChores: ChoreArray | null; - // Warning: (ae-forgotten-export) The symbol "ChoreArray" needs to be exported by the entry point index.d.ts + constructor(flags: _VNodeFlags, parent: _VNode | null, previousSibling: _VNode | null | undefined, nextSibling: _VNode | null | undefined, props: Props | null); + // Warning: (ae-forgotten-export) The symbol "ChoreBits" needs to be exported by the entry point index.d.ts // // (undocumented) - chores: ChoreArray | null; + dirty: ChoreBits; // (undocumented) - flags: _VNodeFlags; + dirtyChildren: _VNode[] | null; // (undocumented) - getAttr(key: string): string | null; + flags: _VNodeFlags; // (undocumented) - getProp(key: string, getObject: ((id: string) => any) | null): T | null; + nextDirtyChildIndex: number; // (undocumented) nextSibling: _VNode | null | undefined; // (undocumented) - parent: _ElementVNode | _VirtualVNode | null; + parent: _VNode | null; // (undocumented) previousSibling: _VNode | null | undefined; // (undocumented) - props: unknown[] | null; - // (undocumented) - setAttr(key: string, value: string | null | boolean, journal: VNodeJournal | null): void; - // (undocumented) - setProp(key: string, value: any): void; + props: Props | null; // (undocumented) slotParent: _VNode | null; // (undocumented) @@ -1894,9 +1886,6 @@ export const _vnode_getAttrKeys: (vnode: _ElementVNode | _VirtualVNode) => strin // @internal (undocumented) export const _vnode_getFirstChild: (vnode: _VNode) => _VNode | null; -// @internal (undocumented) -export const _vnode_getProps: (vnode: _ElementVNode | _VirtualVNode) => unknown[]; - // @internal (undocumented) export const _vnode_isMaterialized: (vNode: _VNode) => boolean; @@ -1911,6 +1900,8 @@ export function _vnode_toString(this: _VNode | null, depth?: number, offset?: st // @internal export const enum _VNodeFlags { + // (undocumented) + Cursor = 64, // (undocumented) Deleted = 32, // (undocumented) @@ -1924,15 +1915,15 @@ export const enum _VNodeFlags { // (undocumented) INFLATED_TYPE_MASK = 15, // (undocumented) - NAMESPACE_MASK = 192, + NAMESPACE_MASK = 384, // (undocumented) - NEGATED_NAMESPACE_MASK = -193, + NEGATED_NAMESPACE_MASK = -385, // (undocumented) NS_html = 0, // (undocumented) - NS_math = 128, + NS_math = 256, // (undocumented) - NS_svg = 64, + NS_svg = 128, // (undocumented) Resolved = 16, // (undocumented) @@ -1946,8 +1937,6 @@ export const enum _VNodeFlags { // @internal (undocumented) export const _waitUntilRendered: (elm: Element) => Promise; -// Warning: (ae-forgotten-export) The symbol "SSRContainer" needs to be exported by the entry point index.d.ts -// // @internal (undocumented) export function _walkJSX(ssr: SSRContainer, value: JSXOutput, options: { currentStyleScoped: string | null; diff --git a/packages/qwik/src/core/reactive-primitives/cleanup.ts b/packages/qwik/src/core/reactive-primitives/cleanup.ts index f69bd6b434c..517fa8822b7 100644 --- a/packages/qwik/src/core/reactive-primitives/cleanup.ts +++ b/packages/qwik/src/core/reactive-primitives/cleanup.ts @@ -1,4 +1,4 @@ -import { ensureMaterialized, vnode_isElementVNode, vnode_isVNode } from '../client/vnode'; +import { ensureMaterialized, vnode_isElementVNode, vnode_isVNode } from '../client/vnode-utils'; import type { Container } from '../shared/types'; import { SignalImpl } from './impl/signal-impl'; import { WrappedSignalImpl } from './impl/wrapped-signal-impl'; diff --git a/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx b/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx index 231b97e2ee7..9dd6a507253 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx +++ b/packages/qwik/src/core/reactive-primitives/impl/signal.unit.tsx @@ -1,12 +1,11 @@ import { $, _wrapProp, isBrowser } from '@qwik.dev/core'; -import { createDocument, getTestPlatform } from '@qwik.dev/core/testing'; +import { createDocument } from '@qwik.dev/core/testing'; import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from 'vitest'; import { getDomContainer } from '../../client/dom-container'; import { implicit$FirstArg } from '../../shared/qrl/implicit_dollar'; import { inlinedQrl } from '../../shared/qrl/qrl'; import { type QRLInternal } from '../../shared/qrl/qrl-class'; import { type QRL } from '../../shared/qrl/qrl.public'; -import { ChoreType } from '../../shared/util-chore-type'; import type { Container, HostElement } from '../../shared/types'; import { retryOnPromise } from '../../shared/utils/promises'; import { invoke, newInvokeContext } from '../../use/use-core'; @@ -27,7 +26,7 @@ import { type Signal, } from '../signal.public'; import { getSubscriber } from '../subscriber'; -import { vnode_newVirtual, vnode_setProp } from '../../client/vnode'; +import { vnode_newVirtual, vnode_setProp } from '../../client/vnode-utils'; import { ELEMENT_SEQ } from '../../shared/utils/markers'; class Foo { diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx b/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx index a071efb28c2..002a0019dc9 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx +++ b/packages/qwik/src/core/reactive-primitives/impl/store.unit.tsx @@ -9,7 +9,7 @@ import { newInvokeContext } from '../../use/use-core'; import type { QRLInternal } from '../../shared/qrl/qrl-class'; import { Task } from '../../use/use-task'; import { getSubscriber } from '../subscriber'; -import { vnode_newVirtual, vnode_setProp } from '../../client/vnode'; +import { vnode_newVirtual, vnode_setProp } from '../../client/vnode-utils'; import { ELEMENT_SEQ } from 'packages/qwik/src/server/qwik-copy'; describe('v2/store', () => { 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 45afb405648..fbab0d36acd 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts @@ -1,4 +1,3 @@ -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'; diff --git a/packages/qwik/src/core/shared/component-execution.ts b/packages/qwik/src/core/shared/component-execution.ts index 68d83db636b..f010fe51f79 100644 --- a/packages/qwik/src/core/shared/component-execution.ts +++ b/packages/qwik/src/core/shared/component-execution.ts @@ -1,5 +1,5 @@ import { isDev } from '@qwik.dev/core/build'; -import { vnode_isVNode } from '../client/vnode'; +import { vnode_isVNode } from '../client/vnode-utils'; import { isSignal } from '../reactive-primitives/utils'; import { clearAllEffects } from '../reactive-primitives/cleanup'; import { diff --git a/packages/qwik/src/core/shared/cursor/chore-execution.ts b/packages/qwik/src/core/shared/cursor/chore-execution.ts index 1c0101c53a9..a42a234591f 100644 --- a/packages/qwik/src/core/shared/cursor/chore-execution.ts +++ b/packages/qwik/src/core/shared/cursor/chore-execution.ts @@ -1,4 +1,4 @@ -import { type VNodeJournal } from '../../client/vnode'; +import { type VNodeJournal } from '../../client/vnode-utils'; import { vnode_diff } from '../../client/vnode-diff'; import { runResource, type ResourceDescriptor } from '../../use/use-resource'; import { Task, TaskFlags, runTask, type TaskFn } from '../../use/use-task'; diff --git a/packages/qwik/src/core/shared/cursor/cursor-flush.ts b/packages/qwik/src/core/shared/cursor/cursor-flush.ts index b085b8b8c3e..9ecca7dddb8 100644 --- a/packages/qwik/src/core/shared/cursor/cursor-flush.ts +++ b/packages/qwik/src/core/shared/cursor/cursor-flush.ts @@ -1,4 +1,4 @@ -import { type VNodeJournal } from '../../client/vnode'; +import { type VNodeJournal } from '../../client/vnode-utils'; import { runTask } from '../../use/use-task'; import { QContainerValue, type Container } from '../types'; import { dangerouslySetInnerHTML, QContainerAttr } from '../utils/markers'; diff --git a/packages/qwik/src/core/shared/cursor/cursor-props.ts b/packages/qwik/src/core/shared/cursor/cursor-props.ts index 5dc5eb1457a..4a6a1a6d3b3 100644 --- a/packages/qwik/src/core/shared/cursor/cursor-props.ts +++ b/packages/qwik/src/core/shared/cursor/cursor-props.ts @@ -2,7 +2,7 @@ import type { VNode } from '../vnode/vnode'; import { isCursor } from './cursor'; import { removeCursorFromQueue } from './cursor-queue'; import type { Container } from '../types'; -import type { VNodeJournal } from '../../client/vnode'; +import type { VNodeJournal } from '../../client/vnode-utils'; import type { Task } from '../../use/use-task'; /** diff --git a/packages/qwik/src/core/shared/scheduler-document-position.ts b/packages/qwik/src/core/shared/scheduler-document-position.ts deleted file mode 100644 index cac04737d89..00000000000 --- a/packages/qwik/src/core/shared/scheduler-document-position.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { VNode } from '../client/vnode-impl'; -import type { ISsrNode } from '../ssr/ssr-types'; - -/// These global variables are used to avoid creating new arrays for each call to `vnode_documentPosition`. -const aVNodePath: VNode[] = []; -const bVNodePath: VNode[] = []; -/** - * Compare two VNodes and determine their document position relative to each other. - * - * @param a VNode to compare - * @param b VNode to compare - * @returns -1 if `a` is before `b`, 0 if `a` is the same as `b`, 1 if `a` is after `b`. - */ -export const vnode_documentPosition = (a: VNode, b: VNode): -1 | 0 | 1 => { - if (a === b) { - return 0; - } - - let aDepth = -1; - let bDepth = -1; - while (a) { - const vNode = (aVNodePath[++aDepth] = a); - a = (vNode.parent || a.slotParent)!; - } - while (b) { - const vNode = (bVNodePath[++bDepth] = b); - b = (vNode.parent || b.slotParent)!; - } - - while (aDepth >= 0 && bDepth >= 0) { - a = aVNodePath[aDepth] as VNode; - b = bVNodePath[bDepth] as VNode; - if (a === b) { - // if the nodes are the same, we need to check the next level. - aDepth--; - bDepth--; - } else { - // We found a difference so we need to scan nodes at this level. - let cursor: VNode | null | undefined = b; - do { - cursor = cursor.nextSibling; - if (cursor === a) { - return 1; - } - } while (cursor); - cursor = b; - do { - cursor = cursor.previousSibling; - if (cursor === a) { - return -1; - } - } while (cursor); - if (b.slotParent) { - // The "b" node is a projection, so we need to set it after "a" node, - // because the "a" node could be a context provider. - return -1; - } - // The node is not in the list of siblings, that means it must be disconnected. - return 1; - } - } - return aDepth < bDepth ? -1 : 1; -}; - -/// These global variables are used to avoid creating new arrays for each call to `ssrNodeDocumentPosition`. -const aSsrNodePath: ISsrNode[] = []; -const bSsrNodePath: ISsrNode[] = []; -/** - * Compare two SSR nodes and determine their document position relative to each other. Compares only - * position between parent and child. - * - * @param a SSR node to compare - * @param b SSR node to compare - * @returns -1 if `a` is before `b`, 0 if `a` is the same as `b`, 1 if `a` is after `b`. - */ -export const ssrNodeDocumentPosition = (a: ISsrNode, b: ISsrNode): -1 | 0 | 1 => { - if (a === b) { - return 0; - } - - let aDepth = -1; - let bDepth = -1; - while (a) { - const ssrNode = (aSsrNodePath[++aDepth] = a); - a = ssrNode.parentComponent!; - } - while (b) { - const ssrNode = (bSsrNodePath[++bDepth] = b); - b = ssrNode.parentComponent!; - } - - while (aDepth >= 0 && bDepth >= 0) { - a = aSsrNodePath[aDepth] as ISsrNode; - b = bSsrNodePath[bDepth] as ISsrNode; - if (a === b) { - // if the nodes are the same, we need to check the next level. - aDepth--; - bDepth--; - } else { - return 1; - } - } - return aDepth < bDepth ? -1 : 1; -}; diff --git a/packages/qwik/src/core/shared/scheduler-document-position.unit.ts b/packages/qwik/src/core/shared/scheduler-document-position.unit.ts deleted file mode 100644 index 0a1db1ac5c6..00000000000 --- a/packages/qwik/src/core/shared/scheduler-document-position.unit.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { vnode_getFirstChild, vnode_newUnMaterializedElement } from '../client/vnode'; -import type { ContainerElement, QDocument } from '../client/types'; -import { vnode_documentPosition } from './scheduler-document-position'; -import { createDocument } from '@qwik.dev/dom'; -import type { ElementVNode } from '../client/vnode-impl'; - -describe('vnode_documentPosition', () => { - let parent: ContainerElement; - let document: QDocument; - let vParent: ElementVNode; - beforeEach(() => { - document = createDocument() as QDocument; - document.qVNodeData = new WeakMap(); - parent = document.createElement('test') as ContainerElement; - parent.qVNodeRefs = new Map(); - vParent = vnode_newUnMaterializedElement(parent); - }); - afterEach(() => { - parent = null!; - document = null!; - vParent = null!; - }); - - it('should compare two elements', () => { - parent.innerHTML = ''; - const b = vnode_getFirstChild(vParent) as ElementVNode; - const i = b.nextSibling as ElementVNode; - expect(vnode_documentPosition(b, i)).toBe(-1); - expect(vnode_documentPosition(i, b)).toBe(1); - }); - it('should compare two virtual vNodes', () => { - parent.innerHTML = 'AB'; - document.qVNodeData.set(parent, '{B}{B}'); - const a = vnode_getFirstChild(vParent) as ElementVNode; - const b = a.nextSibling as ElementVNode; - expect(vnode_documentPosition(a, b)).toBe(-1); - expect(vnode_documentPosition(b, a)).toBe(1); - }); - it('should compare two virtual vNodes', () => { - parent.innerHTML = 'AB'; - document.qVNodeData.set(parent, '{{B}}{B}'); - const a = vnode_getFirstChild(vParent) as ElementVNode; - const a2 = vnode_getFirstChild(a) as ElementVNode; - const b = a.nextSibling as ElementVNode; - expect(vnode_documentPosition(a2, b)).toBe(-1); - expect(vnode_documentPosition(b, a2)).toBe(1); - }); -}); diff --git a/packages/qwik/src/core/shared/scheduler-rules.ts b/packages/qwik/src/core/shared/scheduler-rules.ts deleted file mode 100644 index 96605399e8a..00000000000 --- a/packages/qwik/src/core/shared/scheduler-rules.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - vnode_getProjectionParentOrParent, - vnode_isDescendantOf, - vnode_isVNode, -} from '../client/vnode'; -import { Task, TaskFlags } from '../use/use-task'; -import type { QRLInternal } from './qrl/qrl-class'; -import { ChoreState, type Chore } from './scheduler'; -import type { Container, HostElement } from './types'; -import { ChoreType } from './util-chore-type'; -import { ELEMENT_SEQ } from './utils/markers'; -import { isNumber } from './utils/types'; -import type { VNode } from '../client/vnode-impl'; -import type { ChoreArray } from '../client/chore-array'; - -type BlockingRule = { - blockedType: ChoreType; - blockingType: ChoreType; - match: (blocked: Chore, blocking: Chore, container: Container) => boolean; -}; - -const enum ChoreSetType { - CHORES, - BLOCKED_CHORES, -} - -/** - * Rules for determining if a chore is blocked by another chore. Some chores can block other chores. - * They cannot run until the blocking chore has completed. - * - * The match function is used to determine if the blocked chore is blocked by the blocking chore. - * The match function is called with the blocked chore, the blocking chore, and the container. - */ - -const VISIBLE_BLOCKING_RULES: BlockingRule[] = [ - // NODE_DIFF blocks VISIBLE on same host, - // if the blocked chore is a child of the blocking chore - // or the blocked chore is a sibling of the blocking chore - { - blockedType: ChoreType.VISIBLE, - blockingType: ChoreType.NODE_DIFF, - match: (blocked, blocking) => - isDescendant(blocked, blocking) || isDescendant(blocking, blocked), - }, - // COMPONENT blocks VISIBLE on same host - // if the blocked chore is a child of the blocking chore - // or the blocked chore is a sibling of the blocking chore - { - blockedType: ChoreType.VISIBLE, - blockingType: ChoreType.COMPONENT, - match: (blocked, blocking) => - isDescendant(blocked, blocking) || isDescendant(blocking, blocked), - }, -]; - -const BLOCKING_RULES: BlockingRule[] = [ - // QRL_RESOLVE blocks RUN_QRL, TASK, VISIBLE on same host - { - blockedType: ChoreType.RUN_QRL, - blockingType: ChoreType.QRL_RESOLVE, - match: (blocked, blocking) => { - const blockedQrl = blocked.$target$ as QRLInternal; - const blockingQrl = blocking.$target$ as QRLInternal; - return isSameHost(blocked, blocking) && isSameQrl(blockedQrl, blockingQrl); - }, - }, - { - blockedType: ChoreType.TASK, - blockingType: ChoreType.QRL_RESOLVE, - match: (blocked, blocking) => { - const blockedTask = blocked.$payload$ as Task; - const blockingQrl = blocking.$target$ as QRLInternal; - return isSameHost(blocked, blocking) && isSameQrl(blockedTask.$qrl$, blockingQrl); - }, - }, - { - blockedType: ChoreType.VISIBLE, - blockingType: ChoreType.QRL_RESOLVE, - match: (blocked, blocking) => { - const blockedTask = blocked.$payload$ as Task; - const blockingQrl = blocking.$target$ as QRLInternal; - return isSameHost(blocked, blocking) && isSameQrl(blockedTask.$qrl$, blockingQrl); - }, - }, - // COMPONENT blocks NODE_DIFF, NODE_PROP on same host - { - blockedType: ChoreType.NODE_DIFF, - blockingType: ChoreType.COMPONENT, - match: (blocked, blocking) => blocked.$host$ === blocking.$host$, - }, - { - blockedType: ChoreType.NODE_PROP, - blockingType: ChoreType.COMPONENT, - match: (blocked, blocking) => blocked.$host$ === blocking.$host$, - }, - ...VISIBLE_BLOCKING_RULES, - // TASK blocks subsequent TASKs in the same component - { - blockedType: ChoreType.TASK, - blockingType: ChoreType.TASK, - match: (blocked, blocking, container) => { - if (blocked.$host$ !== blocking.$host$) { - return false; - } - - const blockedIdx = blocked.$idx$ as number; - if (!isNumber(blockedIdx) || blockedIdx <= 0) { - return false; - } - const previousTask = findPreviousTaskInComponent(blocked.$host$, blockedIdx, container); - return previousTask === blocking.$payload$; - }, - }, -]; - -function isDescendant(descendantChore: Chore, ancestorChore: Chore): boolean { - const descendantHost = descendantChore.$host$; - const ancestorHost = ancestorChore.$host$; - if (!vnode_isVNode(descendantHost) || !vnode_isVNode(ancestorHost)) { - return false; - } - return vnode_isDescendantOf(descendantHost, ancestorHost); -} - -function isSameHost(a: Chore, b: Chore): boolean { - return a.$host$ === b.$host$; -} - -function isSameQrl(a: QRLInternal, b: QRLInternal): boolean { - return a.$symbol$ === b.$symbol$; -} - -function findAncestorBlockingChore(chore: Chore, type: ChoreSetType): Chore | null { - const host = chore.$host$; - if (!vnode_isVNode(host)) { - return null; - } - const isNormalQueue = type === ChoreSetType.CHORES; - // Walk up the ancestor tree and check the map - let current: VNode | null = host; - current = vnode_getProjectionParentOrParent(current); - while (current) { - const blockingChores = isNormalQueue ? current.chores : current.blockedChores; - if (blockingChores) { - for (const blockingChore of blockingChores) { - if ( - blockingChore.$type$ < ChoreType.VISIBLE && - blockingChore.$type$ !== ChoreType.TASK && - blockingChore.$type$ !== ChoreType.QRL_RESOLVE && - blockingChore.$type$ !== ChoreType.RUN_QRL && - blockingChore.$state$ === ChoreState.NONE - ) { - return blockingChore; - } - } - } - current = vnode_getProjectionParentOrParent(current); - } - return null; -} - -export function findBlockingChore( - chore: Chore, - choreQueue: ChoreArray, - blockedChores: ChoreArray, - runningChores: Set, - container: Container -): Chore | null { - const blockingChoreInChoreQueue = findAncestorBlockingChore(chore, ChoreSetType.CHORES); - if (blockingChoreInChoreQueue) { - return blockingChoreInChoreQueue; - } - const blockingChoreInBlockedChores = findAncestorBlockingChore( - chore, - ChoreSetType.BLOCKED_CHORES - ); - if (blockingChoreInBlockedChores) { - return blockingChoreInBlockedChores; - } - - for (const rule of BLOCKING_RULES) { - if (chore.$type$ !== rule.blockedType) { - continue; - } - - // Check in choreQueue - // TODO(perf): better to iterate in reverse order? - for (const candidate of choreQueue) { - if ( - candidate.$type$ === rule.blockingType && - rule.match(chore, candidate, container) && - candidate.$state$ === ChoreState.NONE - ) { - return candidate; - } - } - // Check in blockedChores - for (const candidate of blockedChores) { - if ( - candidate.$type$ === rule.blockingType && - rule.match(chore, candidate, container) && - candidate.$state$ === ChoreState.NONE - ) { - return candidate; - } - } - - // Check in runningChores - for (const candidate of runningChores) { - if ( - candidate.$type$ === rule.blockingType && - rule.match(chore, candidate, container) && - candidate.$state$ !== ChoreState.FAILED - ) { - return candidate; - } - } - } - return null; -} - -function findPreviousTaskInComponent( - host: HostElement, - currentTaskIdx: number, - container: Container -): Task | null { - const elementSeq = container.getHostProp(host, ELEMENT_SEQ); - if (!elementSeq || elementSeq.length <= currentTaskIdx) { - return null; - } - - for (let i = currentTaskIdx - 1; i >= 0; i--) { - const candidate = elementSeq[i]; - if (candidate instanceof Task && candidate.$flags$ & TaskFlags.TASK) { - return candidate; - } - } - return null; -} - -export function findBlockingChoreForVisible( - chore: Chore, - runningChores: Set, - container: Container -): Chore | null { - for (const rule of VISIBLE_BLOCKING_RULES) { - if (chore.$type$ !== rule.blockedType) { - continue; - } - - for (const candidate of runningChores) { - if ( - candidate.$type$ === rule.blockingType && - rule.match(chore, candidate, container) && - candidate.$state$ !== ChoreState.FAILED - ) { - return candidate; - } - } - } - return null; -} diff --git a/packages/qwik/src/core/shared/scheduler.ts b/packages/qwik/src/core/shared/scheduler.ts deleted file mode 100644 index 0a9d2e22c04..00000000000 --- a/packages/qwik/src/core/shared/scheduler.ts +++ /dev/null @@ -1,1071 +0,0 @@ -/** - * Scheduler is responsible for running application code in predictable order. - * - * ## What is a Chore? - * - * A Chore is a unit of work that needs to be done. It can be: - * - * - Task / Resource - * - Visible Task - * - Component - * - Computed - * - Node Diff - * - * ## Order of execution - * - * - Parent component chores should run before child component chores. - * - Visible Tasks should run after journal flush (visible tasks often read DOM layout.) - * - * ## Example - * - * ```typescript - * const Child = component$(() => { - * useTask$(() => { - * console.log('Child task'); - * }); - * useVisibleTask$(() => { - * console.log('Child visible-task'); - * }); - * console.log('Child render'); - * return

Child
; - * }); - * - * const Parent = component$(() => { - * const count = useSignal(0); - * useTask$(() => { - * console.log('Parent task', count.value); - * }); - * useVisibleTask$(() => { - * console.log('Parent visible-task', count.value); - * count.value++; - * }); - * console.log('Parent render', count.value); - * return ; - * }); - * ``` - * - * ## In the above example, the order of execution is: - * - * 1. Parent task 0 - * 2. Parent render 0 - * 3. Child task 0 - * 4. Child render 0 - * 5. Journal flush - * 6. Parent visible-task 0 - * 7. Parent render 1 - * 8. Journal flush - * 9. Child visible-task - * - * If at any point a new chore is scheduled it will insert itself into the correct order. - * - * ## Implementation - * - * Chores are kept in a sorted array. When a new chore is scheduled it is inserted into the correct - * location. Processing of the chores always starts from the beginning of the array. This ensures - * that parent chores are processed before child chores. - * - * ## Sorting - * - * Chores are sorted in three levels: - * - * - Macro: beforeJournalFlush, journalFlush, afterJournalFlush - * - Component: depth first order of components - * - Micro: order of chores within a component. - * - * Example of sorting: - * - * - Tasks are beforeJournalFlush, than depth first on component and finally in declaration order - * within component. - * - Visible Tasks are sorted afterJournalFlush, than depth first on component and finally in - * declaration order within component. - */ - -import { type DomContainer } from '../client/dom-container'; -import { VNodeFlags, type ClientContainer } from '../client/types'; -import { VNodeJournalOpCode, vnode_isVNode } from '../client/vnode'; -import { vnode_diff } from '../client/vnode-diff'; -import { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-impl'; -import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; -import { isSignal, type Signal } from '../reactive-primitives/signal.public'; -import type { NodePropPayload } from '../reactive-primitives/subscription-data'; -import { - SignalFlags, - type AsyncComputeQRL, - type ComputeQRL, - type EffectSubscription, - type StoreTarget, -} from '../reactive-primitives/types'; -import { scheduleEffects } from '../reactive-primitives/utils'; -import { type ISsrNode, type SSRContainer } from '../ssr/ssr-types'; -import { runResource, type ResourceDescriptor } from '../use/use-resource'; -import { Task, TaskFlags, runTask, type DescriptorBase, type TaskFn } from '../use/use-task'; -import { executeComponent } from './component-execution'; -import type { OnRenderFn } from './component.public'; -import type { Props } from './jsx/jsx-runtime'; -import type { JSXOutput } from './jsx/types/jsx-node'; -import { isServerPlatform } from './platform/platform'; -import { type QRLInternal } from './qrl/qrl-class'; -import { SsrNodeFlags, type Container, type HostElement } from './types'; -import { ChoreType } from './util-chore-type'; -import { QScopedStyle } from './utils/markers'; -import { isPromise, maybeThen, retryOnPromise, safeCall } from './utils/promises'; -import { addComponentStylePrefix } from './utils/scoped-styles'; -import { serializeAttribute } from './utils/styles'; -import { type ValueOrPromise } from './utils/types'; -import { invoke, newInvokeContext } from '../use/use-core'; -import { findBlockingChore, findBlockingChoreForVisible } from './scheduler-rules'; -import { createNextTick } from './platform/next-tick'; -import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl'; -import { isSsrNode } from '../reactive-primitives/subscriber'; -import { logWarn } from './utils/log'; -import type { ElementVNode, VirtualVNode } from '../client/vnode-impl'; -import { ChoreArray, choreComparator } from '../client/chore-array'; -import { cleanupDestroyable } from '../use/utils/destroyable'; - -// Turn this on to get debug output of what the scheduler is doing. -const DEBUG: boolean = false; - -export enum ChoreState { - NONE = 0, - RUNNING = 1, - FAILED = 2, - DONE = 3, -} - -type ChoreReturnValue = T extends - | ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS - | ChoreType.WAIT_FOR_QUEUE - | ChoreType.NODE_PROP - ? void - : T extends ChoreType.NODE_DIFF | ChoreType.COMPONENT - ? JSXOutput - : unknown; - -export interface Chore { - $type$: T; - $idx$: number | string; - $host$: HostElement; - $target$: ChoreTarget | null; - $payload$: unknown; - $state$: ChoreState; - $blockedChores$: ChoreArray | null; - $startTime$: number | undefined; - $endTime$: number | undefined; - - $resolve$: ((value: any) => void) | undefined; - $reject$: ((reason?: any) => void) | undefined; - $returnValue$: ValueOrPromise>; -} - -export type Scheduler = ReturnType; - -type ChoreTarget = - | HostElement - | QRLInternal<(...args: unknown[]) => unknown> - | Signal - | StoreTarget; - -export const getChorePromise = (chore: Chore) => - chore.$state$ === ChoreState.NONE - ? (chore.$returnValue$ ||= new Promise((resolve, reject) => { - chore.$resolve$ = resolve; - chore.$reject$ = reject; - })) - : chore.$returnValue$; - -export const createScheduler = ( - container: Container, - journalFlush: () => void, - choreQueue: ChoreArray, - blockedChores: ChoreArray, - runningChores: Set -) => { - let drainChore: Chore | null = null; - let drainScheduled = false; - let isDraining = false; - let isJournalFlushRunning = false; - let flushBudgetStart = 0; - let currentTime = performance.now(); - const nextTick = createNextTick(drainChoreQueue); - let flushTimerId: number | null = null; - let blockingChoresCount = 0; - let currentChore: Chore | null = null; - - function drainInNextTick() { - if (!drainScheduled) { - drainScheduled = true; - nextTick(); - } - } - // Drain for ~16.67ms, then apply journal flush for ~16.67ms, then repeat - // We divide by 60 because we want to run at 60fps - const FREQUENCY_MS = Math.floor(1000 / 60); - - return schedule; - - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - - function schedule( - type: ChoreType.QRL_RESOLVE, - ignore: null, - target: ComputeQRL | AsyncComputeQRL - ): Chore; - function schedule(type: ChoreType.WAIT_FOR_QUEUE): Chore; - /** - * Schedule rendering of a component. - * - * @param type - * @param host - Host element where the component is being rendered. - * @param target - */ - function schedule( - type: ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - host: HostElement | undefined, - target: Signal | StoreTarget, - effects: Set | undefined - ): Chore; - function schedule( - type: ChoreType.TASK | ChoreType.VISIBLE, - task: Task - ): Chore; - function schedule( - type: ChoreType.RUN_QRL, - host: HostElement, - target: QRLInternal<(...args: unknown[]) => unknown>, - args: unknown[] - ): Chore; - function schedule( - type: ChoreType.COMPONENT, - host: HostElement, - qrl: QRLInternal>, - props: Props | null - ): Chore; - function schedule( - type: ChoreType.NODE_DIFF, - host: HostElement, - target: HostElement, - value: JSXOutput | Signal - ): Chore; - function schedule( - type: ChoreType.NODE_PROP, - host: HostElement, - prop: string, - value: any - ): Chore; - function schedule(type: ChoreType.CLEANUP_VISIBLE, task: Task): Chore; - ///// IMPLEMENTATION ///// - function schedule( - type: T, - hostOrTask: HostElement | Task | null = null, - targetOrQrl: ChoreTarget | string | null = null, - payload: any = null - ): Chore | null { - if (type === ChoreType.WAIT_FOR_QUEUE && drainChore) { - return drainChore as Chore; - } - - const isTask = - type === ChoreType.TASK || type === ChoreType.VISIBLE || type === ChoreType.CLEANUP_VISIBLE; - - if (isTask) { - (hostOrTask as Task).$flags$ |= TaskFlags.DIRTY; - } - - const chore: Chore = { - $type$: type, - $idx$: isTask - ? (hostOrTask as Task).$index$ - : typeof targetOrQrl === 'string' - ? targetOrQrl - : 0, - $host$: isTask ? (hostOrTask as Task).$el$ : (hostOrTask as HostElement), - $target$: targetOrQrl as ChoreTarget | null, - $payload$: isTask ? hostOrTask : payload, - $state$: ChoreState.NONE, - $blockedChores$: null, - $startTime$: undefined, - $endTime$: undefined, - $resolve$: undefined, - $reject$: undefined, - $returnValue$: null!, - }; - - if (type === ChoreType.WAIT_FOR_QUEUE) { - getChorePromise(chore); - drainChore = chore as Chore; - drainInNextTick(); - return chore; - } - - const isServer = isServerPlatform(); - const isClientOnly = type === ChoreType.NODE_DIFF || type === ChoreType.QRL_RESOLVE; - if (isServer && isClientOnly) { - DEBUG && - debugTrace( - `skip client chore ${debugChoreTypeToString(type)}`, - chore, - choreQueue, - blockedChores - ); - // Mark skipped client-only chores as completed on the server - finishChore(chore, undefined); - return chore; - } - - if (isServer && chore.$host$ && isSsrNode(chore.$host$)) { - const isUpdatable = !!(chore.$host$.flags & SsrNodeFlags.Updatable); - - if (!isUpdatable) { - if ( - // backpatching exceptions: - // - node prop is allowed because it is used to update the node property - // - recompute and schedule effects because it triggers effects (so node prop too) - chore.$type$ !== ChoreType.NODE_PROP && - chore.$type$ !== ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS - ) { - // We are running on the server. - // On server we can't schedule task for a different host! - // Server is SSR, and therefore scheduling for anything but the current host - // implies that things need to be re-run and that is not supported because of streaming. - const warningMessage = `A '${choreTypeToName( - chore.$type$ - )}' chore was scheduled on a host element that has already been streamed to the client. -This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR). - -Problematic chore: - - Type: ${choreTypeToName(chore.$type$)} - - Host: ${chore.$host$.toString()} - - Nearest element location: ${chore.$host$.currentFile} - -This is often caused by modifying a signal in an already rendered component during SSR.`; - logWarn(warningMessage); - DEBUG && - debugTrace('schedule.SKIPPED host is not updatable', chore, choreQueue, blockedChores); - // Decrement counter if this was a blocking chore that we're skipping - if (isRenderBlocking(type)) { - blockingChoresCount--; - } - return chore; - } - } - } - - const shouldBlock = - chore.$type$ !== ChoreType.QRL_RESOLVE && chore.$type$ !== ChoreType.RUN_QRL; - - if (shouldBlock) { - const runningChore = getRunningChore(chore); - if (runningChore) { - if (isResourceChore(runningChore)) { - addBlockedChore(chore, runningChore, blockedChores); - } - return chore; - } - const blockingChore = findBlockingChore( - chore, - choreQueue, - blockedChores, - runningChores, - container - ); - if (blockingChore) { - addBlockedChore(chore, blockingChore, blockedChores); - return chore; - } - } - - addChoreAndIncrementBlockingCounter(chore, choreQueue); - - DEBUG && - debugTrace( - isRenderBlocking(type) ? `schedule (blocking ${blockingChoresCount})` : 'schedule', - chore, - choreQueue, - blockedChores - ); - - const runImmediately = (isServer && type === ChoreType.COMPONENT) || type === ChoreType.RUN_QRL; - - if (runImmediately && !isDraining) { - immediateDrain(); - } else { - drainInNextTick(); - } - return chore; - } - - function immediateDrain() { - drainScheduled = true; - drainChoreQueue(); - } - - //////////////////////////////////////////////////////////////////////////////// - // Drain queue helpers - //////////////////////////////////////////////////////////////////////////////// - - function cancelFlushTimer() { - if (flushTimerId != null) { - clearTimeout(flushTimerId); - flushTimerId = null; - } - } - - function scheduleFlushTimer(): void { - const isServer = isServerPlatform(); - // Never schedule timers on the server - if (isServer) { - return; - } - // Ignore if a timer is already scheduled - if (flushTimerId != null) { - return; - } - - const now = performance.now(); - const elapsed = now - flushBudgetStart; - const delay = Math.max(0, FREQUENCY_MS - elapsed); - // Deadline already reached, flush now - if (delay === 0) { - if (!isDraining) { - applyJournalFlush(); - } - return; - } - - flushTimerId = setTimeout(() => { - flushTimerId = null; - - applyJournalFlush(); - }, delay) as unknown as number; - } - - function applyJournalFlush() { - if (blockingChoresCount > 0) { - DEBUG && - debugTrace( - `journalFlush.BLOCKED (${blockingChoresCount} blocking chores)`, - null, - choreQueue, - blockedChores - ); - return; - } - if (!isJournalFlushRunning) { - // prevent multiple journal flushes from running at the same time - isJournalFlushRunning = true; - journalFlush(); - isJournalFlushRunning = false; - flushBudgetStart = performance.now(); - cancelFlushTimer(); - DEBUG && debugTrace('journalFlush.DONE', null, choreQueue, blockedChores); - } - } - - function shouldApplyJournalFlush(isServer: boolean) { - return !isServer && currentTime - flushBudgetStart >= FREQUENCY_MS; - } - - function drainChoreQueue(): void { - const isServer = isServerPlatform(); - drainScheduled = false; - if (isDraining) { - return; - } - // early return if the queue is empty - if (!choreQueue.length) { - applyJournalFlush(); - if (drainChore && !runningChores.size) { - // resolve drainChore only if there are no running chores, because - // we are sure that we are done - drainChore.$resolve$!(null); - drainChore = null; - } - return; - } - isDraining = true; - flushBudgetStart = performance.now(); - cancelFlushTimer(); - - const maybeFinishDrain = () => { - if (choreQueue.length) { - drainInNextTick(); - return false; - } - if (drainChore && runningChores.size) { - if (shouldApplyJournalFlush(isServer)) { - // apply journal flush even if we are not finished draining the queue - applyJournalFlush(); - } - return false; - } - currentChore = null; - applyJournalFlush(); - drainChore?.$resolve$!(null); - drainChore = null; - DEBUG && debugTrace('drain.DONE', drainChore, choreQueue, blockedChores); - return true; - }; - - const scheduleBlockedChoresAndDrainIfNeeded = (chore: Chore) => { - let blockedChoresScheduled = false; - if (chore.$blockedChores$) { - for (const blockedChore of chore.$blockedChores$) { - const blockingChore = findBlockingChore( - blockedChore, - choreQueue, - blockedChores, - runningChores, - container - ); - if (blockingChore) { - // Chore is still blocked, move it to the new blocking chore's list - // Note: chore is already in blockedChores Set and vnode.blockedChores, - // so we only add to the new blocking chore's list - (blockingChore.$blockedChores$ ||= new ChoreArray()).add(blockedChore); - } else { - blockedChores.delete(blockedChore); - if (vnode_isVNode(blockedChore.$host$)) { - blockedChore.$host$.blockedChores?.delete(blockedChore); - } - addChoreAndIncrementBlockingCounter(blockedChore, choreQueue); - DEBUG && debugTrace('schedule.UNBLOCKED', blockedChore, choreQueue, blockedChores); - blockedChoresScheduled = true; - } - } - chore.$blockedChores$ = null; - } - if (blockedChoresScheduled && !isDraining) { - drainInNextTick(); - } - }; - - try { - while (choreQueue.length) { - currentTime = performance.now(); - const chore = (currentChore = choreQueue.shift()!); - if (chore.$state$ !== ChoreState.NONE) { - // Chore was already processed, counter already decremented in finishChore/handleError - continue; - } - - if ( - vNodeAlreadyDeleted(chore) && - // we need to process cleanup tasks for deleted nodes - chore.$type$ !== ChoreType.CLEANUP_VISIBLE - ) { - // skip deleted chore - DEBUG && debugTrace('skip chore', chore, choreQueue, blockedChores); - if (vnode_isVNode(chore.$host$)) { - chore.$host$.chores?.delete(chore); - } - // Decrement counter if this was a blocking chore that we're skipping - if (isRenderBlocking(chore.$type$)) { - blockingChoresCount--; - } - continue; - } - - if (chore.$type$ === ChoreType.VISIBLE) { - // ensure that the journal flush is applied before the visible chore is executed - // so that the visible chore can see the latest DOM changes - applyJournalFlush(); - const blockingChore = findBlockingChoreForVisible(chore, runningChores, container); - if (blockingChore && blockingChore.$state$ === ChoreState.RUNNING) { - (blockingChore.$blockedChores$ ||= new ChoreArray()).add(chore); - continue; - } - } - - // Note that this never throws - chore.$startTime$ = performance.now(); - const result = executeChore(chore, isServer); - chore.$returnValue$ = result; - if (isPromise(result)) { - runningChores.add(chore); - chore.$state$ = ChoreState.RUNNING; - - result - .then((value) => { - finishChore(chore, value); - }) - .catch((e) => { - if (chore.$state$ !== ChoreState.RUNNING) { - // we already handled an error but maybe it's a different one so we log it - console.error(e); - return; - } - handleError(chore, e); - }) - .finally(() => { - runningChores.delete(chore); - // Note that we ignore failed chores so the app keeps working - // TODO decide if this is ok and document it - scheduleBlockedChoresAndDrainIfNeeded(chore); - // If drainChore is not null, we are waiting for it to finish. - // If there are no running chores, we can finish the drain. - let finished = false; - if (drainChore && !runningChores.size) { - finished = maybeFinishDrain(); - } - if (!finished && !isDraining) { - scheduleFlushTimer(); - } - }); - } else { - finishChore(chore, result); - scheduleBlockedChoresAndDrainIfNeeded(chore); - } - - if (shouldApplyJournalFlush(isServer)) { - applyJournalFlush(); - drainInNextTick(); - return; - } - } - } catch (e) { - handleError(currentChore!, e); - scheduleBlockedChoresAndDrainIfNeeded(currentChore!); - } finally { - isDraining = false; - maybeFinishDrain(); - } - } - - function finishChore(chore: Chore, value: any) { - chore.$endTime$ = performance.now(); - chore.$state$ = ChoreState.DONE; - chore.$returnValue$ = value; - chore.$resolve$?.(value); - if (vnode_isVNode(chore.$host$)) { - chore.$host$.chores?.delete(chore); - } - - // Decrement blocking counter if this chore was blocking journal flush - if (isRenderBlocking(chore.$type$)) { - blockingChoresCount--; - DEBUG && - debugTrace( - `execute.DONE (blocking ${blockingChoresCount})`, - chore, - choreQueue, - blockedChores - ); - } else { - DEBUG && debugTrace('execute.DONE', chore, choreQueue, blockedChores); - } - } - - function handleError(chore: Chore, e: any) { - chore.$endTime$ = performance.now(); - chore.$state$ = ChoreState.FAILED; - - // Decrement blocking counter if this chore was blocking journal flush - if (isRenderBlocking(chore.$type$)) { - blockingChoresCount--; - DEBUG && - debugTrace( - `execute.ERROR (blocking ${blockingChoresCount})`, - chore, - choreQueue, - blockedChores - ); - } else { - DEBUG && debugTrace('execute.ERROR', chore, choreQueue, blockedChores); - } - - // If we used the result as promise, this won't exist - chore.$reject$?.(e); - container.handleError(e, chore.$host$); - } - - function executeChore( - chore: Chore, - isServer: boolean - ): ValueOrPromise> { - const host = chore.$host$; - DEBUG && debugTrace('execute', chore, choreQueue, blockedChores); - let returnValue: ValueOrPromise>; - switch (chore.$type$) { - case ChoreType.COMPONENT: - { - returnValue = safeCall( - () => - executeComponent( - container, - host, - host, - chore.$target$ as QRLInternal>, - chore.$payload$ as Props | null - ), - (jsx) => { - if (isServer) { - return jsx; - } else { - const styleScopedId = container.getHostProp(host, QScopedStyle); - return retryOnPromise(() => - vnode_diff( - container as ClientContainer, - jsx, - host as VirtualVNode, - addComponentStylePrefix(styleScopedId) - ) - ); - } - }, - (err: any) => { - handleError(chore, err); - } - ) as ValueOrPromise>; - } - break; - case ChoreType.RUN_QRL: - { - const fn = (chore.$target$ as QRLInternal<(...args: unknown[]) => unknown>).getFn(); - returnValue = retryOnPromise(() => - fn(...(chore.$payload$ as unknown[])) - ) as ValueOrPromise>; - } - break; - case ChoreType.TASK: - case ChoreType.VISIBLE: - { - const payload = chore.$payload$ as DescriptorBase; - if (payload.$flags$ & TaskFlags.RESOURCE) { - returnValue = runResource( - payload as ResourceDescriptor, - container, - host - ) as ValueOrPromise>; - } else { - const task = payload as Task; - returnValue = runTask(task, container, host) as ValueOrPromise< - ChoreReturnValue - >; - if (task.$flags$ & TaskFlags.RENDER_BLOCKING) { - blockingChoresCount++; - returnValue = maybeThen(returnValue, () => { - blockingChoresCount--; - }); - } - } - } - break; - case ChoreType.CLEANUP_VISIBLE: - { - const task = chore.$payload$ as Task; - cleanupDestroyable(task); - } - break; - case ChoreType.NODE_DIFF: - { - const parentVirtualNode = chore.$target$ as VirtualVNode; - let jsx = chore.$payload$ as JSXOutput; - if (isSignal(jsx)) { - jsx = jsx.value as any; - } - returnValue = retryOnPromise(() => - vnode_diff(container as DomContainer, jsx, parentVirtualNode, null) - ) as ValueOrPromise>; - } - break; - case ChoreType.NODE_PROP: - { - const virtualNode = chore.$host$ as unknown as ElementVNode; - const payload = chore.$payload$ as NodePropPayload; - let value: Signal | string = payload.$value$; - if (isSignal(value)) { - value = value.value as any; - } - const isConst = payload.$isConst$; - const journal = (container as DomContainer).$journal$; - const property = chore.$idx$ as string; - const serializedValue = serializeAttribute( - property, - value, - payload.$scopedStyleIdPrefix$ - ); - if (isServer) { - (container as SSRContainer).addBackpatchEntry( - (chore.$host$ as ISsrNode).id, - property, - serializedValue - ); - returnValue = null; - } else { - if (isConst) { - const element = virtualNode.element; - journal.push(VNodeJournalOpCode.SetAttribute, element, property, serializedValue); - } else { - virtualNode.setAttr(property, serializedValue, journal); - } - returnValue = undefined as ValueOrPromise>; - } - } - break; - case ChoreType.QRL_RESOLVE: { - { - const target = chore.$target$ as QRLInternal; - returnValue = (!target.resolved ? target.resolve() : null) as ValueOrPromise< - ChoreReturnValue - >; - } - break; - } - case ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS: { - { - const target = chore.$target$ as ComputedSignalImpl | WrappedSignalImpl; - - const effects = chore.$payload$ as Set; - if (!effects?.size) { - break; - } - - let shouldCompute = - target instanceof ComputedSignalImpl || target instanceof WrappedSignalImpl; - - // for .error and .loading effects - if (target instanceof AsyncComputedSignalImpl && effects !== target.$effects$) { - shouldCompute = false; - } - - if (shouldCompute) { - const ctx = newInvokeContext(); - ctx.$container$ = container; - // needed for computed signals and throwing QRLs - returnValue = maybeThen( - retryOnPromise(() => - invoke.call(target, ctx, (target as ComputedSignalImpl).$computeIfNeeded$) - ), - () => { - if ((target as ComputedSignalImpl).$flags$ & SignalFlags.RUN_EFFECTS) { - (target as ComputedSignalImpl).$flags$ &= ~SignalFlags.RUN_EFFECTS; - return retryOnPromise(() => scheduleEffects(container, target, effects)); - } - } - ) as ValueOrPromise>; - } else { - returnValue = retryOnPromise(() => { - scheduleEffects(container, target, effects); - }) as ValueOrPromise>; - } - } - break; - } - } - return returnValue as any; - } - - function getRunningChore(chore: Chore): Chore | null { - if (runningChores.size) { - // 1.1. Check if the chore is already running. - for (const runningChore of runningChores) { - const comp = choreComparator(chore, runningChore); - if (comp === 0) { - return runningChore; - } - } - } - return null; - } - - function addChoreAndIncrementBlockingCounter(chore: Chore, choreArray: ChoreArray) { - if (addChore(chore, choreArray)) { - blockingChoresCount++; - } - } -}; - -export function addChore(chore: Chore, choreArray: ChoreArray): boolean { - const idx = choreArray.add(chore); - if (idx < 0) { - if (vnode_isVNode(chore.$host$)) { - (chore.$host$.chores ||= new ChoreArray()).add(chore); - } - return isRenderBlocking(chore.$type$); - } - return false; -} - -function vNodeAlreadyDeleted(chore: Chore): boolean { - return !!(chore.$host$ && vnode_isVNode(chore.$host$) && chore.$host$.flags & VNodeFlags.Deleted); -} - -function isResourceChore(chore: Chore): boolean { - return ( - chore.$type$ === ChoreType.TASK && - !!chore.$payload$ && - !!((chore.$payload$ as Task).$flags$ & TaskFlags.RESOURCE) - ); -} - -export function addBlockedChore( - blockedChore: Chore, - blockingChore: Chore, - blockedChores: ChoreArray -): void { - if (!isResourceChore(blockedChore) && choreComparator(blockedChore, blockingChore) === 0) { - return; - } - DEBUG && - debugTrace( - `blocked chore by ${debugChoreToString(blockingChore)}`, - blockedChore, - undefined, - blockedChores - ); - (blockingChore.$blockedChores$ ||= new ChoreArray()).add(blockedChore); - blockedChores.add(blockedChore); - if (vnode_isVNode(blockedChore.$host$)) { - (blockedChore.$host$.blockedChores ||= new ChoreArray()).add(blockedChore); - } -} - -function isRenderBlocking(type: ChoreType): boolean { - return type === ChoreType.NODE_DIFF || type === ChoreType.COMPONENT; -} - -function choreTypeToName(type: ChoreType): string { - return ( - ( - { - [ChoreType.QRL_RESOLVE]: 'Resolve QRL', - [ChoreType.RUN_QRL]: 'Run QRL', - [ChoreType.TASK]: 'Task', - [ChoreType.NODE_DIFF]: 'Changes diffing', - [ChoreType.NODE_PROP]: 'Updating node property', - [ChoreType.COMPONENT]: 'Component', - [ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS]: 'Signal recompute', - [ChoreType.VISIBLE]: 'Visible', - [ChoreType.CLEANUP_VISIBLE]: 'Cleanup visible', - [ChoreType.WAIT_FOR_QUEUE]: 'Wait for queue', - } as Record - )[type] || 'Unknown: ' + type - ); -} - -function debugChoreTypeToString(type: ChoreType): string { - return ( - ( - { - [ChoreType.QRL_RESOLVE]: 'QRL_RESOLVE', - [ChoreType.RUN_QRL]: 'RUN_QRL', - [ChoreType.TASK]: 'TASK', - [ChoreType.NODE_DIFF]: 'NODE_DIFF', - [ChoreType.NODE_PROP]: 'NODE_PROP', - [ChoreType.COMPONENT]: 'COMPONENT', - [ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS]: 'RECOMPUTE_SIGNAL', - [ChoreType.VISIBLE]: 'VISIBLE', - [ChoreType.CLEANUP_VISIBLE]: 'CLEANUP_VISIBLE', - [ChoreType.WAIT_FOR_QUEUE]: 'WAIT_FOR_QUEUE', - } as Record - )[type] || 'UNKNOWN: ' + type - ); -} - -function debugChoreToString(chore: Chore): string { - const type = debugChoreTypeToString(chore.$type$); - const state = chore.$state$ ? `[${ChoreState[chore.$state$]}] ` : ''; - const host = String(chore.$host$).replaceAll(/\n.*/gim, ''); - const qrlTarget = (chore.$target$ as QRLInternal)?.$symbol$; - return `${state}Chore(${type} ${chore.$type$ === ChoreType.QRL_RESOLVE || chore.$type$ === ChoreType.RUN_QRL ? qrlTarget : host} ${chore.$idx$})`; -} - -function debugTrace( - action: string, - arg?: any | null, - queue?: ChoreArray, - blockedChores?: ChoreArray -) { - const lines: string[] = []; - - // Header - lines.push(`Scheduler: ${action}`); - - // Argument section - if (arg) { - lines.push(''); - if (arg && '$type$' in arg) { - const chore = arg as Chore; - const type = debugChoreTypeToString(chore.$type$); - const host = String(chore.$host$).replaceAll(/\n.*/gim, ''); - const qrlTarget = (chore.$target$ as QRLInternal)?.$symbol$; - const targetOrHost = - chore.$type$ === ChoreType.QRL_RESOLVE || chore.$type$ === ChoreType.RUN_QRL - ? qrlTarget - : host; - - lines.push(`🎯 Current Chore:`); - lines.push(` Type: ${type}`); - lines.push(` Host: ${targetOrHost}`); - - // Show execution time if available - if (chore.$startTime$ && chore.$endTime$) { - const executionTime = chore.$endTime$ - chore.$startTime$; - lines.push(` Time: ${executionTime.toFixed(2)}ms`); - } else if (chore.$startTime$) { - const elapsedTime = performance.now() - chore.$startTime$; - lines.push(` Time: ${elapsedTime.toFixed(2)}ms (running)`); - } - - // Show blocked chores for this chore - if (chore.$blockedChores$ && chore.$blockedChores$.length > 0) { - lines.push(` ⛔ Blocked Chores:`); - chore.$blockedChores$.forEach((blockedChore, index) => { - const blockedType = debugChoreTypeToString(blockedChore.$type$); - const blockedTarget = String(blockedChore.$host$).replaceAll(/\n.*/gim, ''); - lines.push(` ${index + 1}. ${blockedType} ${blockedTarget} ${blockedChore.$idx$}`); - }); - } - } else { - lines.push(`📝 Argument: ${String(arg).replaceAll(/\n.*/gim, '')}`); - } - } - - // Queue section - if (queue && queue.length > 0) { - lines.push(''); - lines.push(`📋 Queue (${queue.length} items):`); - - for (let i = 0; i < queue.length; i++) { - const chore = queue[i]; - const isActive = chore === arg; - const activeMarker = isActive ? `▶ ` : ' '; - const type = debugChoreTypeToString(chore.$type$); - const state = chore.$state$ ? `[${ChoreState[chore.$state$]}]` : ''; - const host = String(chore.$host$).replaceAll(/\n.*/gim, ''); - const qrlTarget = (chore.$target$ as QRLInternal)?.$symbol$; - const target = - chore.$type$ === ChoreType.QRL_RESOLVE || chore.$type$ === ChoreType.RUN_QRL - ? qrlTarget - : host; - const line = `${activeMarker}${state} ${type} ${target} ${chore.$idx$}`; - lines.push(line); - } - } - - // Blocked chores section - if (blockedChores && blockedChores.length > 0) { - lines.push(''); - lines.push(`🚫 Blocked Chores (${blockedChores.length} items):`); - - Array.from(blockedChores).forEach((chore, index) => { - const type = debugChoreTypeToString(chore.$type$); - const host = String(chore.$host$).replaceAll(/\n.*/gim, ''); - const qrlTarget = (chore.$target$ as QRLInternal)?.$symbol$; - const target = - chore.$type$ === ChoreType.QRL_RESOLVE || chore.$type$ === ChoreType.RUN_QRL - ? qrlTarget - : host; - - lines.push(` ${index + 1}. ${type} ${target} ${chore.$idx$}`); - }); - } - - // Footer - lines.push(''); - lines.push('─'.repeat(60)); - - // eslint-disable-next-line no-console - console.log(lines.join('\n') + '\n'); -} diff --git a/packages/qwik/src/core/shared/serdes/allocate.ts b/packages/qwik/src/core/shared/serdes/allocate.ts index 82b2d649603..d5a936b80be 100644 --- a/packages/qwik/src/core/shared/serdes/allocate.ts +++ b/packages/qwik/src/core/shared/serdes/allocate.ts @@ -1,5 +1,10 @@ import type { DomContainer } from '../../client/dom-container'; -import { ensureMaterialized, vnode_getNode, vnode_isVNode, vnode_locate } from '../../client/vnode'; +import { + ensureMaterialized, + vnode_getNode, + vnode_isVNode, + vnode_locate, +} from '../../client/vnode-utils'; import { AsyncComputedSignalImpl } from '../../reactive-primitives/impl/async-computed-signal-impl'; import { ComputedSignalImpl } from '../../reactive-primitives/impl/computed-signal-impl'; import { SerializerSignalImpl } from '../../reactive-primitives/impl/serializer-signal-impl'; diff --git a/packages/qwik/src/core/shared/serdes/deser-proxy.ts b/packages/qwik/src/core/shared/serdes/deser-proxy.ts index 05f25593d20..7f8081d3bd1 100644 --- a/packages/qwik/src/core/shared/serdes/deser-proxy.ts +++ b/packages/qwik/src/core/shared/serdes/deser-proxy.ts @@ -1,6 +1,6 @@ import { TypeIds } from './constants'; import type { DomContainer } from '../../client/dom-container'; -import { vnode_isVNode } from '../../client/vnode'; +import { vnode_isVNode } from '../../client/vnode-utils'; import { isObject } from '../utils/types'; import { allocate } from './allocate'; import { inflate } from './inflate'; diff --git a/packages/qwik/src/core/shared/serdes/dump-state.ts b/packages/qwik/src/core/shared/serdes/dump-state.ts index d944eb9525a..884ac54570e 100644 --- a/packages/qwik/src/core/shared/serdes/dump-state.ts +++ b/packages/qwik/src/core/shared/serdes/dump-state.ts @@ -1,4 +1,4 @@ -import { vnode_isVNode, vnode_toString } from '../../client/vnode'; +import { vnode_isVNode, vnode_toString } from '../../client/vnode-utils'; import { isObject } from '../utils/types'; import { type Constants, TypeIds, _typeIdNames, _constantNames } from './constants'; diff --git a/packages/qwik/src/core/shared/serdes/inflate.ts b/packages/qwik/src/core/shared/serdes/inflate.ts index 93da253c069..de2994228b0 100644 --- a/packages/qwik/src/core/shared/serdes/inflate.ts +++ b/packages/qwik/src/core/shared/serdes/inflate.ts @@ -40,7 +40,7 @@ import { vnode_getText, vnode_isTextVNode, vnode_isVNode, -} from '../../client/vnode'; +} from '../../client/vnode-utils'; import { isString } from '../utils/types'; import type { VirtualVNode } from '../vnode/virtual-vnode'; diff --git a/packages/qwik/src/core/shared/serdes/inflate.unit.ts b/packages/qwik/src/core/shared/serdes/inflate.unit.ts index 38e5c118bf2..0dae3978318 100644 --- a/packages/qwik/src/core/shared/serdes/inflate.unit.ts +++ b/packages/qwik/src/core/shared/serdes/inflate.unit.ts @@ -3,7 +3,7 @@ import { NEEDS_COMPUTATION, EffectProperty } from '../../reactive-primitives/typ import { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl'; import { VNodeFlags } from '../../client/types'; import { inflateWrappedSignalValue } from './inflate'; -import { vnode_newElement, vnode_newText, vnode_setProp } from '../../client/vnode'; +import { vnode_setProp } from '../../client/vnode-utils'; import { ElementVNode } from '../vnode/element-vnode'; import { TextVNode } from '../vnode/text-vnode'; import { VirtualVNode } from '../vnode/virtual-vnode'; diff --git a/packages/qwik/src/core/shared/serdes/serialize.ts b/packages/qwik/src/core/shared/serdes/serialize.ts index 956aa094273..348fe4dbe4a 100644 --- a/packages/qwik/src/core/shared/serdes/serialize.ts +++ b/packages/qwik/src/core/shared/serdes/serialize.ts @@ -1,7 +1,7 @@ import { isDev } from '@qwik.dev/core/build'; import { VNodeDataFlag } from 'packages/qwik/src/server/types'; import type { VNodeData } from 'packages/qwik/src/server/vnode-data'; -import { vnode_isVNode } from '../../client/vnode'; +import { vnode_isVNode } from '../../client/vnode-utils'; import { _EFFECT_BACK_REF } from '../../internal'; import { AsyncComputedSignalImpl } from '../../reactive-primitives/impl/async-computed-signal-impl'; import { ComputedSignalImpl } from '../../reactive-primitives/impl/computed-signal-impl'; diff --git a/packages/qwik/src/core/shared/util-chore-type.ts b/packages/qwik/src/core/shared/util-chore-type.ts deleted file mode 100644 index 890869eb57e..00000000000 --- a/packages/qwik/src/core/shared/util-chore-type.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const enum ChoreType { - /// MASKS defining three levels of sorting - MACRO /* **************************** */ = 240, - /* order of elements (not encoded here) */ - MICRO /* **************************** */ = 15, - - /** Ensure that the QRL promise is resolved before processing next chores in the queue */ - QRL_RESOLVE /* ********************** */ = 1, - RUN_QRL, - TASK, - NODE_DIFF, - NODE_PROP, - COMPONENT, - RECOMPUTE_AND_SCHEDULE_EFFECTS, - // Next macro level - VISIBLE /* ************************** */ = 16, - // Next macro level - CLEANUP_VISIBLE /* ****************** */ = 32, - // Next macro level - WAIT_FOR_QUEUE /* ********************** */ = 255, -} diff --git a/packages/qwik/src/core/shared/vnode/element-vnode.ts b/packages/qwik/src/core/shared/vnode/element-vnode.ts index f62bd718285..5aec4c58c38 100644 --- a/packages/qwik/src/core/shared/vnode/element-vnode.ts +++ b/packages/qwik/src/core/shared/vnode/element-vnode.ts @@ -2,6 +2,7 @@ import type { VNodeFlags } from '../../client/types'; import type { Props } from '../jsx/jsx-runtime'; import { VNode } from './vnode'; +/** @internal */ export class ElementVNode extends VNode { constructor( public key: string | null, diff --git a/packages/qwik/src/core/shared/vnode/ssr-vnode.ts b/packages/qwik/src/core/shared/vnode/ssr-vnode.ts deleted file mode 100644 index 739763e6a81..00000000000 --- a/packages/qwik/src/core/shared/vnode/ssr-vnode.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 index 047554aa5cb..55dc939d77b 100644 --- a/packages/qwik/src/core/shared/vnode/text-vnode.ts +++ b/packages/qwik/src/core/shared/vnode/text-vnode.ts @@ -2,6 +2,7 @@ import type { VNodeFlags } from '../../client/types'; import type { Props } from '../jsx/jsx-runtime'; import { VNode } from './vnode'; +/** @internal */ export class TextVNode extends VNode { constructor( flags: VNodeFlags, diff --git a/packages/qwik/src/core/shared/vnode/virtual-vnode.ts b/packages/qwik/src/core/shared/vnode/virtual-vnode.ts index 95bb4de7c7b..835c1247b22 100644 --- a/packages/qwik/src/core/shared/vnode/virtual-vnode.ts +++ b/packages/qwik/src/core/shared/vnode/virtual-vnode.ts @@ -3,6 +3,7 @@ import type { Props } from '../jsx/jsx-runtime'; import type { ElementVNode } from './element-vnode'; import { VNode } from './vnode'; +/** @internal */ export class VirtualVNode extends VNode { constructor( public key: string | null, diff --git a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts index 59ae55ea090..65dddbf46c0 100644 --- a/packages/qwik/src/core/shared/vnode/vnode-dirty.ts +++ b/packages/qwik/src/core/shared/vnode/vnode-dirty.ts @@ -1,4 +1,4 @@ -import type { VNodeJournal } from '../../client/vnode'; +import type { VNodeJournal } from '../../client/vnode-utils'; import type { ISsrNode, SSRContainer } from '../../ssr/ssr-types'; import { addCursor, findCursor, isCursor } from '../cursor/cursor'; import { getCursorData, type CursorData } from '../cursor/cursor-props'; diff --git a/packages/qwik/src/core/shared/vnode/vnode.ts b/packages/qwik/src/core/shared/vnode/vnode.ts index 5639ba0ab2f..5c162e62f6a 100644 --- a/packages/qwik/src/core/shared/vnode/vnode.ts +++ b/packages/qwik/src/core/shared/vnode/vnode.ts @@ -1,10 +1,11 @@ -import { isDev } from '@qwik.dev/core'; import type { VNodeFlags } from '../../client/types'; -import { vnode_toString } from '../../client/vnode'; +import { vnode_toString } from '../../client/vnode-utils'; import type { Props } from '../jsx/jsx-runtime'; import { ChoreBits } from './enums/chore-bits.enum'; import { BackRef } from '../../reactive-primitives/backref'; +import { isDev } from '@qwik.dev/core/build'; +/** @internal */ export abstract class VNode extends BackRef { slotParent: VNode | null = null; dirty: ChoreBits = ChoreBits.NONE; diff --git a/packages/qwik/src/core/tests/client-render.spec.tsx b/packages/qwik/src/core/tests/client-render.spec.tsx index fbfd4b34262..479db8f4feb 100644 --- a/packages/qwik/src/core/tests/client-render.spec.tsx +++ b/packages/qwik/src/core/tests/client-render.spec.tsx @@ -11,7 +11,7 @@ import { } from '@qwik.dev/core'; import '@qwik.dev/core/testing'; import { describe, expect, it } from 'vitest'; -import { vnode_getFirstChild } from '../client/vnode'; +import { vnode_getFirstChild } from '../client/vnode-utils'; import { createDocument } from '@qwik.dev/dom'; import { getTestPlatform, trigger } from '@qwik.dev/core/testing'; import type { _ContainerElement } from '@qwik.dev/core/internal'; diff --git a/packages/qwik/src/core/tests/component.spec.tsx b/packages/qwik/src/core/tests/component.spec.tsx index 8d749d5b419..2b75493cbf3 100644 --- a/packages/qwik/src/core/tests/component.spec.tsx +++ b/packages/qwik/src/core/tests/component.spec.tsx @@ -30,7 +30,7 @@ import { ErrorProvider } from '../../testing/rendering.unit-util'; import * as qError from '../shared/error/error'; import { QContainerValue } from '../shared/types'; import { ELEMENT_PROPS, OnRenderProp, QContainerAttr } from '../shared/utils/markers'; -import { vnode_getProp, vnode_locate } from '../client/vnode'; +import { vnode_getProp, vnode_locate } from '../client/vnode-utils'; import type { PropsProxy } from '../shared/jsx/props-proxy'; import { _PROPS_HANDLER } from '../shared/utils/constants'; diff --git a/packages/qwik/src/core/tests/container.spec.tsx b/packages/qwik/src/core/tests/container.spec.tsx index 13787517252..a3c6f9ca818 100644 --- a/packages/qwik/src/core/tests/container.spec.tsx +++ b/packages/qwik/src/core/tests/container.spec.tsx @@ -11,7 +11,7 @@ import { vnode_getFirstChild, vnode_getProp, vnode_getText, -} from '../client/vnode'; +} from '../client/vnode-utils'; import { createComputed$, createSignal } from '../reactive-primitives/signal.public'; import { SignalFlags } from '../reactive-primitives/types'; import { SERIALIZABLE_STATE, component$ } from '../shared/component.public'; diff --git a/packages/qwik/src/core/tests/projection.spec.tsx b/packages/qwik/src/core/tests/projection.spec.tsx index cbdadfd97f9..aa0dead1c7e 100644 --- a/packages/qwik/src/core/tests/projection.spec.tsx +++ b/packages/qwik/src/core/tests/projection.spec.tsx @@ -21,7 +21,7 @@ import { import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing'; import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture'; import { beforeEach, describe, expect, it } from 'vitest'; -import { vnode_getProp, vnode_locate } from '../client/vnode'; +import { vnode_getProp, vnode_locate } from '../client/vnode-utils'; import { HTML_NS, QContainerAttr, SVG_NS } from '../shared/utils/markers'; import { QContainerValue } from '../shared/types'; diff --git a/packages/qwik/src/core/tests/render-api.spec.tsx b/packages/qwik/src/core/tests/render-api.spec.tsx index 77d8aadd9c9..648ef7c3d39 100644 --- a/packages/qwik/src/core/tests/render-api.spec.tsx +++ b/packages/qwik/src/core/tests/render-api.spec.tsx @@ -44,7 +44,7 @@ import { type _ContainerElement, type _DomContainer, } from '../internal'; -import { vnode_getFirstChild } from '../client/vnode'; +import { vnode_getFirstChild } from '../client/vnode-utils'; import { QContainerValue } from '../shared/types'; import { QContainerAttr } from '../shared/utils/markers'; diff --git a/packages/qwik/src/core/tests/use-signal.spec.tsx b/packages/qwik/src/core/tests/use-signal.spec.tsx index e3808ca3eaf..a98aefa56bb 100644 --- a/packages/qwik/src/core/tests/use-signal.spec.tsx +++ b/packages/qwik/src/core/tests/use-signal.spec.tsx @@ -10,7 +10,7 @@ import { describe, expect, it } from 'vitest'; import { trigger, domRender, ssrRenderToDom } from '@qwik.dev/core/testing'; import { component$, Slot, type Signal as SignalType, untrack, useSignal } from '@qwik.dev/core'; import { _EFFECT_BACK_REF } from '@qwik.dev/core/internal'; -import { vnode_getFirstChild, vnode_locate } from '../client/vnode'; +import { vnode_getFirstChild, vnode_locate } from '../client/vnode-utils'; import { EffectSubscriptionProp } from '../reactive-primitives/types'; const debug = false; //true; diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index 3d1c55539a1..13ecd2619b5 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -8,7 +8,12 @@ import { seal } from '../shared/utils/qdev'; import { isArray, isObject } from '../shared/utils/types'; 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 { + vnode_getNode, + vnode_isElementVNode, + vnode_isVNode, + vnode_locate, +} from '../client/vnode-utils'; import { _getQContainerElement, getDomContainer } from '../client/dom-container'; import { type ClientContainer } from '../client/types'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; diff --git a/packages/qwik/src/server/qwik-copy.ts b/packages/qwik/src/server/qwik-copy.ts index 21b6e891875..8b99c1a4b44 100644 --- a/packages/qwik/src/server/qwik-copy.ts +++ b/packages/qwik/src/server/qwik-copy.ts @@ -20,7 +20,6 @@ export { } from '../core/client/util-mapArray'; export { QError, qError } from '../core/shared/error/error'; export { SYNC_QRL } from '../core/shared/qrl/qrl-utils'; -export { ChoreType } from '../core/shared/util-chore-type'; export { DEBUG_TYPE, QContainerValue, VirtualType } from '../core/shared/types'; export { escapeHTML } from '../core/shared/utils/character-escaping'; export { diff --git a/packages/qwik/src/server/vnode-data.unit.tsx b/packages/qwik/src/server/vnode-data.unit.tsx index a0e5ee2b496..25c7f8e8f4c 100644 --- a/packages/qwik/src/server/vnode-data.unit.tsx +++ b/packages/qwik/src/server/vnode-data.unit.tsx @@ -4,7 +4,7 @@ import { inlinedQrl } from '../core/shared/qrl/qrl'; import { useSignal } from '../core/use/use-signal'; import { ssrRenderToDom } from '../testing/rendering.unit-util'; import { encodeAsAlphanumeric } from './vnode-data'; -import { vnode_getProp, vnode_locate } from '../core/client/vnode'; +import { vnode_getProp, vnode_locate } from '../core/client/vnode-utils'; import { ELEMENT_PROPS, OnRenderProp } from '../core/shared/utils/markers'; import { type QRLInternal } from '../core/shared/qrl/qrl-class'; import type { DomContainer } from '../core/client/dom-container'; diff --git a/packages/qwik/src/testing/rendering.unit-util.tsx b/packages/qwik/src/testing/rendering.unit-util.tsx index 34c82700930..35673eb575f 100644 --- a/packages/qwik/src/testing/rendering.unit-util.tsx +++ b/packages/qwik/src/testing/rendering.unit-util.tsx @@ -26,7 +26,7 @@ import { vnode_remove, vnode_setProp, vnode_toString, -} from '../core/client/vnode'; +} from '../core/client/vnode-utils'; import { ERROR_CONTEXT } from '../core/shared/error/error-handling'; import { getPlatform, setPlatform } from '../core/shared/platform/platform'; import { _dumpState, preprocessState } from '../core/shared/serdes/index'; diff --git a/packages/qwik/src/testing/vdom-diff.unit-util.ts b/packages/qwik/src/testing/vdom-diff.unit-util.ts index d4228e90d75..f80026661fb 100644 --- a/packages/qwik/src/testing/vdom-diff.unit-util.ts +++ b/packages/qwik/src/testing/vdom-diff.unit-util.ts @@ -31,7 +31,7 @@ import { vnode_newVirtual, vnode_setAttr, vnode_setProp, -} from '../core/client/vnode'; +} from '../core/client/vnode-utils'; import { format } from 'prettier'; import { serializeBooleanOrNumberAttribute } from '../core/shared/utils/styles'; From 96e6277a40f515005603dd9219ccc06204469694 Mon Sep 17 00:00:00 2001 From: Varixo Date: Tue, 16 Dec 2025 17:57:41 +0100 Subject: [PATCH 38/38] feat(cursors): fix signals e2e tests --- .../e2e/src/components/signals/signals.tsx | 205 +++++++++--------- starters/e2e/signals.e2e.ts | 1 + 2 files changed, 107 insertions(+), 99 deletions(-) diff --git a/starters/apps/e2e/src/components/signals/signals.tsx b/starters/apps/e2e/src/components/signals/signals.tsx index 1f378c7621c..8d087116703 100644 --- a/starters/apps/e2e/src/components/signals/signals.tsx +++ b/starters/apps/e2e/src/components/signals/signals.tsx @@ -5,6 +5,7 @@ import { createComputed$, createSignal, isBrowser, + untrack, useComputed$, useConstant, useResource$, @@ -37,111 +38,117 @@ export const Signals = component$(() => { Rerender Renders: {rerender.value} - + rerender.value)} + rerenderCount={rerender.value} + /> ); }); -export const SignalsChildren = component$(() => { - const ref = useSignal(); - const ref2 = useSignal(); - const id = useSignal(0); - const signal = useSignal(""); - const renders = useStore( - { - count: 0, - }, - { reactive: false }, - ); - const store = useStore({ - foo: 10, - attribute: "even", - signal, - }); +export const SignalsChildren = component$<{ rerenderCount: number }>( + ({ rerenderCount }) => { + const ref = useSignal(); + const ref2 = useSignal(); + const id = useSignal(0); + const signal = useSignal(""); + const renders = useStore( + { + count: 0, + }, + { reactive: false }, + ); + const store = useStore({ + foo: 10, + attribute: "even", + signal, + }); - const styles = useSignal("body { background: white}"); + const styles = useSignal("body { background: white}"); - useVisibleTask$(() => { - ref.value!.setAttribute("data-set", "ref"); - ref2.value!.setAttribute("data-set", "ref2"); - }); + useVisibleTask$(() => { + ref.value!.setAttribute("data-set", "ref"); + ref2.value!.setAttribute("data-set", "ref2"); + }); - renders.count++; - const rerenders = renders.count + 0; - return ( -
- - - - -
Parent renders: {rerenders}
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ); -}); + renders.count++; + const rerenders = renders.count + 0; + return ( +
+ + + + +
Parent renders: {rerenders}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + {rerenderCount} +
+ ); + }, +); interface ChildProps { count: number; diff --git a/starters/e2e/signals.e2e.ts b/starters/e2e/signals.e2e.ts index 6af0b5de4fe..b56c363f162 100644 --- a/starters/e2e/signals.e2e.ts +++ b/starters/e2e/signals.e2e.ts @@ -578,6 +578,7 @@ test.describe("signals", () => { const toggleRender = page.locator("#rerender"); await toggleRender.click(); await expect(page.locator("#rerender-count")).toHaveText("Renders: 1"); + await expect(page.locator("#rerender-check")).toHaveText("1"); }); tests(); });