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

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

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

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() { 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(() => { const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries()); 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; let size = 30;
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let current = $.derived(() => { let current = $.derived(() => {
let result = 0; let result = 0;
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {

@ -11,7 +11,7 @@ function count(number) {
} }
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let current = head; let current = head;
let list = []; let list = [];
for (let i = 0; i < width; i++) { 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'; import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
const double = $.derived(() => $.get(head) * 2); const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head)); const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => { let current = $.derived(() => {

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

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

@ -207,7 +207,7 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') { 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 (binding.kind === 'store_sub') {
if (store_setup.length === 0) { if (store_setup.length === 0) {

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

@ -126,7 +126,7 @@ export function VariableDeclaration(node, context) {
value = b.call('$.proxy', value); value = b.call('$.proxy', value);
} }
if (is_state_source(binding, context.state.analysis)) { if (is_state_source(binding, context.state.analysis)) {
value = b.call('$.source', value); value = b.call('$.state', value);
} }
return value; return value;
}; };
@ -291,7 +291,7 @@ export function VariableDeclaration(node, context) {
*/ */
function create_state_declarators(declarator, scope, value) { function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') { 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'); 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); const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator( return b.declarator(
path.node, 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} */ /** @type {Effect | null} */
var catch_effect; var catch_effect;
var input_source = runes var input_source = (runes ? source : mutable_source)(/** @type {V} */ (undefined));
? source(/** @type {V} */ (undefined)) var error_source = (runes ? source : mutable_source)(undefined);
: mutable_source(/** @type {V} */ (undefined));
var error_source = runes ? source(undefined) : mutable_source(undefined);
var resolved = false; var resolved = false;
/** /**

@ -105,7 +105,7 @@ export {
user_effect, user_effect,
user_pre_effect user_pre_effect
} from './reactivity/effects.js'; } 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 { export {
prop, prop,
rest_props, 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 // 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)) { 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); sources.set(prop, s);
} }
@ -170,7 +170,7 @@ export function proxy(value, parent = null, prev) {
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { if (s === undefined) {
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, null); s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
sources.set(prop, s); sources.set(prop, s);
} }

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

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

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

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

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

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

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

Loading…
Cancel
Save