Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0b519f1
unskip failing test
Rich-Harris Nov 16, 2025
c7472a6
Merge branch 'main' into each-block-pending
Rich-Harris Jan 28, 2026
3c73882
Merge branch 'main' into each-block-pending
Rich-Harris Feb 13, 2026
db97c85
Merge branch 'main' into each-block-pending
Rich-Harris Feb 26, 2026
37d270b
WIP
Rich-Harris Feb 26, 2026
0901f4b
Merge branch 'main' into each-block-pending
Rich-Harris Feb 26, 2026
c04f69b
fix: preserve each items that are needed by pending batches
Rich-Harris Feb 27, 2026
6e8c746
save some work
Rich-Harris Feb 27, 2026
484f841
fix
Rich-Harris Feb 27, 2026
b9eefe4
Merge branch 'async-each-preserve-pending' into each-block-pending
Rich-Harris Feb 27, 2026
a6a548c
tidy up
Rich-Harris Feb 27, 2026
2cb3759
fix test
Rich-Harris Feb 27, 2026
1b68706
Merge branch 'main' into each-block-pending
Rich-Harris Feb 27, 2026
b17330e
Merge branch 'main' into async-each-preserve-pending
Rich-Harris Feb 27, 2026
a02b1c4
Merge branch 'async-each-preserve-pending' into each-block-pending
Rich-Harris Feb 27, 2026
68fa346
merge main
Rich-Harris Mar 10, 2026
43ed2ba
better test
Rich-Harris Mar 10, 2026
dac3364
Merge branch 'main' into each-block-pending
Rich-Harris Mar 10, 2026
903eedb
progress i think?
Rich-Harris Mar 11, 2026
95a0bec
holy shit it woooooorks
Rich-Harris Mar 11, 2026
40f05ab
update changeset
Rich-Harris Mar 11, 2026
2c2bc76
WIP
Rich-Harris Mar 11, 2026
3cdeda6
fix
Rich-Harris Mar 11, 2026
a5b7ac8
WIP test
Rich-Harris Mar 11, 2026
839e678
rename test
Rich-Harris Mar 11, 2026
aa4a8f7
Merge branch 'each-block-pending' into async-svelte-set
Rich-Harris Mar 11, 2026
9baf459
update test
Rich-Harris Mar 11, 2026
6af7f90
tweak
Rich-Harris Mar 11, 2026
c313797
tweak
Rich-Harris Mar 11, 2026
1e6d87d
Merge branch 'each-block-pending' into async-svelte-set
Rich-Harris Mar 11, 2026
3b29319
tweak
Rich-Harris Mar 11, 2026
cbd804b
tweak
Rich-Harris Mar 11, 2026
2e2d77b
Merge branch 'each-block-pending' into async-svelte-set
Rich-Harris Mar 11, 2026
cfc47b5
changeset
Rich-Harris Mar 11, 2026
fb7f1ee
Fix: SvelteSet's read_methods (forEach, isDisjointFrom, isSubsetOf, i…
vercel[bot] Mar 11, 2026
b5cece5
WIP
Rich-Harris Mar 11, 2026
dd2f22d
fix, tidy up
Rich-Harris Mar 11, 2026
df4070d
prettier
Rich-Harris Mar 11, 2026
f2030bb
tidy up
Rich-Harris Mar 11, 2026
d7d7c31
merge main
Rich-Harris Mar 31, 2026
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
5 changes: 5 additions & 0 deletions .changeset/async-svelte-set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: make SvelteSet work with async mode
5 changes: 5 additions & 0 deletions .changeset/spicy-teeth-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: defer batch resolution until earlier intersecting batches have committed
91 changes: 49 additions & 42 deletions packages/svelte/src/reactivity/set.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { source, set, state, increment } from '../internal/client/reactivity/sources.js';
import { source, set, state } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get, update_version } from '../internal/client/runtime.js';
import { async_mode_flag } from '../internal/flags/index.js';

