Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0d70d3d
chore(core): _getQContainerElement only on element
wmertens Nov 26, 2025
1ee27e5
chore(core): _run should not wait
wmertens Nov 26, 2025
04746ad
chore(serdes): name SerializationBackRef
wmertens Nov 26, 2025
9f7ca25
refactor(core): ignore className
wmertens Nov 26, 2025
78e966c
wip cursors scheduling
wmertens Nov 26, 2025
c366b10
more logging
wmertens Nov 26, 2025
a1f7314
feat(cursors): pass container to vnode functions
Varixo Nov 26, 2025
6b0a632
chore(types): add event handler type tests
wmertens Nov 28, 2025
7128b3f
feat(cursors): migrate to journal
Varixo Nov 29, 2025
233e581
feat(cursors): handle changing component props
Varixo Nov 29, 2025
1bd1df9
feat(cursors): fix some promises cases
Varixo Nov 29, 2025
ac95674
feat(cursors): compute chore impl
Varixo Nov 30, 2025
41397e6
feat(cursors): fix apply journal
Varixo Nov 30, 2025
62afb5a
feat(cursors): fix handling errors
Varixo Nov 30, 2025
91d1ec5
feat(cursors): migrate to single cursor prop
Varixo Dec 1, 2025
e2d539d
fix(cursors): setting falsy prop
Varixo Dec 2, 2025
c724518
fix(cursors): vnode_diff unit tests
wmertens Dec 3, 2025
ed3a716
fix(cursors): merge cursors during vnode-diff and fix debug to string
Varixo Dec 4, 2025
f680f03
feat(cursors): initialize render context with waitOn
Varixo Dec 5, 2025
97be9d8
feat(cursors): propogate dirty flag to blocking cursor
Varixo Dec 7, 2025
75ddaa7
feat(cursors): fix async computed signals
Varixo Dec 8, 2025
13e0be4
feat(cursor): process dirty non-projection vnodes first
Varixo Dec 8, 2025
46e10e2
feat(cursors): dont execute qrls for deleted vnodes
Varixo Dec 8, 2025
ddf34f6
feat(cursors): fix resource tests
Varixo Dec 8, 2025
f36b102
feat(cursors): fix visible tasks
Varixo Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 52 additions & 53 deletions packages/qwik/src/core/client/dom-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
QScopedStyle,
QStyle,
QStyleSelector,
QStylesAllSelector,
Q_PROPS_SEPARATOR,
USE_ON_LOCAL_SEQ_IDX,
getQFuncs,
Expand All @@ -47,23 +48,22 @@ import {
} from './types';
import { mapArray_get, mapArray_has, mapArray_set } from './util-mapArray';
import {
VNodeJournalOpCode,
vnode_applyJournal,
vnode_createErrorDiv,
vnode_getDomParent,
vnode_getProps,
vnode_getProp,
vnode_insertBefore,
vnode_isElementVNode,
vnode_isVNode,
vnode_isVirtualVNode,
vnode_locate,
vnode_newUnMaterializedElement,
vnode_setProp,
type VNodeJournal,
} from './vnode';
import type { ElementVNode, VNode, VirtualVNode } from './vnode-impl';
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);
Expand All @@ -73,19 +73,12 @@ export function getDomContainer(element: Element | VNode): IClientContainer {

export function getDomContainerFromQContainerElement(qContainerElement: Element): IClientContainer {
const qElement = qContainerElement as ContainerElement;
let container = qElement.qContainer;
if (!container) {
container = new DomContainer(qElement);
}
return container;
return (qElement.qContainer ||= new DomContainer(qElement));
}

/** @internal */
export function _getQContainerElement(element: Element | VNode): Element | null {
const qContainerElement: Element | null = vnode_isVNode(element)
? (vnode_getDomParent(element, true) as Element)
: element;
return qContainerElement.closest(QContainerSelector);
export function _getQContainerElement(element: Element): Element | null {
return element.closest(QContainerSelector);
}

export const isDomContainer = (container: any): container is DomContainer => {
Expand All @@ -99,7 +92,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
public qManifestHash: string;
public rootVNode: ElementVNode;
public document: QDocument;
public $journal$: VNodeJournal;
public $rawStateData$: unknown[];
public $storeProxyMap$: ObjToProxyMap = new WeakMap();
public $qFuncs$: Array<(...args: unknown[]) => unknown>;
Expand All @@ -111,29 +103,14 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
private $styleIds$: Set<string> | 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 <head>)
// 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)!;
Expand All @@ -160,7 +137,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 <head>)
*
* 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;
}

