Skip to content

Commit 29e8da1

Browse files
committed
feat(cursors): fix infinite loop
1 parent 7cb88c4 commit 29e8da1

File tree

4 files changed

+70
-126
lines changed

4 files changed

+70
-126
lines changed

packages/qwik/src/core/shared/cursor/chore-execution.ts

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { serializeAttribute } from '../utils/styles';
3131
import type { ISsrNode, SSRContainer } from '../../ssr/ssr-types';
3232
import type { ElementVNode } from '../vnode/element-vnode';
3333
import { VNodeOperationType } from '../vnode/enums/vnode-operation-type.enum';
34-
import { dirtyChildrenStack } from './cursor-walker';
3534
import type { JSXOutput } from '../jsx/types/jsx-node';
3635

3736
/**
@@ -315,29 +314,6 @@ export function executeNodeProps(vNode: VNode, container: Container): void {
315314
vNode.dirty &= ~ChoreBits.NODE_PROPS;
316315
}
317316

318-
/**
319-
* Processes children of a vNode if the CHILDREN dirty bit is set. Processes the dirtyChildren array
320-
* depth-first.
321-
*
322-
* @param vNode - The vNode whose children need processing
323-
* @param container - The container
324-
* @returns Void
325-
*/
326-
export function executeChildren(vNode: VNode, container: Container): void {
327-
if (!(vNode.dirty & ChoreBits.CHILDREN)) {
328-
return;
329-
}
330-
331-
const dirtyChildren = vNode.dirtyChildren;
332-
if (!dirtyChildren || dirtyChildren.length === 0) {
333-
// No dirty children
334-
vNode.dirty &= ~ChoreBits.CHILDREN;
335-
return;
336-
}
337-
338-
dirtyChildrenStack.push({ parent: vNode, index: 0 });
339-
}
340-
341317
/**
342318
* Executes cleanup tasks for a vNode if the CLEANUP dirty bit is set.
343319
*
@@ -376,66 +352,3 @@ export function executeCompute(vNode: VNode, container: Container): ValueOrPromi
376352
vNode.dirty &= ~ChoreBits.COMPUTE;
377353
return;
378354
}
379-
380-
/**
381-
* Executes all pending chores for a vNode in the correct order: TASKS -> COMPONENT -> NODE_PROPS ->
382-
* COMPUTE -> CHILDREN -> CLEANUP
383-
*
384-
* @param vNode - The vNode to execute chores for
385-
* @param container - The container
386-
* @param cursor - The cursor root vNode (for storing visible task promises)
387-
* @returns Promise if any chore is async, void otherwise
388-
*/
389-
export function executeChoreSequence(
390-
vNode: VNode,
391-
container: Container,
392-
cursor: Cursor | null
393-
): ValueOrPromise<void> {
394-
console.log('executeChoreSequence', vNode.dirty);
395-
// Execute chores in order
396-
if (vNode.dirty & ChoreBits.TASKS) {
397-
const result = executeTasks(vNode, container, cursor);
398-
if (isPromise(result)) {
399-
return result;
400-
}
401-
return;
402-
}
403-
404-
if (vNode.dirty & ChoreBits.NODE_DIFF) {
405-
const result = executeNodeDiff(vNode, container);
406-
if (isPromise(result)) {
407-
return result;
408-
}
409-
return;
410-
}
411-
412-
if (vNode.dirty & ChoreBits.COMPONENT) {
413-
const result = executeComponentChore(vNode, container);
414-
if (isPromise(result)) {
415-
return result;
416-
}
417-
return;
418-
}
419-
420-
if (vNode.dirty & ChoreBits.NODE_PROPS) {
421-
executeNodeProps(vNode, container);
422-
return;
423-
}
424-
425-
if (vNode.dirty & ChoreBits.COMPUTE) {
426-
const result = executeCompute(vNode, container);
427-
if (isPromise(result)) {
428-
return result;
429-
}
430-
return;
431-
}
432-
433-
if (vNode.dirty & ChoreBits.CHILDREN) {
434-
executeChildren(vNode, container);
435-
return;
436-
}
437-
438-
if (vNode.dirty & ChoreBits.CLEANUP) {
439-
executeCleanup(vNode, container);
440-
}
441-
}