var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
Expand Down Expand Up @@ -47,8 +48,8 @@ var inited = false;
export class SvelteSet extends Set {
/** @type {Map<T, Source<boolean>>} */
#sources = new Map();
#version = state(0);
#size = state(0);
/** @type {Source<Set<T>>} */
#items;
#update_version = update_version || -1;

/**
Expand All @@ -57,19 +58,16 @@ export class SvelteSet extends Set {
constructor(value) {
super();

if (DEV) {
// If the value is invalid then the native exception will fire here
value = new Set(value);
this.#items = state(new Set(value));

var sources = this.#sources;

tag(this.#version, 'SvelteSet version');
tag(this.#size, 'SvelteSet.size');
for (const value of this.#items.v) {
sources.set(value, this.#source(true));
}

if (value) {
for (var element of value) {
super.add(element);
}
this.#size.v = super.size;
if (DEV) {
tag(this.#items, 'SvelteSet items');
}

if (!inited) this.#init();
Expand Down Expand Up @@ -98,38 +96,35 @@ export class SvelteSet extends Set {
for (const method of read_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
return set_proto[method].apply(this, v);
return set_proto[method].apply(get(this.#items), v);
};
}

for (const method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
var set = /** @type {Set<T>} */ (set_proto[method].apply(get(this.#items), v));
return new SvelteSet(set);
};
}
}

/** @param {T} value */
has(value) {
var has = super.has(value);
var sources = this.#sources;
var s = sources.get(value);

if (s === undefined) {
if (!has) {
if (!async_mode_flag) {
// If the value doesn't exist, track the version in case it's added later
// but don't create sources willy-nilly to track all possible values
get(this.#version);
get(this.#items);
return false;
}

s = this.#source(true);
s = this.#source(get(this.#items).has(value));

if (DEV) {
tag(s, `SvelteSet has(${label(value)})`);
Expand All @@ -138,24 +133,36 @@ export class SvelteSet extends Set {
sources.set(value, s);
}

get(s);
return has;
return get(s);
}

/** @param {T} value */
add(value) {
if (!super.has(value)) {
super.add(value);
set(this.#size, super.size);
increment(this.#version);
var sources = this.#sources;
var s = sources.get(value);

if (s !== undefined) {
set(s, true);
} else {
sources.set(value, this.#source(true));
}

var items = get(this.#items);

if (!items.has(value)) {
const clone = new Set(items);
clone.add(value);

set(this.#items, clone);
}

return this;
}

/** @param {T} value */
delete(value) {
var deleted = super.delete(value);
var items = get(this.#items);
var has = items.has(value);
var sources = this.#sources;
var s = sources.get(value);

Expand All @@ -164,50 +171,50 @@ export class SvelteSet extends Set {
set(s, false);
}

if (deleted) {
set(this.#size, super.size);
increment(this.#version);
if (has) {
const clone = new Set(items);
clone.delete(value);

set(this.#items, clone);
}

return deleted;
return has;
}

clear() {
if (super.size === 0) {
if (get(this.#items).size === 0) {
return;
}

// Clear first, so we get nice console.log outputs with $inspect
super.clear();
set(this.#items, new Set());

var sources = this.#sources;

for (var s of sources.values()) {
set(s, false);
}

sources.clear();
set(this.#size, 0);
increment(this.#version);
}

keys() {
return this.values();
}

values() {
get(this.#version);
return super.values();
return get(this.#items).values();
}

entries() {
get(this.#version);
return super.entries();
return get(this.#items).entries();
}

[Symbol.iterator]() {
return this.keys();
return this.values();
}

get size() {
return get(this.#size);
return get(this.#items).size;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { tick } from 'svelte';
import { test } from '../../test';

export default test({
async test({ assert, target }) {
await tick();

const [add, shift, pop] = target.querySelectorAll('button');

add.click();
await tick();
add.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=2 values.size=1 values=[1]</p>
<hr>
<p>1: true</p>
<p>2: false</p>
<p>3: false</p>
<p>4: false</p>
<p>5: false</p>
<hr>
<p>1</p>
`
);

shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=1 values.size=2 values=[1,2]</p>
<hr>
<p>1: true</p>
<p>2: true</p>
<p>3: false</p>
<p>4: false</p>
<p>5: false</p>
<hr>
<p>1</p>
<p>2</p>
`
);

shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=0 values.size=3 values=[1,2,3]</p>
<hr>
<p>1: true</p>
<p>2: true</p>
<p>3: true</p>
<p>4: false</p>
<p>5: false</p>
<hr>
<p>1</p>
<p>2</p>
<p>3</p>
`
);

add.click();
await tick();
add.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=2 values.size=3 values=[1,2,3]</p>
<hr>
<p>1: true</p>
<p>2: true</p>
<p>3: true</p>
<p>4: false</p>
<p>5: false</p>
<hr>
<p>1</p>
<p>2</p>
<p>3</p>
`
);

pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=1 values.size=3 values=[1,2,3]</p>
<hr>
<p>1: true</p>
<p>2: true</p>
<p>3: true</p>
<p>4: false</p>
<p>5: false</p>
<hr>
<p>1</p>
<p>2</p>
<p>3</p>
`
);

pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=0 values.size=5 values=[1,2,3,4,5]</p>
<hr>
<p>1: true</p>
<p>2: true</p>
<p>3: true</p>
<p>4: true</p>
<p>5: true</p>
<hr>
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
`
);
}
});
Loading
Loading