Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 20 additions & 16 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,34 +334,36 @@ export class Batch {
for (const [e, t] of this.#skipped_branches) {
reset_branch(e, t);
}
} else {
if (this.#pending === 0) {
batches.delete(this);

if (updates.length > 0) {
/** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process();
}

// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
return;
}

// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();

previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();

this.#deferred?.resolve();
}
previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;

this.#deferred?.resolve();

var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));

// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !batches.has(this)) {
if (async_mode_flag && this.#pending === 0) {
this.#commit();
// Rebases can activate other batches or null it out, therefore restore the new one here
current_batch = next_batch;
Expand Down Expand Up @@ -524,6 +526,8 @@ export class Batch {
}

#commit() {
batches.delete(this);

// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';

// Test that the store is unsubscribed from, even if it's not referenced once the store itself is set to null
export default test({
skip_no_async: true,

async test({ target, assert }) {
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>0</p> <button>add watcher</button>`
);

target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>1</p> hello 1 <button>remove watcher</button>`
);

const input = target.querySelector('input');
ok(input);

input.stepUp();
input.dispatchEvent(new Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> hello 2 <button>remove watcher</button>`
);

target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> <button>add watcher</button>`
);

input.stepUp();
input.dispatchEvent(new Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> <button>add watcher</button>`
);

target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>3</p> hello 3 <button>remove watcher</button>`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script>
import { writable, derived } from "svelte/store";

const obj = writable({ a: 1 });
let count = $state(0);
let watcherA = $state();

function watch (prop) {
return derived(obj, (o) => {
count++;
return o[prop];
});
}
</script>

<input type="number" bind:value={$obj.a}>
<p>{count}</p>

{#if watcherA}
<!-- make sure the presence of async work doesn't break the `legacy_updates` mechanism -->
{#if true}
{await 'hello'}
{/if}

{$watcherA}
<button on:click={() => watcherA = null}>remove watcher</button>
{:else}
<button on:click={() => watcherA = watch("a")}>add watcher</button>
{/if}
Loading