packages/qwik/src/core/shared/cursor/cursor-props.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { removeCursorFromQueue } from './cursor-queue';
1010
*/
1111
const CURSOR_PRIORITY_KEY = 'q:priority';
1212
const CURSOR_POSITION_KEY = 'q:position';
13-
const CURSOR_PROMISE_KEY = 'q:promise';
13+
const VNODE_PROMISE_KEY = 'q:promise';
1414
const CURSOR_EXTRA_PROMISES_KEY = 'q:extraPromises';
1515

1616
/**
@@ -72,28 +72,25 @@ export function setCursorPosition(vNode: VNode, position: VNode | null): void {
7272
}
7373

7474
/**
75-
* Gets the blocking promise from a cursor vNode.
75+
* Gets the blocking promise from a vNode.
7676
*
77-
* @param vNode - The cursor vNode
77+
* @param vNode - The vNode
7878
* @returns The promise, or null if none or not a cursor
7979
*/
80-
export function getCursorPromise(vNode: VNode): Promise<void> | null {
81-
if (!(vNode.flags & VNodeFlags.Cursor)) {
82-
return null;
83-
}
80+
export function getVNodePromise(vNode: VNode): Promise<void> | null {
8481
const props = vNode.props;
85-
return (props?.[CURSOR_PROMISE_KEY] as Promise<void> | null) ?? null;
82+
return (props?.[VNODE_PROMISE_KEY] as Promise<void> | null) ?? null;
8683
}
8784

8885
/**
89-
* Sets the blocking promise on a cursor vNode.
86+
* Sets the blocking promise on a vNode.
9087
*
91-
* @param vNode - The cursor vNode
88+
* @param vNode - The vNode
9289
* @param promise - The promise to set, or null to clear
9390
*/
94-
export function setCursorPromise(vNode: VNode, promise: Promise<void> | null): void {
91+
export function setVNodePromise(vNode: VNode, promise: Promise<void> | null): void {
9592
const props = (vNode.props ||= {});
96-
props[CURSOR_PROMISE_KEY] = promise;
93+
props[VNODE_PROMISE_KEY] = promise;
9794
}
9895

9996
/**

packages/qwik/src/core/shared/cursor/cursor-walker.ts

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66
*/
77

88
import { isServerPlatform } from '../platform/platform';
9-
import { isPromise } from '../utils/promises';
109
import type { VNode } from '../vnode/vnode';
11-
import { executeChoreSequence } from './chore-execution';
10+
import {
11+
executeCleanup,
12+
executeComponentChore,
13+
executeCompute,
14+
executeNodeDiff,
15+
executeNodeProps,
16+
executeTasks,
17+
} from './chore-execution';
1218
import type { Cursor } from './cursor';
1319
import {
1420
getCursorPosition,
1521
setCursorPosition,
16-
getCursorPromise,
17-
setCursorPromise,
22+
getVNodePromise,
23+
setVNodePromise,
1824
} from './cursor-props';
1925
import { pauseCursor } from './cursor';
2026
import { ChoreBits } from '../vnode/enums/chore-bits.enum';
@@ -25,6 +31,8 @@ import { createNextTick } from '../platform/next-tick';
2531
import { vnode_isElementVNode } from '../../client/vnode';
2632
import type { Container } from '../types';
2733
import { VNodeFlags } from '../../client/types';
34+
import { isPromise } from '../utils/promises';
35+
import type { ValueOrPromise } from '../utils/types';
2836

2937
export const dirtyChildrenStack: { parent: VNode; index: number }[] = [];
3038

@@ -114,7 +122,7 @@ export function walkCursor(cursor: Cursor, options: WalkOptions): void {
114122
}
115123

116124
// Check if cursor is blocked by a promise
117-
const blockingPromise = getCursorPromise(cursor);
125+
const blockingPromise = getVNodePromise(cursor);
118126
if (blockingPromise) {
119127
return;
120128
}
@@ -123,7 +131,11 @@ export function walkCursor(cursor: Cursor, options: WalkOptions): void {
123131
// Get starting position (resume from last position or start at root)
124132
let currentVNode: VNode | null = null;
125133

134+
let count = 0;
126135
while ((currentVNode = getCursorPosition(cursor))) {
136+
if (count++ > 500000) {
137+
throw new Error('Infinite loop detected in cursor walker');
138+
}
127139
// Check time budget (only for DOM, not SSR)
128140
// if (!isServer) {
129141
// const elapsed = performance.now() - startTime;
@@ -136,35 +148,57 @@ export function walkCursor(cursor: Cursor, options: WalkOptions): void {
136148
// }
137149

138150
// Skip if the vNode is not dirty
139-
if (!currentVNode.dirty) {
151+
if (!(currentVNode.dirty & ChoreBits.DIRTY_MASK) || getVNodePromise(currentVNode)) {
140152
// Move to next node
141153
setCursorPosition(cursor, getNextVNode());
142154
continue;
143155
}
144156

145-
const result = executeChoreSequence(currentVNode, container, cursor);
157+
let result: ValueOrPromise<void> | undefined;
158+
console.log('executeChoreSequence', currentVNode.dirty);
159+
// Execute chores in order
160+
if (currentVNode.dirty & ChoreBits.TASKS) {
161+
result = executeTasks(currentVNode, container, cursor);
162+
} else if (currentVNode.dirty & ChoreBits.NODE_DIFF) {
163+
result = executeNodeDiff(currentVNode, container);
164+
} else if (currentVNode.dirty & ChoreBits.COMPONENT) {
165+
result = executeComponentChore(currentVNode, container);
166+
} else if (currentVNode.dirty & ChoreBits.NODE_PROPS) {
167+
executeNodeProps(currentVNode, container);
168+
} else if (currentVNode.dirty & ChoreBits.COMPUTE) {
169+
result = executeCompute(currentVNode, container);
170+
} else if (currentVNode.dirty & ChoreBits.CHILDREN) {
171+
const dirtyChildren = currentVNode.dirtyChildren;
172+
if (!dirtyChildren || dirtyChildren.length === 0) {
173+
// No dirty children
174+
currentVNode.dirty &= ~ChoreBits.CHILDREN;
175+
} else {
176+
dirtyChildrenStack.push({ parent: currentVNode, index: 0 });
177+
// descend
178+
setCursorPosition(cursor, getNextVNode());
179+
}
180+
} else if (currentVNode.dirty & ChoreBits.CLEANUP) {
181+
executeCleanup(currentVNode, container);
182+
}
146183

147184
// Handle blocking promise
148-
// if (isPromise(result)) {
149-
// // Store promise on cursor and pause
150-
// setCursorPromise(cursor, result);
151-
// pauseCursor(cursor, currentVNode);
152-
153-
// result
154-
// .then(() => {
155-
// setCursorPromise(cursor, null);
156-
// })
157-
// .catch((error) => {
158-
// setCursorPromise(cursor, null);
159-
// container.handleError(error, currentVNode);
160-
// })
161-
// .finally(() => {
162-
// triggerCursors();
163-
// });
164-
// return;
165-
// }
185+
if (result && isPromise(result)) {
186+
// Store promise on cursor and pause
187+
setVNodePromise(cursor, result);
188+
// pauseCursor(cursor, currentVNode);
166189

167-
startTime = performance.now();
190+
result
191+
.then(() => {
192+
setVNodePromise(cursor, null);
193+
})
194+
.catch((error) => {
195+
setVNodePromise(cursor, null);
196+
container.handleError(error, currentVNode);
197+
})
198+
.finally(() => {
199+
triggerCursors();
200+
});
201+
}
168202
}
169203
if (!(cursor.dirty & ChoreBits.DIRTY_MASK)) {
170204
cursor.flags &= ~VNodeFlags.Cursor;

packages/qwik/src/core/shared/vnode/vnode-dirty.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function markVNodeDirty(
1414
bits: ChoreBits,
1515
resetCursorPosition = true
1616
): void {
17-
if (vNode.dirty & bits) {
17+
if (vNode.dirty & ChoreBits.DIRTY_MASK) {
1818
return;
1919
}
2020
vNode.dirty |= bits;

0 commit comments

Comments
 (0)