make internal sources ownerless (#13013)

* make internal sources ownerless

* WIP

* WIP

* WIP

* same for mutable_state

* oops

* DRY

* unrelated

* changeset

* fix
pull/13015/head
Rich Harris 4 months ago committed by GitHub
parent 574d26071c
commit ae27f27810
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make internal sources ownerless

@ -3,7 +3,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
import { busy } from './util.js';
function setup() {
let head = $.source(0);
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
let head = $.state(0);
let last = head;
let counter = 0;

@ -5,7 +5,7 @@ let len = 50;
const iter = 50;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;

@ -4,7 +4,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 5;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let heads = new Array(100).fill(null).map((_) => $.source(0));
let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});

@ -4,7 +4,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let size = 30;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {

@ -11,7 +11,7 @@ function count(number) {
}
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {

@ -20,8 +20,8 @@ const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() {
let res = [];
const A = $.source(0);
const B = $.source(0);
const A = $.state(0);
const B = $.state(0);
const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2));
const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
D.equals = function (/** @type {number[]} */ l) {

@ -9,7 +9,7 @@ const COUNT = 1e5;
*/
function create_data_signals(n, sources) {
for (let i = 0; i < n; i++) {
sources[i] = $.source(i);
sources[i] = $.state(i);
}
return sources;
}

@ -207,7 +207,7 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_source')));
legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_state')));
}
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {

@ -113,17 +113,17 @@ export function ClassBody(node, context) {
value =
field.kind === 'state'
? b.call(
'$.source',
'$.state',
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'raw_state'
? b.call('$.source', init)
? b.call('$.state', init)
: field.kind === 'derived_by'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.source');
value = b.call('$.state');
}
if (is_private) {

@ -126,7 +126,7 @@ export function VariableDeclaration(node, context) {
value = b.call('$.proxy', value);
}
if (is_state_source(binding, context.state.analysis)) {
value = b.call('$.source', value);
value = b.call('$.state', value);
}
return value;
};
@ -291,7 +291,7 @@ export function VariableDeclaration(node, context) {
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
return [b.declarator(declarator.id, b.call('$.mutable_state', value))];
}
const tmp = scope.generate('tmp');
@ -303,7 +303,7 @@ function create_state_declarators(declarator, scope, value) {
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
binding?.kind === 'state' ? b.call('$.mutable_state', value) : value
);
})
];

@ -52,10 +52,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {Effect | null} */
var catch_effect;
var input_source = runes
? source(/** @type {V} */ (undefined))
: mutable_source(/** @type {V} */ (undefined));
var error_source = runes ? source(undefined) : mutable_source(undefined);
var input_source = (runes ? source : mutable_source)(/** @type {V} */ (undefined));
var error_source = (runes ? source : mutable_source)(undefined);
var resolved = false;
/**

@ -105,7 +105,7 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
export { mutable_source, mutate, source, set } from './reactivity/sources.js';
export { mutable_state, mutate, set, state } from './reactivity/sources.js';
export {
prop,
rest_props,

@ -118,7 +118,7 @@ export function proxy(value, parent = null, prev) {
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), null);
s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata));
sources.set(prop, s);
}
@ -170,7 +170,7 @@ export function proxy(value, parent = null, prev) {
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, null);
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
sources.set(prop, s);
}

@ -34,39 +34,34 @@ let inspect_effects = new Set();
/**
* @template V
* @param {V} v
* @param {Reaction | null} [owner]
* @returns {Source<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function source(v, owner = current_reaction) {
var source = {
export function source(v) {
return {
f: 0, // TODO ideally we could skip this altogether, but it causes type errors
v,
reactions: null,
equals,
version: 0
};
}
if (owner !== null && (owner.f & DERIVED) !== 0) {
if (derived_sources === null) {
set_derived_sources([source]);
} else {
derived_sources.push(source);
}
}
return source;
/**
* @template V
* @param {V} v
*/
export function state(v) {
return push_derived_source(source(v));
}
/**
* @template V
* @param {V} initial_value
* @param {Reaction | null} [owner]
* @returns {Source<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function mutable_source(initial_value, owner) {
const s = source(initial_value, owner);
export function mutable_source(initial_value) {
const s = source(initial_value);
s.equals = safe_equals;
// bind the signal to the component context, in case we need to
@ -78,6 +73,32 @@ export function mutable_source(initial_value, owner) {
return s;
}
/**
* @template V
* @param {V} v
* @returns {Source<V>}
*/
export function mutable_state(v) {
return push_derived_source(mutable_source(v));
}
/**
* @template V
* @param {Source<V>} source
*/
/*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) {
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
if (derived_sources === null) {
set_derived_sources([source]);
} else {
derived_sources.push(source);
}
}
return source;
}
/**
* @template V
* @param {Value<V>} source

@ -19,7 +19,7 @@ import { mutable_source, set } from './sources.js';
export function store_get(store, store_name, stores) {
const entry = (stores[store_name] ??= {
store: null,
source: mutable_source(undefined, null),
source: mutable_source(undefined),
unsubscribe: noop
});

@ -7,10 +7,12 @@ import {
render_effect,
user_effect
} from '../../src/internal/client/reactivity/effects';
import { source, set } from '../../src/internal/client/reactivity/sources';
import { state, set } from '../../src/internal/client/reactivity/sources';
import type { Derived, Value } from '../../src/internal/client/types';
import { proxy } from '../../src/internal/client/proxy';
import { derived } from '../../src/internal/client/reactivity/deriveds';
import { snapshot } from '../../src/internal/shared/clone.js';
import { SvelteSet } from '../../src/reactivity/set';
/**
* @param runes runes mode
@ -51,7 +53,7 @@ describe('signals', () => {
test('effect with state and derived in it', () => {
const log: string[] = [];
let count = source(0);
let count = state(0);
let double = derived(() => $.get(count) * 2);
effect(() => {
log.push(`${$.get(count)}:${$.get(double)}`);
@ -68,7 +70,7 @@ describe('signals', () => {
test('multiple effects with state and derived in it#1', () => {
const log: string[] = [];
let count = source(0);
let count = state(0);
let double = derived(() => $.get(count) * 2);
effect(() => {
@ -89,7 +91,7 @@ describe('signals', () => {
test('multiple effects with state and derived in it#2', () => {
const log: string[] = [];
let count = source(0);
let count = state(0);
let double = derived(() => $.get(count) * 2);
effect(() => {
@ -110,7 +112,7 @@ describe('signals', () => {
test('derived from state', () => {
const log: number[] = [];
let count = source(0);
let count = state(0);
let double = derived(() => $.get(count) * 2);
effect(() => {
@ -128,7 +130,7 @@ describe('signals', () => {
test('derived from derived', () => {
const log: number[] = [];
let count = source(0);
let count = state(0);
let double = derived(() => $.get(count) * 2);
let quadruple = derived(() => $.get(double) * 2);
@ -147,7 +149,7 @@ describe('signals', () => {
test('state reset', () => {
const log: number[] = [];
let count = source(0);
let count = state(0);
let double = derived(() => $.get(count) * 2);
effect(() => {
@ -183,8 +185,8 @@ describe('signals', () => {
const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2));
const hard = (n: number, l: string) => n + fib(16);
const A = source(0);
const B = source(0);
const A = state(0);
const B = state(0);
const C = derived(() => ($.get(A) % 2) + ($.get(B) % 2));
const D = derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
const E = derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E'));
@ -223,7 +225,7 @@ describe('signals', () => {
test('effects correctly handle unowned derived values that do not change', () => {
const log: number[] = [];
let count = source(0);
let count = state(0);
const read = () => {
const x = derived(() => ({ count: $.get(count) }));
return $.get(x);
@ -251,8 +253,8 @@ describe('signals', () => {
return () => {
const nested: Derived<string>[] = [];
const a = source(0);
const b = source(0);
const a = state(0);
const b = state(0);
const c = derived(() => {
const a_2 = derived(() => $.get(a) + '!');
const b_2 = derived(() => $.get(b) + '?');
@ -280,7 +282,7 @@ describe('signals', () => {
});
// outside of test function so that they are unowned signals
let count = source(0);
let count = state(0);
let calc = derived(() => {
if ($.get(count) >= 2) {
return 'limit';
@ -335,7 +337,7 @@ describe('signals', () => {
};
});
let some_state = source({});
let some_state = state({});
let some_deps = derived(() => {
return [$.get(some_state)];
});
@ -360,7 +362,7 @@ describe('signals', () => {
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
const value = source({ count: 0 });
const value = state({ count: 0 });
user_effect(() => {
set(value, { count: 0 });
$.get(value);
@ -400,7 +402,7 @@ describe('signals', () => {
});
test('effect teardown is removed on re-run', () => {
const count = source(0);
const count = state(0);
let first = true;
let teardown = 0;
@ -428,8 +430,8 @@ describe('signals', () => {
let outer: Value<string | number>;
const destroy = effect_root(() => {
inner = source(0);
outer = source(0);
inner = state(0);
outer = state(0);
render_effect(() => {
a = derived(() => {
@ -470,12 +472,12 @@ describe('signals', () => {
test('owned deriveds correctly cleanup when no longer connected to graph', () => {
let a: Derived<unknown>;
let state = source(0);
let s = state(0);
const destroy = effect_root(() => {
render_effect(() => {
a = derived(() => {
$.get(state);
$.get(s);
});
$.get(a);
});
@ -484,16 +486,16 @@ describe('signals', () => {
return () => {
flushSync();
assert.equal(a?.deps?.length, 1);
assert.equal(state?.reactions?.length, 1);
assert.equal(s?.reactions?.length, 1);
destroy();
assert.equal(a?.deps?.length, 1);
assert.equal(state?.reactions, null);
assert.equal(s?.reactions, null);
};
});
test('deriveds update upon reconnection #1', () => {
let a = source(false);
let b = source(false);
let a = state(false);
let b = state(false);
let c = derived(() => $.get(a));
let d = derived(() => $.get(c));
@ -534,9 +536,9 @@ describe('signals', () => {
});
test('deriveds update upon reconnection #2', () => {
let a = source(false);
let b = source(false);
let c = source(false);
let a = state(false);
let b = state(false);
let c = state(false);
let d = derived(() => $.get(a) || $.get(b));
@ -583,8 +585,8 @@ describe('signals', () => {
});
test('deriveds update upon reconnection #3', () => {
let a = source(false);
let b = source(false);
let a = state(false);
let b = state(false);
let c = derived(() => $.get(a) || $.get(b));
let d = derived(() => $.get(c));
@ -617,7 +619,7 @@ describe('signals', () => {
});
test('unowned deriveds are not added as reactions', () => {
var count = source(0);
var count = state(0);
function create_derived() {
return derived(() => $.get(count) * 2);
@ -642,7 +644,7 @@ describe('signals', () => {
});
test('unowned deriveds are correctly connected and disconnected from the graph', () => {
var count = source(0);
var count = state(0);
function create_derived() {
return derived(() => $.get(count) * 2);
@ -693,4 +695,34 @@ describe('signals', () => {
assert.equal($.get(derived_length), 1);
};
});
test('deriveds cannot depend on state they own', () => {
return () => {
const d = derived(() => {
const s = state(0);
return $.get(s);
});
assert.throws(() => $.get(d), 'state_unsafe_local_read');
};
});
test('proxy version state does not trigger self-dependency guard', () => {
return () => {
const s = proxy({ a: { b: 1 } });
const d = derived(() => snapshot(s));
assert.deepEqual($.get(d), s);
};
});
test('set version state does not trigger self-dependency guard', () => {
return () => {
const set = new SvelteSet();
const d = derived(() => set.has('test'));
set.add('test');
assert.equal($.get(d), true);
};
});
});

@ -13,7 +13,7 @@ export default function Bind_component_snippet($$anchor) {
$.append($$anchor, text);
};
let value = $.source('');
let value = $.state('');
const _snippet = snippet;
var fragment = root();
var node = $.first_child(fragment);

@ -5,7 +5,7 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
$.push($$props, true);
class Foo {
#a = $.source();
#a = $.state();
get a() {
return $.get(this.#a);
@ -15,7 +15,7 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
$.set(this.#a, $.proxy(value));
}
#b = $.source();
#b = $.state();
constructor() {
this.a = 1;

@ -1,8 +1,8 @@
/* index.svelte.js generated by Svelte VERSION */
import * as $ from "svelte/internal/client";
let a = $.source(1);
let b = $.source(2);
let a = $.state(1);
let b = $.state(2);
let c = 3;
let d = 4;

@ -2,7 +2,7 @@ import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Function_prop_no_getter($$anchor) {
let count = $.source(0);
let count = $.state(0);
function onmouseup() {
$.set(count, $.get(count) + 2);

@ -11,8 +11,8 @@ function reset(_, str, tpl) {
var root = $.template(`<input> <input> <button>reset</button>`, 1);
export default function State_proxy_literal($$anchor) {
let str = $.source('');
let tpl = $.source(``);
let str = $.state('');
let tpl = $.state(``);
var fragment = root();
var input = $.first_child(fragment);

Loading…
Cancel
Save