Skip to content

feat: scheduler rewrite #7816

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 34 commits into
base: build/v2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5a13225
WIP: scheduler rewrite
Varixo Jun 30, 2025
6d9f3cf
fix: schedule store effects
Varixo Jul 2, 2025
46808d1
fix test to be correct
Varixo Jul 2, 2025
e611c0d
fix: ref test
Varixo Jul 19, 2025
90a8083
WIP: render done promise
Varixo Jul 20, 2025
699fdf1
fix: use correct qinit event name
Varixo Jul 21, 2025
d6c3ef8
feat: implement blocking rules logic
Varixo Jul 22, 2025
ca66382
feat: block already scheduled chores if needed before execution
Varixo Jul 23, 2025
52272c2
fix: don't schedule effect for array store targets for removing
Varixo Jul 26, 2025
ee55d2b
fix: inserting errored-host
Varixo Jul 27, 2025
3645f09
fix: immediately run RUN_QRL chores
Varixo Jul 27, 2025
ba362a5
fix: prevent running journal flush multiple times at the same time
Varixo Jul 28, 2025
489780f
fix: don't schedule chore if it is already running
Varixo Jul 29, 2025
5e49e7a
fix: unit tests await for queue
Varixo Jul 29, 2025
13bc691
fix: processing deleted vnode
Varixo Aug 2, 2025
b02fa6f
feat: use setImmediate or MessageChannel as next tick
Varixo Aug 2, 2025
8361983
fix: expected in visible task test
Varixo Aug 2, 2025
21fff80
fix: block visible tasks by its parent and child
Varixo Aug 2, 2025
0bf2c30
refactor: rename scheduler-document-position test file
Varixo Aug 3, 2025
d3267e0
fix: scheduler rules with QRLs
Varixo Aug 3, 2025
846e2b0
chore: reenable view transition
Varixo Aug 3, 2025
6f97744
error handling and block parent's chores
Varixo Aug 5, 2025
9aeddf8
fix: e2e flaky tests
Varixo Aug 6, 2025
968bf18
feat: better scheduler messages
Varixo Aug 6, 2025
c2af70d
fix: signals invalidation
Varixo Aug 7, 2025
2cd1cea
fix: block descendant chores
Varixo Aug 8, 2025
93e7a8d
feat: introduce $flushEpoch$ to manage DOM updates and improve task s…
Varixo Aug 9, 2025
92455c8
chore: small changes
Varixo Aug 10, 2025
fe1338f
change toggle test expects
Varixo Aug 11, 2025
6542030
test: fix expect assertPage util
Varixo Aug 13, 2025
d9e615e
fix: useResource tests
Varixo Aug 14, 2025
f3dae90
fix(qwik-router): dont execute task from removed layout
Varixo Aug 16, 2025
b403f6b
chore: change scheduler return value
Varixo Aug 16, 2025
4f32d06
fix: trigger async computed effects through scheduler
Varixo Aug 16, 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
2 changes: 1 addition & 1 deletion packages/docs/src/routes/api/qwik-testing/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
}
],
"kind": "Function",
"content": "Trigger an event in unit tests on an element.\n\nFuture deprecation candidate.\n\n\n```typescript\nexport declare function trigger(root: Element, queryOrElement: string | Element | keyof HTMLElementTagNameMap | null, eventName: string, eventPayload?: any): Promise<void>;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nroot\n\n\n</td><td>\n\nElement\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nqueryOrElement\n\n\n</td><td>\n\nstring \\| Element \\| keyof HTMLElementTagNameMap \\| null\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\neventName\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\neventPayload\n\n\n</td><td>\n\nany\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nPromise&lt;void&gt;",
"content": "Trigger an event in unit tests on an element.\n\nFuture deprecation candidate.\n\n\n```typescript\nexport declare function trigger(root: Element, queryOrElement: string | Element | keyof HTMLElementTagNameMap | null, eventName: string, eventPayload?: any, options?: {\n waitForIdle?: boolean;\n}): Promise<void>;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nroot\n\n\n</td><td>\n\nElement\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nqueryOrElement\n\n\n</td><td>\n\nstring \\| Element \\| keyof HTMLElementTagNameMap \\| null\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\neventName\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\neventPayload\n\n\n</td><td>\n\nany\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\noptions\n\n\n</td><td>\n\n{ waitForIdle?: boolean; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nPromise&lt;void&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/testing/element-fixture.ts",
"mdFile": "core.trigger.md"
},
Expand Down
16 changes: 16 additions & 0 deletions packages/docs/src/routes/api/qwik-testing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ export declare function trigger(
queryOrElement: string | Element | keyof HTMLElementTagNameMap | null,
eventName: string,
eventPayload?: any,
options?: {
waitForIdle?: boolean;
},
): Promise<void>;
```

Expand Down Expand Up @@ -568,6 +571,19 @@ any

_(Optional)_

</td></tr>
<tr><td>

options

</td><td>

\{ waitForIdle?: boolean; }

</td><td>

_(Optional)_

</td></tr>
</tbody></table>

Expand Down
14 changes: 14 additions & 0 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,20 @@
"content": "Use this to force running subscribers, for example when the calculated value mutates but remains the same object.\n\n\n```typescript\nforce(): void;\n```\n**Returns:**\n\nvoid",
"mdFile": "core.computedsignal.force.md"
},
{
"name": "forceStoreEffects",
"id": "forcestoreeffects",
"hierarchy": [
{
"name": "forceStoreEffects",
"id": "forcestoreeffects"
}
],
"kind": "Function",
"content": "Force a store to recompute and schedule effects.\n\n\n```typescript\nforceStoreEffects: (value: StoreTarget, prop: keyof StoreTarget) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nvalue\n\n\n</td><td>\n\nStoreTarget\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nprop\n\n\n</td><td>\n\nkeyof StoreTarget\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/impl/store.ts",
"mdFile": "core.forcestoreeffects.md"
},
{
"name": "Fragment",
"id": "fragment",
Expand Down
51 changes: 51 additions & 0 deletions packages/docs/src/routes/api/qwik/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,57 @@ force(): void;

void

## forceStoreEffects

Force a store to recompute and schedule effects.

```typescript
forceStoreEffects: (value: StoreTarget, prop: keyof StoreTarget) => void
```

<table><thead><tr><th>

Parameter

</th><th>

Type

</th><th>

Description

</th></tr></thead>
<tbody><tr><td>

value

</td><td>

StoreTarget

</td><td>

</td></tr>
<tr><td>

prop

</td><td>

keyof StoreTarget

</td><td>

</td></tr>
</tbody></table>

**Returns:**

void

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/impl/store.ts)

