Skip to content

Commit c29c02e

Browse files
committed
fix: ensure topological order for render effects
1 parent 05bd922 commit c29c02e

File tree

5 files changed

+85
-4
lines changed

5 files changed

+85
-4
lines changed

.changeset/ten-ties-repair.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: ensure topological order for render effects

packages/svelte/src/internal/client/runtime.js

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ function create_computation_signal(flags, value, block) {
214214
f: flags,
215215
// init
216216
i: null,
217+
// level
218+
l: 0,
217219
// references
218220
r: null,
219221
// value
@@ -238,6 +240,8 @@ function create_computation_signal(flags, value, block) {
238240
e: null,
239241
// flags
240242
f: flags,
243+
// level
244+
l: 0,
241245
// init
242246
i: null,
243247
// references
@@ -632,7 +636,45 @@ export function schedule_effect(signal, sync) {
632636
mark_subtree_children_inert(signal, true);
633637
}
634638
} else {
635-
current_queued_pre_and_render_effects.push(signal);
639+
// We need to ensure we insert the signal in the right topological order. In other words,
640+
// we need to evaluate where to insert the signal based off its level and whether or not it's
641+
// a pre-effect and within the same block. By checking the signals in the queue in reverse order
642+
// we can find the right place quickly. TODO: maybe opt to use a linked list rather than an array
643+
// for these operations.
644+
const length = current_queued_pre_and_render_effects.length;
645+
let should_append = length === 0;
646+
647+
if (!should_append) {
648+
const target_level = signal.l;
649+
const target_block = signal.b;
650+
const is_pre_effect = (flags & PRE_EFFECT) !== 0;
651+
let target_signal;
652+
let is_target_pre_effect;
653+
let i = length;
654+
while (true) {
655+
target_signal = current_queued_pre_and_render_effects[--i];
656+
is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0;
657+
if (target_signal.l <= target_level) {
658+
if (i + 1 === length) {
659+
should_append = true;
660+
} else {
661+
if (target_signal.b !== target_block || (is_target_pre_effect && !is_pre_effect)) {
662+
i++;
663+
}
664+
current_queued_pre_and_render_effects.splice(i, 0, signal);
665+
}
666+
break;
667+
}
668+
if (i === 0) {
669+
current_queued_pre_and_render_effects.unshift(signal);
670+
break;
671+
}
672+
}
673+
}
674+
675+
if (should_append) {
676+
current_queued_pre_and_render_effects.push(signal);
677+
}
636678
}
637679
}
638680
}
@@ -1310,12 +1352,15 @@ function internal_create_effect(type, init, sync, block, schedule) {
13101352
const signal = create_computation_signal(type | DIRTY, null, block);
13111353
signal.i = init;
13121354
signal.x = current_component_context;
1355+
if (current_effect !== null) {
1356+
signal.l = current_effect.l + 1;
1357+
if ((type & MANAGED) === 0) {
1358+
push_reference(current_effect, signal);
1359+
}
1360+
}
13131361
if (schedule) {
13141362
schedule_effect(signal, sync);
13151363
}
1316-
if (current_effect !== null && (type & MANAGED) === 0) {
1317-
push_reference(current_effect, signal);
1318-
}
13191364
return signal;
13201365
}
13211366

packages/svelte/src/internal/client/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export type ComputationSignal<V = unknown> = {
108108
r: null | ComputationSignal[];
109109
/** value: The latest value for this signal, doubles as the teardown for effects */
110110
v: V;
111+
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
112+
l: number;
111113
};
112114

113115
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `<button>Click</button><p>expires in 1 click</p>`,
6+
7+
async test({ assert, target }) {
8+
const [btn1] = target.querySelectorAll('button');
9+
10+
flushSync(() => {
11+
btn1.click();
12+
});
13+
14+
assert.htmlEqual(target.innerHTML, ``);
15+
}
16+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let data = $state({ num: 1 });
3+
4+
function expire() {
5+
data.num = data.num - 1;
6+
if (data.num <= 0) data = undefined;
7+
}
8+
</script>
9+
10+
{#if data}
11+
<button onclick={expire}>Click</button>
12+
<p>expires in {data.num} click</p>
13+
{/if}

0 commit comments

Comments
 (0)