Skip to content

Commit 8a85059

Browse files
authored
fix: deeply unstate objects passed to inspect (#10056)
When doing `$inspect({ x, y })`, both `x` and `y` are now unstated if they are signals, compared to before where `unstate` was only called on the top level object, leaving the proxies in place which results in a worse debugging experience. Also improved typings which makes it easier to find related code paths.
1 parent e46a71e commit 8a85059

File tree

5 files changed

+92
-11
lines changed

5 files changed

+92
-11
lines changed

.changeset/curvy-ties-shout.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: deeply unstate objects passed to inspect

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

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ let current_dependencies = null;
6464
let current_dependencies_index = 0;
6565
/** @type {null | import('./types.js').Signal[]} */
6666
let current_untracked_writes = null;
67-
/** @type {null | import('./types.js').Signal} */
67+
/** @type {null | import('./types.js').SignalDebug} */
6868
let last_inspected_signal = null;
6969
/** If `true`, `get`ting the signal should not register it as a dependency */
7070
export let current_untracking = false;
@@ -81,7 +81,7 @@ let captured_signals = new Set();
8181
/** @type {Function | null} */
8282
let inspect_fn = null;
8383

84-
/** @type {Array<import('./types.js').SourceSignal & import('./types.js').SourceSignalDebug>} */
84+
/** @type {Array<import('./types.js').SignalDebug>} */
8585
let inspect_captured_signals = [];
8686

8787
// Handle rendering tree blocks and anchors
@@ -127,7 +127,6 @@ export function batch_inspect(target, prop, receiver) {
127127
} finally {
128128
is_batching_effect = previously_batching_effect;
129129
if (last_inspected_signal !== null) {
130-
// @ts-expect-error
131130
for (const fn of last_inspected_signal.inspect) fn();
132131
last_inspected_signal = null;
133132
}
@@ -737,8 +736,7 @@ function update_derived(signal, force_schedule) {
737736

738737
// @ts-expect-error
739738
if (DEV && signal.inspect && force_schedule) {
740-
// @ts-expect-error
741-
for (const fn of signal.inspect) fn();
739+
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
742740
}
743741
}
744742
}
@@ -841,8 +839,7 @@ export function unsubscribe_on_destroy(stores) {
841839
export function get(signal) {
842840
// @ts-expect-error
843841
if (DEV && signal.inspect && inspect_fn) {
844-
// @ts-expect-error
845-
signal.inspect.add(inspect_fn);
842+
/** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn);
846843
// @ts-expect-error
847844
inspect_captured_signals.push(signal);
848845
}
@@ -1111,10 +1108,9 @@ export function set_signal_value(signal, value) {
11111108
// @ts-expect-error
11121109
if (DEV && signal.inspect) {
11131110
if (is_batching_effect) {
1114-
last_inspected_signal = signal;
1111+
last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal);
11151112
} else {
1116-
// @ts-expect-error
1117-
for (const fn of signal.inspect) fn();
1113+
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
11181114
}
11191115
}
11201116
}
@@ -1836,6 +1832,37 @@ function deep_read(value, visited = new Set()) {
18361832
}
18371833
}
18381834

1835+
/**
1836+
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
1837+
* @param {any} value
1838+
* @param {Map<any, any>} visited
1839+
* @returns {any}
1840+
*/
1841+
function deep_unstate(value, visited = new Map()) {
1842+
if (typeof value === 'object' && value !== null && !visited.has(value)) {
1843+
const unstated = unstate(value);
1844+
if (unstated !== value) {
1845+
visited.set(value, unstated);
1846+
return unstated;
1847+
}
1848+
1849+
let contains_unstated = false;
1850+
/** @type {any} */
1851+
const nested_unstated = Array.isArray(value) ? [] : {};
1852+
for (let key in value) {
1853+
const result = deep_unstate(value[key], visited);
1854+
nested_unstated[key] = result;
1855+
if (result !== value[key]) {
1856+
contains_unstated = true;
1857+
}
1858+
}
1859+
1860+
visited.set(value, contains_unstated ? nested_unstated : value);
1861+
}
1862+
1863+
return visited.get(value) ?? value;
1864+
}
1865+
18391866
// TODO remove in a few versions, before 5.0 at the latest
18401867
let warned_inspect_changed = false;
18411868

@@ -1849,7 +1876,7 @@ export function inspect(get_value, inspect = console.log) {
18491876

18501877
pre_effect(() => {
18511878
const fn = () => {
1852-
const value = get_value().map(unstate);
1879+
const value = get_value().map((v) => deep_unstate(v));
18531880
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
18541881
// eslint-disable-next-line no-console
18551882
console.warn(

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

109109
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
110110

111+
export type SignalDebug<V = unknown> = SourceSignalDebug & Signal<V>;
112+
111113
export type EffectSignal = ComputationSignal<null | (() => void)>;
112114

113115
export type MaybeSignal<T = unknown> = T | Signal<T>;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test } from '../../test';
2+
3+
/**
4+
* @type {any[]}
5+
*/
6+
let log;
7+
/**
8+
* @type {typeof console.log}}
9+
*/
10+
let original_log;
11+
12+
export default test({
13+
compileOptions: {
14+
dev: true
15+
},
16+
before_test() {
17+
log = [];
18+
original_log = console.log;
19+
console.log = (...v) => {
20+
log.push(...v);
21+
};
22+
},
23+
after_test() {
24+
console.log = original_log;
25+
},
26+
async test({ assert, target }) {
27+
const [b1] = target.querySelectorAll('button');
28+
b1.click();
29+
await Promise.resolve();
30+
31+
assert.deepEqual(log, [
32+
'init',
33+
{ x: { count: 0 } },
34+
[{ count: 0 }],
35+
'update',
36+
{ x: { count: 1 } },
37+
[{ count: 1 }]
38+
]);
39+
}
40+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let x = $state({count: 0});
3+
4+
$inspect({x}, [x]);
5+
</script>
6+
7+
<button on:click={() => x.count++}>{x.count}</button>

0 commit comments

Comments
 (0)