Skip to content

Commit 6915c12

Browse files
authored
feat: allow state created in deriveds/effects to be written/read locally without self-invalidation (#15553)
* move parent property onto Signal * don't self-invalidate when updating a source create inside current reaction * lazily create deep state with parent reaction * no need to push_derived_source with mutable_state, as it never coexists with $.derived * reduce indirection * remove state_unsafe_local_read error * changeset * tests * fix test * inelegant fix * remove arg * tweak * some progress * more * tidy up * parent -> p * tmp * alternative approach * tidy up * reduce diff size * more * update comment
1 parent c7ce9fc commit 6915c12

File tree

12 files changed

+151
-101
lines changed

12 files changed

+151
-101
lines changed

.changeset/dirty-pianos-sparkle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: allow state created in deriveds/effects to be written/read locally without self-invalidation

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,6 @@ Property descriptors defined on `$state` objects must contain `value` and always
122122
Cannot set prototype of `$state` object
123123
```
124124

125-
### state_unsafe_local_read
126-
127-
```
128-
Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
129-
```
130-
131125
### state_unsafe_mutation
132126

133127
```

packages/svelte/messages/client-errors/errors.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,6 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
8080

8181
> Cannot set prototype of `$state` object
8282
83-
## state_unsafe_local_read
84-
85-
> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
86-
8783
## state_unsafe_mutation
8884

8985
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,10 @@ export function client_component(analysis, options) {
219219
for (const [name, binding] of analysis.instance.scope.declarations) {
220220
if (binding.kind === 'legacy_reactive') {
221221
legacy_reactive_declarations.push(
222-
b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined))
222+
b.const(
223+
name,
224+
b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined)
225+
)
223226
);
224227
}
225228
if (binding.kind === 'store_sub') {

packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
299299
return [
300300
b.declarator(
301301
declarator.id,
302-
b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined)
302+
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
303303
)
304304
];
305305
}
@@ -314,7 +314,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
314314
return b.declarator(
315315
path.node,
316316
binding?.kind === 'state'
317-
? b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined)
317+
? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
318318
: value
319319
);
320320
})

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const LEGACY_DERIVED_PROP = 1 << 17;
2020
export const INSPECT_EFFECT = 1 << 18;
2121
export const HEAD_EFFECT = 1 << 19;
2222
export const EFFECT_HAS_DERIVED = 1 << 20;
23+
export const EFFECT_IS_UPDATING = 1 << 21;
2324

2425
export const STATE_SYMBOL = Symbol('$state');
2526
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');

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

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -307,21 +307,6 @@ export function state_prototype_fixed() {
307307
}
308308
}
309309

