Skip to content

Commit f343b00

Browse files
authored
fix: clean up scheduling system (#16741)
Consolidates our scheduling system that had diverged into two - queue_micro_task and Batch.ensure-queues and resolves some edge case race conditions related to flushSync along the way.
1 parent f778975 commit f343b00

File tree

6 files changed

+38
-27
lines changed

6 files changed

+38
-27
lines changed

.changeset/old-taxis-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: clean up scheduling system

packages/svelte/src/internal/client/dom/task.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { run_all } from '../../shared/utils.js';
2+
import { is_flushing_sync } from '../reactivity/batch.js';
23

34
// Fallback for when requestIdleCallback is not available
45
const request_idle_callback =
@@ -24,12 +25,27 @@ function run_idle_tasks() {
2425
run_all(tasks);
2526
}
2627

28+
export function has_pending_tasks() {
29+
return micro_tasks.length > 0 || idle_tasks.length > 0;
30+
}
31+
2732
/**
2833
* @param {() => void} fn
2934
*/
3035
export function queue_micro_task(fn) {
31-
if (micro_tasks.length === 0) {
32-
queueMicrotask(run_micro_tasks);
36+
if (micro_tasks.length === 0 && !is_flushing_sync) {
37+
var tasks = micro_tasks;
38+
queueMicrotask(() => {
39+
// If this is false, a flushSync happened in the meantime. Do _not_ run new scheduled microtasks in that case
40+
// as the ordering of microtasks would be broken at that point - consider this case:
41+
// - queue_micro_task schedules microtask A to flush task X
42+
// - synchronously after, flushSync runs, processing task X
43+
// - synchronously after, some other microtask B is scheduled, but not through queue_micro_task but for example a Promise.resolve() in user code
44+
// - synchronously after, queue_micro_task schedules microtask C to flush task Y
45+
// - one tick later, microtask A now resolves, flushing task Y before microtask B, which is incorrect
46+
// This if check prevents that race condition (that realistically will only happen in tests)
47+
if (tasks === micro_tasks) run_micro_tasks();
48+
});
3349
}
3450

3551
micro_tasks.push(fn);

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
update_effect
2626
} from '../runtime.js';
2727
import * as e from '../errors.js';
28-
import { flush_tasks } from '../dom/task.js';
28+
import { flush_tasks, has_pending_tasks, queue_micro_task } from '../dom/task.js';
2929
import { DEV } from 'esm-env';
3030
import { invoke_error_boundary } from '../error-handling.js';
3131
import { old_values } from './sources.js';
@@ -56,27 +56,14 @@ export let batch_deriveds = null;
5656
/** @type {Set<() => void>} */
5757
export let effect_pending_updates = new Set();
5858

59-
/** @type {Array<() => void>} */
60-
let tasks = [];
61-
62-
function dequeue() {
63-
const task = /** @type {() => void} */ (tasks.shift());
64-
65-
if (tasks.length > 0) {
66-
queueMicrotask(dequeue);
67-
}
68-
69-
task();
70-
}
71-
7259
/** @type {Effect[]} */
7360
let queued_root_effects = [];
7461

7562
/** @type {Effect | null} */
7663
let last_scheduled_effect = null;
7764

7865
let is_flushing = false;
79-
let is_flushing_sync = false;
66+
export let is_flushing_sync = false;
8067

8168
export class Batch {
8269
/**
@@ -470,11 +457,7 @@ export class Batch {
470457

471458
/** @param {() => void} task */
472459
static enqueue(task) {
473-
if (tasks.length === 0) {
474-
queueMicrotask(dequeue);
475-
}
476-
477-
tasks.unshift(task);
460+
queue_micro_task(task);
478461
}
479462
}
480463

@@ -505,7 +488,7 @@ export function flushSync(fn) {
505488
while (true) {
506489
flush_tasks();
507490

508-
if (queued_root_effects.length === 0) {
491+
if (queued_root_effects.length === 0 && !has_pending_tasks()) {
509492
current_batch?.flush();
510493

511494
// we need to check again, in case we just updated an `$effect.pending()`

packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
export let route = $state({ current: 'home' });
44
</script>
55

6+
<script>
7+
// reset from earlier tests
8+
route.current = 'home'
9+
</script>
10+
611
<button onclick={() => route.reject()}>reject</button>
712

813
<svelte:boundary>

packages/svelte/tests/runtime-runes/samples/dynamic-component-transition/_config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ export default test({
55
async test({ assert, target, raf }) {
66
const btn = target.querySelector('button');
77

8-
raf.tick(0);
8+
// one tick to not be at 0. Else the flushSync would revert the in-transition which hasn't started, and directly remove the button
9+
raf.tick(1);
910

1011
flushSync(() => {
1112
btn?.click();
1213
});
1314

1415
assert.htmlEqual(target.innerHTML, `<h1>Outside</h1><button style="opacity: 0;">Hide</button>`);
1516

16-
raf.tick(100);
17+
raf.tick(101);
1718

1819
assert.htmlEqual(target.innerHTML, `<h1>Outside</h1>`);
1920
}

packages/svelte/tests/runtime-runes/samples/dynamic-if-component-transition/_config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ export default test({
55
async test({ assert, target, raf }) {
66
const btn = target.querySelector('button');
77

8-
raf.tick(0);
8+
// one tick to not be at 0. Else the flushSync would revert the in-transition which hasn't started, and directly remove the button
9+
raf.tick(1);
910

1011
flushSync(() => {
1112
btn?.click();
1213
});
1314

1415
assert.htmlEqual(target.innerHTML, `<h1>Outside</h1><button style="opacity: 0;">Hide</button>`);
1516

16-
raf.tick(100);
17+
raf.tick(101);
1718

1819
assert.htmlEqual(target.innerHTML, `<h1>Outside</h1>`);
1920
}

0 commit comments

Comments
 (0)