Expand All @@ -171,17 +165,21 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
handleError(err: any, host: VNode | null): void {
if (qDev && host) {
if (typeof document !== 'undefined') {
const vHost = host as VirtualVNode;
const journal: VNodeJournal = [];
const vHost = host;
const vHostParent = vHost.parent;
const vHostNextSibling = vHost.nextSibling as VNode | null;
const vErrorDiv = vnode_createErrorDiv(document, vHost, err, journal);
const journal: VNodeJournal = [];
const vErrorDiv = vnode_createErrorDiv(journal, document, vHost, err);
// If the host is an element node, we need to insert the error div into its parent.
const insertHost = vnode_isElementVNode(vHost) ? vHostParent || vHost : vHost;
// If the host is different then we need to insert errored-host in the same position as the host.
const insertBefore = insertHost === vHost ? null : vHostNextSibling;
vnode_insertBefore(journal, insertHost, vErrorDiv, insertBefore);
vnode_applyJournal(journal);
vnode_insertBefore(
journal,
insertHost as ElementVNode | VirtualVNode,
vErrorDiv,
insertBefore
);
}

if (err && err instanceof Error) {
Expand Down Expand Up @@ -223,7 +221,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
let vNode: VNode | null = host.parent;
while (vNode) {
if (vnode_isVirtualVNode(vNode)) {
if (vNode.getProp(OnRenderProp, null) !== null) {
if (vnode_getProp(vNode, OnRenderProp, null) !== null) {
return vNode;
}
vNode =
Expand All @@ -239,7 +237,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {

setHostProp<T>(host: HostElement, name: string, value: T): void {
const vNode: VirtualVNode = host as any;
vNode.setProp(name, value);
vnode_setProp(vNode, name, value);
}

getHostProp<T>(host: HostElement, name: string): T | null {
Expand All @@ -258,20 +256,21 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
getObjectById = parseInt;
break;
}
return vNode.getProp(name, getObjectById);
return vnode_getProp(vNode, name, getObjectById);
}

ensureProjectionResolved(vNode: VirtualVNode): void {
if ((vNode.flags & VNodeFlags.Resolved) === 0) {
vNode.flags |= VNodeFlags.Resolved;
const props = vnode_getProps(vNode);
for (let i = 0; i < props.length; i = i + 2) {
const prop = props[i] as string;
if (isSlotProp(prop)) {
const value = props[i + 1];
if (typeof value == 'string') {
const projection = this.vNodeLocate(value);
props[i + 1] = projection;
const props = vNode.props;
if (props) {
for (const prop of Object.keys(props)) {
if (isSlotProp(prop)) {
const value = props[prop];
if (typeof value == 'string') {
const projection = this.vNodeLocate(value);
props[prop] = projection;
}
}
}
}
Expand Down Expand Up @@ -307,7 +306,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
const styleElement = this.document.createElement('style');
styleElement.setAttribute(QStyle, styleId);
styleElement.textContent = content;
this.$journal$.push(VNodeJournalOpCode.Insert, this.document.head, null, styleElement);
this.document.head.appendChild(styleElement);
}
}

Expand Down
18 changes: 13 additions & 5 deletions packages/qwik/src/core/client/dom-render.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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 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';
import { NODE_DIFF_DATA_KEY } from '../shared/cursor/cursor-props';

/**
* Render JSX.
Expand Down Expand Up @@ -42,11 +45,16 @@ export const render = async (
const container = getDomContainer(parent as HTMLElement) as DomContainer;
container.$serverData$ = opts.serverData || {};
const host = container.rootVNode;
container.$scheduler$(ChoreType.NODE_DIFF, host, host, jsxNode as JSXNode);
await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$;
vnode_setProp(host, NODE_DIFF_DATA_KEY, jsxNode);
markVNodeDirty(container, host, ChoreBits.NODE_DIFF);
await container.$renderPromise$;
return {
cleanup: () => {
cleanup(container, container.rootVNode);
/**
* This can lead to cleaning up projection vnodes via the journal, but since we're cleaning up
* they don't matter so we ignore the journal
*/
cleanup(container, [], container.rootVNode);
},
};
};
2 changes: 1 addition & 1 deletion packages/qwik/src/core/client/process-vnode-data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// NOTE: we want to move this function to qwikloader, and therefore this function should not have any external dependencies
import { VNodeDataChar, VNodeDataSeparator } from '../shared/vnode-data-types';
import type { ContainerElement, QDocument } from './types';
import type { ElementVNode } from './vnode-impl';
import type { ElementVNode } from '../shared/vnode/element-vnode';

/**
* Process the VNodeData script tags and store the VNodeData in the VNodeDataMap.
Expand Down
27 changes: 8 additions & 19 deletions packages/qwik/src/core/client/run-qrl.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,20 +15,11 @@ export const _run = (...args: unknown[]): ValueOrPromise<unknown> => {
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);
};
42 changes: 21 additions & 21 deletions packages/qwik/src/core/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,9 +17,7 @@ export interface ClientContainer extends Container {
$locale$: string;
qManifestHash: string;
rootVNode: ElementVNode;
$journal$: VNodeJournal;
$forwardRefs$: Array<number> | null;
$flushEpoch$: number;
parseQRL<T = unknown>(qrl: string): QRL<T>;
$setRawState$(id: number, vParent: ElementVNode | VirtualVNode): void;
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading