pull/18758/merge
keyur 2 days ago committed by GitHub
commit 426fd4f634
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,9 @@
---
'svelte': patch
---
fix: `flushSync` at the top of a component script no longer crashes or breaks reactivity
Calling `flushSync()` while the effect tree is still being constructed (e.g. at the top of a component's `<script>`) used to flush the in-flight batch mid-construction. The boundary then resolved against a nulled `current_batch` and crashed with `Cannot read properties of null (reading 'transfer_effects')`, and the half-built tree left the enclosing branch's CLEAN flag unbalanced — which made every later `schedule` call bail, silently killing reactivity.
`flushSync` now detects that a branch/root effect's update is in flight and skips the flush (there is nothing user-visible to flush yet; the batch flushes normally once construction settles). The boundary's resolution also tolerates a null batch as defense in depth, transferring deferred effects into a fresh batch instead of crashing.

@ -353,6 +353,11 @@ export class Boundary {
#resolve(batch) {
this.is_pending = false;
// If no batch is active (e.g. `flushSync` was called during mount and
// flushed the batch the boundary is still inside), create one so the
// deferred effects still get flushed instead of being dropped.
if (batch === null) batch = Batch.ensure();
// any effects that were previously deferred should be transferred
// to the batch, which will flush in the next microtask
batch.transfer_effects(this.#dirty_effects, this.#maybe_dirty_effects);

@ -17,6 +17,7 @@ import {
ERROR_VALUE,
MANAGED_EFFECT,
REACTION_RAN,
REACTION_IS_UPDATING,
DESTROYING
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
@ -1031,6 +1032,23 @@ export class Batch {
* @returns {T}
*/
export function flushSync(fn) {
// If we're still constructing the effect tree (e.g. `flushSync` called at the
// top of a component script, while the enclosing branch effect's update is in
// flight), there is nothing user-visible to flush yet — and flushing the
// half-built tree corrupts it: the traverse can't reach branches that aren't
// attached to their parent yet, leaving their CLEAN flags unbalanced, which
// makes every later `schedule` call on effects inside them bail. The branch's
// in-flight update will settle the tree, and the batch flushes normally
// afterwards.
if (
active_effect !== null &&
(active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0 &&
(active_effect.f & REACTION_IS_UPDATING) !== 0
) {
if (fn) return fn();
return;
}
var was_flushing_sync = is_flushing_sync;
var prev_previous_batch = previous_batch;
previous_batch = null;

@ -0,0 +1,15 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, '<button>0</button>');
// No explicit flushSync after the click — the update must still propagate
// through the normal microtask flush.
/** @type {HTMLButtonElement} */ (target.querySelector('button')).click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>1</button>');
}
});

@ -0,0 +1,9 @@
<script>
import { flushSync } from 'svelte';
let count = $state(0);
flushSync();
</script>
<button onclick={() => count++}>{count}</button>

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, logs }) {
// $effect.pre must have run during init, despite the top-level flushSync
assert.deepEqual(logs, ['pre ran']);
assert.htmlEqual(target.innerHTML, '<button>0</button>');
/** @type {HTMLButtonElement} */ (target.querySelector('button')).click();
flushSync();
// reactivity must still work after the top-level flushSync
assert.htmlEqual(target.innerHTML, '<button>1</button>');
assert.deepEqual(logs, ['pre ran']);
}
});

@ -0,0 +1,13 @@
<script>
import { flushSync } from 'svelte';
let count = $state(0);
$effect.pre(() => {
console.log('pre ran');
});
flushSync();
</script>
<button onclick={() => count++}>{count}</button>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
assert.htmlEqual(target.innerHTML, '<button>0</button>');
/** @type {HTMLButtonElement} */ (target.querySelector('button')).click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>1</button>');
}
});

@ -0,0 +1,9 @@
<script>
import { flushSync } from 'svelte';
let count = $state(0);
flushSync();
</script>
<button onclick={() => count++}>{count}</button>
Loading…
Cancel
Save