Repository: sveltejs/svelte (2025-07-16T11:45:41Z)

Issue:
Cannot create a sparse array in runes mode
### Describe the bug

If the `delete` keyword is used to remove an item from an array (i.e., create a [sparse array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#deleting_array_elements)) in runes mode, every item following the requested one will be removed as well.

This behavior is different from the one in legacy mode, which I consider to be the correct behavior.

### Reproduction

- Runes mode (Buggy): https://svelte.dev/playground/8fb588588e0f48719879970b8a48ecd8?version=5.36.1
- Legacy mode (Works fine): https://svelte.dev/playground/4b629ebddb164a7ebfeeba4800621e00?version=5.36.1

### Logs

```shell

```

### System Info

```shell
System:
    OS: Linux 6.14 Fedora Linux Asahi Remix 42 (Forty Two [Adams])
    CPU: (8) arm64 unknown
    Memory: 2.26 GB / 15.27 GB
    Container: Yes
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.16.0 - /run/user/1000/fnm_multishells/20738_1752596942062/bin/node
    Yarn: 1.22.22 - /run/user/1000/fnm_multishells/20738_1752596942062/bin/yarn
    npm: 11.4.2 - /run/user/1000/fnm_multishells/20738_1752596942062/bin/npm
    pnpm: 10.11.0 - /run/user/1000/fnm_multishells/20738_1752596942062/bin/pnpm
```

### Severity

annoyance


Discussion:
So, it was introduced in #13549: as `$inspect` is sync, it logs every step of built-in methods, e.g. shift:
```
init   Array(3) [ 1, 2, 3 ]
update Array(3) [ 2, 2, 3 ]
update Array(3) [ 2, 3, <1 empty slot> ]
update Array(2) [ 2, 3 ]
```
The 3rd line isn't printed currently.
For a moment, `undefined` (empty slot) slips into the array and can cause an error if `$inspect` prints something derived from this array that doesn't expect an `undefined` value.



Patch:
diff --git a/.changeset/curvy-houses-jog.md b/.changeset/curvy-houses-jog.md
new file mode 100644
index 000000000000..0a2323d8e4f0
--- /dev/null
+++ b/.changeset/curvy-houses-jog.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: better handle $inspect on array mutations
diff --git a/.changeset/metal-coats-thank.md b/.changeset/metal-coats-thank.md
new file mode 100644
index 000000000000..b307fd3d45c0
--- /dev/null
+++ b/.changeset/metal-coats-thank.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: leave proxied array `length` untouched when deleting properties
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 5da1b7e188fd..3ae4b87ed5d6 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -15,7 +15,13 @@ import {
 	is_array,
 	object_prototype
 } from '../shared/utils.js';
-import { state as source, set, increment } from './reactivity/sources.js';
+import {
+	state as source,
+	set,
+	increment,
+	flush_inspect_effects,
+	set_inspect_effects_deferred
+} from './reactivity/sources.js';
 import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
 import { UNINITIALIZED } from '../../constants.js';
 import * as e from './errors.js';
@@ -80,6 +86,9 @@ export function proxy(value) {
 		// We need to create the length source eagerly to ensure that
 		// mutations to the array are properly synced with our proxy
 		sources.set('length', source(/** @type {any[]} */ (value).length, stack));
+		if (DEV) {
+			value = /** @type {any} */ (inspectable_array(/** @type {any[]} */ (value)));
+		}
 	}
 
 	/** Used in dev for $inspect.trace() */
@@ -142,16 +151,6 @@ export function proxy(value) {
 					}
 				}
 			} else {
-				// When working with arrays, we need to also ensure we update the length when removing
-				// an indexed property
-				if (is_proxied_array && typeof prop === 'string') {
-					var ls = /** @type {Source<number>} */ (sources.get('length'));
-					var n = Number(prop);
-
-					if (Number.isInteger(n) && n < ls.v) {
-						set(ls, n);
-					}
-				}
 				set(s, UNINITIALIZED);
 				increment(version);
 			}