310-
/**
311-
* Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
312-
* @returns {never}
313-
*/
314-
export function state_unsafe_local_read() {
315-
if (DEV) {
316-
const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state\nhttps://svelte.dev/e/state_unsafe_local_read`);
317-
318-
error.name = 'Svelte error';
319-
throw error;
320-
} else {
321-
throw new Error(`https://svelte.dev/e/state_unsafe_local_read`);
322-
}
323-
}
324-
325310
/**
326311
* Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
327312
* @returns {never}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ export {
113113
user_effect,
114114
user_pre_effect
115115
} from './reactivity/effects.js';
116-
export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js';
116+
export {
117+
mutable_source,
118+
mutate,
119+
set,
120+
source as state,
121+
update,
122+
update_pre
123+
} from './reactivity/sources.js';
117124
export {
118125
prop,
119126
rest_props,

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { ProxyMetadata, Source } from '#client' */
22
import { DEV } from 'esm-env';
3-
import { get, active_effect } from './runtime.js';
3+
import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
44
import { component_context } from './context.js';
55
import {
66
array_prototype,
@@ -17,14 +17,16 @@ import * as e from './errors.js';
1717
import { get_stack } from './dev/tracing.js';
1818
import { tracing_mode_flag } from '../flags/index.js';
1919

20+
/** @type {ProxyMetadata | null} */
21+
var parent_metadata = null;
22+
2023
/**
2124
* @template T
2225
* @param {T} value
23-
* @param {ProxyMetadata | null} [parent]
2426
* @param {Source<T>} [prev] dev mode only
2527
* @returns {T}
2628
*/
27-
export function proxy(value, parent = null, prev) {
29+
export function proxy(value, prev) {
2830
// if non-proxyable, or is already a proxy, return `value`
2931
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
3032
return value;
@@ -42,6 +44,31 @@ export function proxy(value, parent = null, prev) {
4244
var version = source(0);
4345

4446
var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
47+
var reaction = active_reaction;
48+
49+
/**
50+
* @template T
51+
* @param {() => T} fn
52+
*/
53+
var with_parent = (fn) => {
54+
var previous_reaction = active_reaction;
55+
set_active_reaction(reaction);
56+
57+
/** @type {T} */
58+
var result;
59+
60+
if (DEV) {
61+
var previous_metadata = parent_metadata;
62+
parent_metadata = metadata;
63+
result = fn();
64+
parent_metadata = previous_metadata;
65+
} else {
66+
result = fn();
67+
}
68+
69+
set_active_reaction(previous_reaction);
70+
return result;
71+
};
4572

4673
if (is_proxied_array) {
4774
// We need to create the length source eagerly to ensure that
@@ -54,7 +81,7 @@ export function proxy(value, parent = null, prev) {
5481

5582
if (DEV) {
5683
metadata = {
57-
parent,
84+
parent: parent_metadata,
5885
owners: null
5986
};
6087

@@ -66,7 +93,7 @@ export function proxy(value, parent = null, prev) {
6693
metadata.owners = prev_owners ? new Set(prev_owners) : null;
6794
} else {
6895
metadata.owners =
69-
parent === null
96+
parent_metadata === null
7097
? component_context !== null
7198
? new Set([component_context.function])
7299
: null
@@ -92,10 +119,13 @@ export function proxy(value, parent = null, prev) {
92119
var s = sources.get(prop);
93120

94121
if (s === undefined) {
95-
s = source(descriptor.value, stack);
122+
s = with_parent(() => source(descriptor.value, stack));
96123
sources.set(prop, s);
97124
} else {
98-
set(s, proxy(descriptor.value, metadata));
125+
set(
126+
s,
127+
with_parent(() => proxy(descriptor.value))
128+
);
99129
}
100130

101131
return true;
@@ -106,7 +136,10 @@ export function proxy(value, parent = null, prev) {
106136

107137
if (s === undefined) {
108138
if (prop in target) {
109-
sources.set(prop, source(UNINITIALIZED, stack));
139+
sources.set(
140+
prop,
141+
with_parent(() => source(UNINITIALIZED, stack))
142+
);
110143
}
111144
} else {
112145
// When working with arrays, we need to also ensure we update the length when removing
@@ -140,7 +173,7 @@ export function proxy(value, parent = null, prev) {
140173

141174
// create a source, but only if it's an own property and not a prototype property
142175
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
143-
s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack);
176+
s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack));
144177
sources.set(prop, s);
145178
}
146179

@@ -208,7 +241,7 @@ export function proxy(value, parent = null, prev) {
208241
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
209242
) {
210243
if (s === undefined) {
211-
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack);
244+
s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack));
212245
sources.set(prop, s);
213246
}
214247

@@ -235,7 +268,7 @@ export function proxy(value, parent = null, prev) {
235268
// If the item exists in the original, we need to create a uninitialized source,
236269
// else a later read of the property would result in a source being created with
237270
// the value of the original item at that index.
238-
other_s = source(UNINITIALIZED, stack);
271+
other_s = with_parent(() => source(UNINITIALIZED, stack));
239272
sources.set(i + '', other_s);
240273
}
241274
}
@@ -247,13 +280,19 @@ export function proxy(value, parent = null, prev) {
247280
// object property before writing to that property.
248281
if (s === undefined) {
249282
if (!has || get_descriptor(target, prop)?.writable) {
250-
s = source(undefined, stack);
251-
set(s, proxy(value, metadata));
283+
s = with_parent(() => source(undefined, stack));
284+
set(
285+
s,
286+
with_parent(() => proxy(value))
287+
);
252288
sources.set(prop, s);
253289
}
254290
} else {
255291
has = s.v !== UNINITIALIZED;
256-
set(s, proxy(value, metadata));
292+
set(
293+
s,
294+
with_parent(() => proxy(value))
295+
);
257296
}
258297

259298
if (DEV) {

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
untrack,
1212
increment_write_version,
1313
update_effect,
14-
derived_sources,
15-
set_derived_sources,
14+
reaction_sources,
15+
set_reaction_sources,
1616
check_dirtiness,
1717
untracking,
1818
is_destroying_effect
@@ -27,7 +27,8 @@ import {
2727
UNOWNED,
2828
MAYBE_DIRTY,
2929
BLOCK_EFFECT,
30-
ROOT_EFFECT
30+
ROOT_EFFECT,
31+
EFFECT_IS_UPDATING
3132
} from '../constants.js';
3233
import * as e from '../errors.js';
3334
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -51,6 +52,7 @@ export function set_inspect_effects(v) {
5152
* @param {Error | null} [stack]
5253
* @returns {Source<V>}
5354
*/
55+
// TODO rename this to `state` throughout the codebase
5456
export function source(v, stack) {
5557
/** @type {Value} */
5658
var signal = {
@@ -62,6 +64,14 @@ export function source(v, stack) {
6264
wv: 0
6365
};
6466

67+
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
68+
if (reaction_sources === null) {
69+
set_reaction_sources([signal]);
70+
} else {
71+
reaction_sources.push(signal);
72+
}
73+
}
74+
6575
if (DEV && tracing_mode_flag) {
6676
signal.created = stack ?? get_stack('CreatedAt');
6777
signal.debug = null;
@@ -70,14 +80,6 @@ export function source(v, stack) {
7080
return signal;
7181
}
7282

73-
/**
74-
* @template V
75-
* @param {V} v
76-
*/
77-
export function state(v) {
78-
return push_derived_source(source(v));
79-
}
80-
8183
/**
8284
* @template V
8385
* @param {V} initial_value
@@ -100,33 +102,6 @@ export function mutable_source(initial_value, immutable = false) {
100102
return s;
101103
}
102104

103-
/**
104-
* @template V
105-
* @param {V} v
106-
* @param {boolean} [immutable]
107-
* @returns {Source<V>}
108-
*/
109-
export function mutable_state(v, immutable = false) {
110-
return push_derived_source(mutable_source(v, immutable));
111-
}
112-
113-
/**
114-
* @template V
115-
* @param {Source<V>} source
116-
*/
117-
/*#__NO_SIDE_EFFECTS__*/
118-
function push_derived_source(source) {
119-
if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) {
120-
if (derived_sources === null) {
121-
set_derived_sources([source]);
122-
} else {
123-
derived_sources.push(source);
124-
}
125-
}
126-
127-
return source;
128-
}
129-
130105
/**
131106
* @template V
132107
* @param {Value<V>} source
@@ -153,14 +128,12 @@ export function set(source, value, should_proxy = false) {
153128
!untracking &&
154129
is_runes() &&
155130
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
156-
// If the source was created locally within the current derived, then
157-
// we allow the mutation.
158-
(derived_sources === null || !derived_sources.includes(source))
131+
!reaction_sources?.includes(source)
159132
) {
160133
e.state_unsafe_mutation();
161134
}
162135

163-
let new_value = should_proxy ? proxy(value, null, source) : value;
136+
let new_value = should_proxy ? proxy(value, source) : value;
164137

165138
return internal_set(source, new_value);
166139
}

0 commit comments

Comments
 (0)