Skip to content

Commit 67ce34b

Browse files
committed
feat: add ssrNodeDocumentPosition function
1 parent 50adb35 commit 67ce34b

File tree

5 files changed

+178
-94
lines changed

5 files changed

+178
-94
lines changed

packages/qwik/src/core/client/vnode.ts

-60
Original file line numberDiff line numberDiff line change
@@ -1889,66 +1889,6 @@ export const vnode_getType = (vnode: VNode): 1 | 3 | 11 => {
18891889
const isElement = (node: any): node is Element =>
18901890
node && typeof node == 'object' && fastNodeType(node) === /** Node.ELEMENT_NODE* */ 1;
18911891

1892-
/// These global variables are used to avoid creating new arrays for each call to `vnode_documentPosition`.
1893-
const aPath: VNode[] = [];
1894-
const bPath: VNode[] = [];
1895-
export const vnode_documentPosition = (
1896-
a: VNode,
1897-
b: VNode,
1898-
rootVNode: ElementVNode | null
1899-
): -1 | 0 | 1 => {
1900-
if (a === b) {
1901-
return 0;
1902-
}
1903-
1904-
let aDepth = -1;
1905-
let bDepth = -1;
1906-
while (a) {
1907-
const vNode = (aPath[++aDepth] = a);
1908-
a = (vNode[VNodeProps.parent] ||
1909-
(rootVNode && vnode_getProp(a, QSlotParent, (id) => vnode_locate(rootVNode, id))))!;
1910-
}
1911-
while (b) {
1912-
const vNode = (bPath[++bDepth] = b);
1913-
b = (vNode[VNodeProps.parent] ||
1914-
(rootVNode && vnode_getProp(b, QSlotParent, (id) => vnode_locate(rootVNode, id))))!;
1915-
}
1916-
1917-
while (aDepth >= 0 && bDepth >= 0) {
1918-
a = aPath[aDepth] as VNode;
1919-
b = bPath[bDepth] as VNode;
1920-
if (a === b) {
1921-
// if the nodes are the same, we need to check the next level.
1922-
aDepth--;
1923-
bDepth--;
1924-
} else {
1925-
// We found a difference so we need to scan nodes at this level.
1926-
let cursor: VNode | null = b;
1927-
do {
1928-
cursor = vnode_getNextSibling(cursor);
1929-
if (cursor === a) {
1930-
return 1;
1931-
}
1932-
} while (cursor);
1933-
cursor = b;
1934-
do {
1935-
cursor = vnode_getPreviousSibling(cursor);
1936-
if (cursor === a) {
1937-
return -1;
1938-
}
1939-
} while (cursor);
1940-
if (rootVNode && vnode_getProp(b, QSlotParent, (id) => vnode_locate(rootVNode, id))) {
1941-
// The "b" node is a projection, so we need to set it after "a" node,
1942-
// because the "a" node could be a context provider.
1943-
return -1;
1944-
}
1945-
// The node is not in the list of siblings, that means it must be disconnected.
1946-
return 1;
1947-
}
1948-
}
1949-
return aDepth < bDepth ? -1 : 1;
1950-
};
1951-
19521892
/**
19531893
* Use this method to find the parent component for projection.
19541894
*

packages/qwik/src/core/client/vnode.unit.tsx

-27
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {
1313
} from './types';
1414
import {
1515
vnode_applyJournal,
16-
vnode_documentPosition,
1716
vnode_getAttr,
1817
vnode_getFirstChild,
1918
vnode_getNextSibling,
@@ -650,30 +649,4 @@ describe('vnode', () => {
650649
});
651650
});
652651
});
653-
describe('vnode_documentPosition', () => {
654-
it('should compare two elements', () => {
655-
parent.innerHTML = '<b></b><i></i>';
656-
const b = vnode_getFirstChild(vParent) as ElementVNode;
657-
const i = vnode_getNextSibling(b) as ElementVNode;
658-
expect(vnode_documentPosition(b, i, null)).toBe(-1);
659-
expect(vnode_documentPosition(i, b, null)).toBe(1);
660-
});
661-
it('should compare two virtual vNodes', () => {
662-
parent.innerHTML = 'AB';
663-
document.qVNodeData.set(parent, '{B}{B}');
664-
const a = vnode_getFirstChild(vParent) as ElementVNode;
665-
const b = vnode_getNextSibling(a) as ElementVNode;
666-
expect(vnode_documentPosition(a, b, null)).toBe(-1);
667-
expect(vnode_documentPosition(b, a, null)).toBe(1);
668-
});
669-
it('should compare two virtual vNodes', () => {
670-
parent.innerHTML = 'AB';
671-
document.qVNodeData.set(parent, '{{B}}{B}');
672-
const a = vnode_getFirstChild(vParent) as ElementVNode;
673-
const a2 = vnode_getFirstChild(a) as ElementVNode;
674-
const b = vnode_getNextSibling(a) as ElementVNode;
675-
expect(vnode_documentPosition(a2, b, null)).toBe(-1);
676-
expect(vnode_documentPosition(b, a2, null)).toBe(1);
677-
});
678-
});
679652
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import {
3+
vnode_getFirstChild,
4+
vnode_getNextSibling,
5+
vnode_newUnMaterializedElement,
6+
} from '../client/vnode';
7+
import type { ContainerElement, ElementVNode, QDocument } from '../client/types';
8+
import { vnode_documentPosition } from './scheduler-document-position';
9+
import { createDocument } from '@qwik.dev/dom';
10+
11+
describe('vnode_documentPosition', () => {
12+
let parent: ContainerElement;
13+
let document: QDocument;
14+
let vParent: ElementVNode;
15+
let qVNodeRefs: Map<number, Element | ElementVNode>;
16+
beforeEach(() => {
17+
document = createDocument() as QDocument;
18+
document.qVNodeData = new WeakMap();
19+
parent = document.createElement('test') as ContainerElement;
20+
parent.qVNodeRefs = qVNodeRefs = new Map();
21+
vParent = vnode_newUnMaterializedElement(parent);
22+
});
23+
afterEach(() => {
24+
parent = null!;
25+
document = null!;
26+
vParent = null!;
27+
});
28+
29+
it('should compare two elements', () => {
30+
parent.innerHTML = '<b></b><i></i>';
31+
const b = vnode_getFirstChild(vParent) as ElementVNode;
32+
const i = vnode_getNextSibling(b) as ElementVNode;
33+
expect(vnode_documentPosition(b, i, null)).toBe(-1);
34+
expect(vnode_documentPosition(i, b, null)).toBe(1);
35+
});
36+
it('should compare two virtual vNodes', () => {
37+
parent.innerHTML = 'AB';
38+
document.qVNodeData.set(parent, '{B}{B}');
39+
const a = vnode_getFirstChild(vParent) as ElementVNode;
40+
const b = vnode_getNextSibling(a) as ElementVNode;
41+
expect(vnode_documentPosition(a, b, null)).toBe(-1);
42+
expect(vnode_documentPosition(b, a, null)).toBe(1);
43+
});
44+
it('should compare two virtual vNodes', () => {
45+
parent.innerHTML = 'AB';
46+
document.qVNodeData.set(parent, '{{B}}{B}');
47+
const a = vnode_getFirstChild(vParent) as ElementVNode;
48+
const a2 = vnode_getFirstChild(a) as ElementVNode;
49+
const b = vnode_getNextSibling(a) as ElementVNode;
50+
expect(vnode_documentPosition(a2, b, null)).toBe(-1);
51+
expect(vnode_documentPosition(b, a2, null)).toBe(1);
52+
});
53+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { VNodeProps, type ElementVNode, type VNode } from '../client/types';
2+
import {
3+
vnode_getNextSibling,
4+
vnode_getPreviousSibling,
5+
vnode_getProp,
6+
vnode_locate,
7+
} from '../client/vnode';
8+
import type { ISsrNode } from '../ssr/ssr-types';
9+
import { QSlotParent } from './utils/markers';
10+
11+
/// These global variables are used to avoid creating new arrays for each call to `vnode_documentPosition`.
12+
const aVNodePath: VNode[] = [];
13+
const bVNodePath: VNode[] = [];
14+
/**
15+
* Compare two VNodes and determine their document position relative to each other.
16+
*
17+
* @param a VNode to compare
18+
* @param b VNode to compare
19+
* @param rootVNode - Root VNode of a container
20+
* @returns -1 if `a` is before `b`, 0 if `a` is the same as `b`, 1 if `a` is after `b`.
21+
*/
22+
export const vnode_documentPosition = (
23+
a: VNode,
24+
b: VNode,
25+
rootVNode: ElementVNode | null
26+
): -1 | 0 | 1 => {
27+
if (a === b) {
28+
return 0;
29+
}
30+
31+
let aDepth = -1;
32+
let bDepth = -1;
33+
while (a) {
34+
const vNode = (aVNodePath[++aDepth] = a);
35+
a = (vNode[VNodeProps.parent] ||
36+
(rootVNode && vnode_getProp(a, QSlotParent, (id) => vnode_locate(rootVNode, id))))!;
37+
}
38+
while (b) {
39+
const vNode = (bVNodePath[++bDepth] = b);
40+
b = (vNode[VNodeProps.parent] ||
41+
(rootVNode && vnode_getProp(b, QSlotParent, (id) => vnode_locate(rootVNode, id))))!;
42+
}
43+
44+
while (aDepth >= 0 && bDepth >= 0) {
45+
a = aVNodePath[aDepth] as VNode;
46+
b = bVNodePath[bDepth] as VNode;
47+
if (a === b) {
48+
// if the nodes are the same, we need to check the next level.
49+
aDepth--;
50+
bDepth--;
51+
} else {
52+
// We found a difference so we need to scan nodes at this level.
53+
let cursor: VNode | null = b;
54+
do {
55+
cursor = vnode_getNextSibling(cursor);
56+
if (cursor === a) {
57+
return 1;
58+
}
59+
} while (cursor);
60+
cursor = b;
61+
do {
62+
cursor = vnode_getPreviousSibling(cursor);
63+
if (cursor === a) {
64+
return -1;
65+
}
66+
} while (cursor);
67+
if (rootVNode && vnode_getProp(b, QSlotParent, (id) => vnode_locate(rootVNode, id))) {
68+
// The "b" node is a projection, so we need to set it after "a" node,
69+
// because the "a" node could be a context provider.
70+
return -1;
71+
}
72+
// The node is not in the list of siblings, that means it must be disconnected.
73+
return 1;
74+
}
75+
}
76+
return aDepth < bDepth ? -1 : 1;
77+
};
78+
79+
/// These global variables are used to avoid creating new arrays for each call to `ssrNodeDocumentPosition`.
80+
const aSsrNodePath: ISsrNode[] = [];
81+
const bSsrNodePath: ISsrNode[] = [];
82+
/**
83+
* Compare two SSR nodes and determine their document position relative to each other. Compares only
84+
* position between parent and child.
85+
*
86+
* @param a SSR node to compare
87+
* @param b SSR node to compare
88+
* @returns -1 if `a` is before `b`, 0 if `a` is the same as `b`, 1 if `a` is after `b`.
89+
*/
90+
export const ssrNodeDocumentPosition = (a: ISsrNode, b: ISsrNode): -1 | 0 | 1 => {
91+
if (a === b) {
92+
return 0;
93+
}
94+
95+
let aDepth = -1;
96+
let bDepth = -1;
97+
while (a) {
98+
const ssrNode = (aSsrNodePath[++aDepth] = a);
99+
a = ssrNode.currentComponentNode!;
100+
}
101+
while (b) {
102+
const ssrNode = (bSsrNodePath[++bDepth] = b);
103+
b = ssrNode.currentComponentNode!;
104+
}
105+
106+
while (aDepth >= 0 && bDepth >= 0) {
107+
a = aSsrNodePath[aDepth] as ISsrNode;
108+
b = bSsrNodePath[bDepth] as ISsrNode;
109+
if (a === b) {
110+
// if the nodes are the same, we need to check the next level.
111+
aDepth--;
112+
bDepth--;
113+
} else {
114+
return 1;
115+
}
116+
}
117+
return aDepth < bDepth ? -1 : 1;
118+
};

packages/qwik/src/core/shared/scheduler.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,7 @@ import {
9797
type ElementVNode,
9898
type VirtualVNode,
9999
} from '../client/types';
100-
import {
101-
vnode_documentPosition,
102-
vnode_isVNode,
103-
vnode_setAttr,
104-
VNodeJournalOpCode,
105-
} from '../client/vnode';
100+
import { vnode_isVNode, vnode_setAttr, VNodeJournalOpCode } from '../client/vnode';
106101
import { vnode_diff } from '../client/vnode-diff';
107102
import { executeComponent } from './component-execution';
108103
import type { Container, HostElement } from './types';
@@ -115,6 +110,8 @@ import { QScopedStyle } from './utils/markers';
115110
import { addComponentStylePrefix } from './utils/scoped-styles';
116111
import { type WrappedSignal, type ComputedSignal, triggerEffects } from '../signal/signal';
117112
import type { TargetType } from '../signal/store';
113+
import { ssrNodeDocumentPosition, vnode_documentPosition } from './scheduler-document-position';
114+
import type { ISsrNode } from '../ssr/ssr-types';
118115

119116
// Turn this on to get debug output of what the scheduler is doing.
120117
const DEBUG: boolean = false;
@@ -497,7 +494,10 @@ function choreComparator(a: Chore, b: Chore, rootVNode: ElementVNode | null): nu
497494
This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR).
498495
Problematic Node: ${aHost.toString()}`;
499496
logWarn(errorMessage);
500-
return 1;
497+
const hostDiff = ssrNodeDocumentPosition(aHost as ISsrNode, bHost as ISsrNode);
498+
if (hostDiff !== 0) {
499+
return hostDiff;
500+
}
501501
}
502502
}
503503

0 commit comments

Comments
 (0)