diff --git a/.changeset/async-svelte-set.md b/.changeset/async-svelte-set.md new file mode 100644 index 0000000000..6f66f950d4 --- /dev/null +++ b/.changeset/async-svelte-set.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make SvelteSet work with async mode diff --git a/.changeset/spicy-teeth-tan.md b/.changeset/spicy-teeth-tan.md new file mode 100644 index 0000000000..c7efa65130 --- /dev/null +++ b/.changeset/spicy-teeth-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: defer batch resolution until earlier intersecting batches have committed diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index d7c2deeaae..7f6adad047 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -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']; @@ -47,8 +48,8 @@ var inited = false; export class SvelteSet extends Set { /** @type {Map>} */ #sources = new Map(); - #version = state(0); - #size = state(0); + /** @type {Source>} */ + #items; #update_version = update_version || -1; /** @@ -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(); @@ -98,18 +96,16 @@ 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} */ (set_proto[method].apply(this, v)); + var set = /** @type {Set} */ (set_proto[method].apply(get(this.#items), v)); return new SvelteSet(set); }; } @@ -117,19 +113,18 @@ export class SvelteSet extends 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)})`); @@ -138,16 +133,27 @@ 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; @@ -155,7 +161,8 @@ export class SvelteSet extends Set { /** @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); @@ -164,20 +171,24 @@ 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()) { @@ -185,8 +196,6 @@ export class SvelteSet extends Set { } sources.clear(); - set(this.#size, 0); - increment(this.#version); } keys() { @@ -194,20 +203,18 @@ export class SvelteSet extends Set { } 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; } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js new file mode 100644 index 0000000000..e28ba86489 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js @@ -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, + ` + + + +

pending=2 values.size=1 values=[1]

+
+

1: true

+

2: false

+

3: false

+

4: false

+

5: false

+
+

1

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=1 values.size=2 values=[1,2]

+
+

1: true

+

2: true

+

3: false

+

4: false

+

5: false

+
+

1

+

2

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.size=3 values=[1,2,3]

+
+

1: true

+

2: true

+

3: true

+

4: false

+

5: false

+
+

1

+

2

+

3

+ ` + ); + + add.click(); + await tick(); + add.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=2 values.size=3 values=[1,2,3]

+
+

1: true

+

2: true

+

3: true

+

4: false

+

5: false

+
+

1

+

2

+

3

+ ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=1 values.size=3 values=[1,2,3]

+
+

1: true

+

2: true

+

3: true

+

4: false

+

5: false

+
+

1

+

2

+

3

+ ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.size=5 values=[1,2,3,4,5]

+
+

1: true

+

2: true

+

3: true

+

4: true

+

5: true

+
+

1

+

2

+

3

+

4

+

5

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte new file mode 100644 index 0000000000..5995a44990 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte @@ -0,0 +1,43 @@ + + + + + + +

+ pending={$effect.pending()} + values.size={values.size} + values=[{Array.from(values)}] +

+ +
+ +

1: {values.has(1)}

+

2: {values.has(2)}

+

3: {values.has(3)}

+

4: {values.has(4)}

+

5: {values.has(5)}

+ +
+ +{#each values as v(v)} +

{await push(v)}

+{/each}