@@ -388,3 +387,42 @@ export function get_proxied_value(value) {
 export function is(a, b) {
 	return Object.is(get_proxied_value(a), get_proxied_value(b));
 }
+
+const ARRAY_MUTATING_METHODS = new Set([
+	'copyWithin',
+	'fill',
+	'pop',
+	'push',
+	'reverse',
+	'shift',
+	'sort',
+	'splice',
+	'unshift'
+]);
+
+/**
+ * Wrap array mutating methods so $inspect is triggered only once and
+ * to prevent logging an array in intermediate state (e.g. with an empty slot)
+ * @param {any[]} array
+ */
+function inspectable_array(array) {
+	return new Proxy(array, {
+		get(target, prop, receiver) {
+			var value = Reflect.get(target, prop, receiver);
+			if (!ARRAY_MUTATING_METHODS.has(/** @type {string} */ (prop))) {
+				return value;
+			}
+
+			/**
+			 * @this {any[]}
+			 * @param {any[]} args
+			 */
+			return function (...args) {
+				set_inspect_effects_deferred();
+				var result = value.apply(this, args);
+				flush_inspect_effects();
+				return result;
+			};
+		}
+	});
+}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 29c657bdd038..bd55b9d935f4 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -49,6 +49,12 @@ export function set_inspect_effects(v) {
 	inspect_effects = v;
 }
 
+let inspect_effects_deferred = false;
+
+export function set_inspect_effects_deferred() {
+	inspect_effects_deferred = true;
+}
+
 /**
  * @template V
  * @param {V} v
@@ -213,26 +219,32 @@ export function internal_set(source, value) {
 			}
 		}
 
-		if (DEV && inspect_effects.size > 0) {
-			const inspects = Array.from(inspect_effects);
+		if (DEV && inspect_effects.size > 0 && !inspect_effects_deferred) {
+			flush_inspect_effects();
+		}
+	}
+
+	return value;
+}
 
-			for (const effect of inspects) {
-				// Mark clean inspect-effects as maybe dirty and then check their dirtiness
-				// instead of just updating the effects - this way we avoid overfiring.
-				if ((effect.f & CLEAN) !== 0) {
-					set_signal_status(effect, MAYBE_DIRTY);
-				}
+export function flush_inspect_effects() {
+	inspect_effects_deferred = false;
 
-				if (is_dirty(effect)) {
-					update_effect(effect);
-				}
-			}
+	const inspects = Array.from(inspect_effects);
 
-			inspect_effects.clear();
+	for (const effect of inspects) {
+		// Mark clean inspect-effects as maybe dirty and then check their dirtiness
+		// instead of just updating the effects - this way we avoid overfiring.
+		if ((effect.f & CLEAN) !== 0) {
+			set_signal_status(effect, MAYBE_DIRTY);
+		}
+
+		if (is_dirty(effect)) {
+			update_effect(effect);
 		}
 	}
 
-	return value;
+	inspect_effects.clear();
 }
 
 /**


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js b/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js
new file mode 100644
index 000000000000..71341170672b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js
@@ -0,0 +1,13 @@
+import { ok, test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+	mode: ['client'],
+	async test({ target, assert, logs }) {
+		const btn = target.querySelector('button');
+
+		flushSync(() => btn?.click());
+
+		assert.deepEqual(logs[0], [0, , 2]);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte
new file mode 100644
index 000000000000..ca00a85491ec
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	const arr = [0, 1, 2];
+</script>
+
+<button onclick={() => {
+	delete arr[1];
+	console.log(arr);
+}}>del</button>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js
index 0e6b12508b0f..49f1b5de4170 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js
@@ -13,17 +13,6 @@ export default test({
 			button?.click();
 		});
 
-		assert.deepEqual(logs, [
-			'init',
-			[1, 2, 3, 7],
-			'update',
-			[2, 2, 3, 7],
-			'update',
-			[2, 3, 3, 7],
-			'update',
-			[2, 3, 7, 7],
-			'update',
-			[2, 3, 7]
-		]);
+		assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]);
 	}
 });
diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts
index 6954b8b683f6..bbd252b8e188 100644
--- a/packages/svelte/tests/suite.ts
+++ b/packages/svelte/tests/suite.ts
@@ -20,6 +20,9 @@ const filter = process.env.FILTER
 		)
 	: /./;
 
+// this defaults to 10, which is too low for some of our tests
+Error.stackTraceLimit = 100;
+
 export function suite<Test extends BaseTest>(fn: (config: Test, test_dir: string) => void) {
 	return {
 		test: (config: Test) => config,