## Fragment

```typescript
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-router/src/buildtime/build-layout.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const test = testAppSuite('Build Layout');

test('total layouts', ({ ctx: { layouts } }) => {
// $ find starters/apps/qwikrouter-test/src/routes -name layout*tsx | wc -l
assert.equal(layouts.length, 12, JSON.stringify(layouts, null, 2));
assert.equal(layouts.length, 13, JSON.stringify(layouts, null, 2));
});

test('nested named layout', ({ assertLayout }) => {
Expand Down
7 changes: 4 additions & 3 deletions packages/qwik-router/src/runtime/src/link-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ export const Link = component$<LinkProps>((props) => {
})
: undefined;
const handleClick = clientNavPath
? $(async (event: Event, elm: HTMLAnchorElement) => {
? $((event: Event, elm: HTMLAnchorElement) => {
if (event.defaultPrevented) {
// If default was prevented, than it is up to us to make client side navigation.
if (elm.href) {
elm.setAttribute('aria-pressed', 'true');
await nav(elm.href, { forceReload: reload, replaceState, scroll });
elm.removeAttribute('aria-pressed');
nav(elm.href, { forceReload: reload, replaceState, scroll }).then(() => {
elm.removeAttribute('aria-pressed');
});
}
}
})
Expand Down
53 changes: 39 additions & 14 deletions packages/qwik-router/src/runtime/src/qwik-router-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ import {
_getContextContainer,
_getContextElement,
_getQContainerElement,
_UNINITIALIZED,
_waitUntilRendered,
_UNINITIALIZED,
SerializerSymbol,
type _ElementVNode,
type AsyncComputedReadonlySignal,
type SerializationStrategy,
forceStoreEffects,
_hasStoreEffects,
} from '@qwik.dev/core/internal';
import { clientNavigate } from './client-navigate';
import { CLIENT_DATA_CACHE, DEFAULT_LOADERS_SERIALIZATION_STRATEGY, Q_ROUTE } from './constants';
Expand Down Expand Up @@ -156,15 +158,13 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
}

const url = new URL(urlEnv);
const routeLocation = useStore<MutableRouteLocation>(
{
url,
params: env.params,
isNavigating: false,
prevUrl: undefined,
},
{ deep: false }
);
const routeLocationTarget: MutableRouteLocation = {
url,
params: env.params,
isNavigating: false,
prevUrl: undefined,
};
const routeLocation = useStore<MutableRouteLocation>(routeLocationTarget, { deep: false });
const navResolver: { r?: () => void } = {};
const container = _getContextContainer();
const getSerializationStrategy = (loaderId: string): SerializationStrategy => {
Expand Down Expand Up @@ -470,14 +470,30 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
if (navigation.dest.search && !!isSamePath(trackUrl, prevUrl)) {
trackUrl.search = navigation.dest.search;
}

let shouldForcePrevUrl = false;
let shouldForceUrl = false;
let shouldForceParams = false;
// Update route location
if (!isSamePath(trackUrl, prevUrl)) {
routeLocation.prevUrl = prevUrl;
if (_hasStoreEffects(routeLocation, 'prevUrl')) {
shouldForcePrevUrl = true;
}
routeLocationTarget.prevUrl = prevUrl;
}

if (routeLocationTarget.url !== trackUrl) {
if (_hasStoreEffects(routeLocation, 'url')) {
shouldForceUrl = true;
}
routeLocationTarget.url = trackUrl;
}

routeLocation.url = trackUrl;
routeLocation.params = { ...params };
if (routeLocationTarget.params !== params) {
if (_hasStoreEffects(routeLocation, 'params')) {
shouldForceParams = true;
}
routeLocationTarget.params = params;
}

(routeInternal as any).untrackedValue = { type: navType, dest: trackUrl };

Expand Down Expand Up @@ -746,6 +762,15 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
callRestoreScrollOnDocument();
}

if (shouldForcePrevUrl) {
forceStoreEffects(routeLocation, 'prevUrl');
}
if (shouldForceUrl) {
forceStoreEffects(routeLocation, 'url');
}
if (shouldForceParams) {
forceStoreEffects(routeLocation, 'params');
}
routeLocation.isNavigating = false;
navResolver.r?.();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useContext,
useServerData,
} from '@qwik.dev/core';
import { _getContextElement, _getDomContainer } from '@qwik.dev/core/internal';

import { ContentInternalContext } from './contexts';
import type { ClientSPAWindow } from './qwik-router-component';
Expand All @@ -20,13 +21,16 @@ export const RouterOutlet = component$(() => {
throw new Error('PrefetchServiceWorker component must be rendered on the server.');
}

const { value } = useContext(ContentInternalContext);
if (value && value.length > 0) {
const contentsLen = value.length;
const internalContext = useContext(ContentInternalContext);

const contents = internalContext.value;

if (contents && contents.length > 0) {
const contentsLen = contents.length;
let cmp: JSXNode | null = null;
for (let i = contentsLen - 1; i >= 0; i--) {
if (value[i].default) {
cmp = jsx(value[i].default as any, {
if (contents[i].default) {
cmp = jsx(contents[i].default as any, {
children: cmp,
});
}
Expand Down
67 changes: 17 additions & 50 deletions packages/qwik/src/core/client/dom-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import { assertTrue } from '../shared/error/assert';
import { QError, qError } from '../shared/error/error';
import { ERROR_CONTEXT, isRecoverable } from '../shared/error/error-handling';
import { getPlatform } from '../shared/platform/platform';
import { emitEvent, type QRLInternal } from '../shared/qrl/qrl-class';
import { type QRLInternal } from '../shared/qrl/qrl-class';
import type { QRL } from '../shared/qrl/qrl.public';
import { ChoreType } from '../shared/util-chore-type';
import { _SharedContainer } from '../shared/shared-container';
Expand Down Expand Up @@ -38,7 +37,6 @@ import {
QLocaleAttr,
QManifestHashAttr,
} from '../shared/utils/markers';
import { isPromise } from '../shared/utils/promises';
import { isSlotProp } from '../shared/utils/prop';
import { qDev } from '../shared/utils/qdev';
import {
Expand All @@ -60,15 +58,16 @@ import {
import {
VNodeJournalOpCode,
vnode_applyJournal,
vnode_getDOMChildNodes,
vnode_createErrorDiv,
vnode_getDomParent,
vnode_getNextSibling,
vnode_getParent,
vnode_getProp,
vnode_getProps,
vnode_insertBefore,
vnode_isElementVNode,
vnode_isVirtualVNode,
vnode_locate,
vnode_newElement,
vnode_newUnMaterializedElement,
vnode_setProp,
type VNodeJournal,
Expand Down Expand Up @@ -113,7 +112,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
public rootVNode: ElementVNode;
public document: QDocument;
public $journal$: VNodeJournal;
public renderDone: Promise<void> | null = null;
public $rawStateData$: unknown[];
public $storeProxyMap$: ObjToProxyMap = new WeakMap();
public $qFuncs$: Array<(...args: unknown[]) => unknown>;
Expand All @@ -124,12 +122,13 @@ export class DomContainer extends _SharedContainer implements IClientContainer {

private $stateData$: unknown[];
private $styleIds$: Set<string> | null = null;
private $renderCount$ = 0;

constructor(element: ContainerElement) {
super(
() => this.scheduleRender(),
() => vnode_applyJournal(this.$journal$),
() => {
this.$flushEpoch$++;
vnode_applyJournal(this.$journal$);
},
{},
element.getAttribute(QLocaleAttr)!
);
Expand Down Expand Up @@ -181,24 +180,19 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
return inflateQRL(this, parseQRL(qrl)) as QRL<T>;
}

handleError(err: any, host: HostElement): void {
handleError(err: any, host: HostElement | null): void {
if (qDev && host) {
// Clean vdom
if (typeof document !== 'undefined') {
const vHost = host as VirtualVNode;
const errorDiv = document.createElement('errored-host');
if (err && err instanceof Error) {
(errorDiv as any).props = { error: err };
}
errorDiv.setAttribute('q:key', '_error_');
const journal: VNodeJournal = [];

const vErrorDiv = vnode_newElement(errorDiv, 'errored-host');

vnode_getDOMChildNodes(journal, vHost, true).forEach((child) => {
vnode_insertBefore(journal, vErrorDiv, child, null);
});
vnode_insertBefore(journal, vHost, vErrorDiv, null);
const vHostParent = vnode_getParent(vHost) as VirtualVNode | ElementVNode | undefined;
const vHostNextSibling = vnode_getNextSibling(vHost);
const vErrorDiv = vnode_createErrorDiv(document, vHost, err, journal);
// 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);
}

Expand Down Expand Up @@ -279,33 +273,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
return vnode_getProp(vNode, name, getObjectById);
}

scheduleRender() {
this.$renderCount$++;
this.renderDone ||= getPlatform().nextTick(() => this.processChores());
return this.renderDone.finally(() =>
emitEvent('qrender', { instanceHash: this.$instanceHash$, renderCount: this.$renderCount$ })
);
}

private processChores() {
let renderCount = this.$renderCount$;
const result = this.$scheduler$(ChoreType.WAIT_FOR_ALL);
if (isPromise(result)) {
return result.then(async () => {
while (renderCount !== this.$renderCount$) {
renderCount = this.$renderCount$;
await this.$scheduler$(ChoreType.WAIT_FOR_ALL);
}
this.renderDone = null;
});
}
if (renderCount !== this.$renderCount$) {
this.processChores();
return;
}
this.renderDone = null;
}

ensureProjectionResolved(vNode: VirtualVNode): void {
if ((vNode[VNodeProps.flags] & VNodeFlags.Resolved) === 0) {
vNode[VNodeProps.flags] |= VNodeFlags.Resolved;
Expand Down
Loading
Loading