chore: better store subscriptions (#12277)

* use existing teardown function

* set up stores and unsubscriptions in one go

* we never set last_value, this is gibberish

* simplify

* tidy up

* inline

* more
pull/12278/head
Rich Harris 3 months ago committed by GitHub
parent 15873ab09b
commit f2179c90e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: tidy up store logic

@ -154,19 +154,12 @@ export function client_component(source, analysis, options) {
} }
if (binding.kind === 'store_sub') { if (binding.kind === 'store_sub') {
if (store_setup.length === 0) { if (store_setup.length === 0) {
store_setup.push( store_setup.push(b.const('$$stores', b.call('$.setup_stores')));
b.const('$$subscriptions', b.object([])),
b.stmt(b.call('$.unsubscribe_on_destroy', b.id('$$subscriptions')))
);
} }
// We're creating an arrow function that gets the store value which minifies better for two or more references // We're creating an arrow function that gets the store value which minifies better for two or more references
const store_reference = serialize_get_binding(b.id(name.slice(1)), instance_state); const store_reference = serialize_get_binding(b.id(name.slice(1)), instance_state);
const store_get = b.call( const store_get = b.call('$.store_get', store_reference, b.literal(name), b.id('$$stores'));
'$.store_get',
store_reference,
b.literal(name),
b.id('$$subscriptions')
);
store_setup.push( store_setup.push(
b.const( b.const(
binding.node, binding.node,

@ -345,7 +345,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
} }
if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') { if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') {
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$subscriptions')); return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores'));
} else { } else {
return call; return call;
} }

@ -2438,7 +2438,7 @@ export const template_visitors = {
b.thunk(b.sequence(indirect_dependencies)) b.thunk(b.sequence(indirect_dependencies))
); );
const invalidate_store = store_to_invalidate const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$subscriptions'), b.literal(store_to_invalidate)) ? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined; : undefined;
const sequence = []; const sequence = [];

@ -111,10 +111,10 @@ export {
export { export {
invalidate_store, invalidate_store,
mutate_store, mutate_store,
setup_stores,
store_get, store_get,
store_set, store_set,
store_unsub, store_unsub,
unsubscribe_on_destroy,
update_pre_store, update_pre_store,
update_store update_store
} from './reactivity/store.js'; } from './reactivity/store.js';

@ -1,10 +1,10 @@
/** @import { StoreReferencesContainer, Source } from '#client' */ /** @import { StoreReferencesContainer } from '#client' */
/** @import { Store } from '#shared' */ /** @import { Store } from '#shared' */
import { subscribe_to_store } from '../../../store/utils.js'; import { subscribe_to_store } from '../../../store/utils.js';
import { noop } from '../../shared/utils.js'; import { noop } from '../../shared/utils.js';
import { UNINITIALIZED } from '../../../constants.js'; import { UNINITIALIZED } from '../../../constants.js';
import { get, untrack } from '../runtime.js'; import { get } from '../runtime.js';
import { effect } from './effects.js'; import { teardown } from './effects.js';
import { mutable_source, set } from './sources.js'; import { mutable_source, set } from './sources.js';
/** /**
@ -18,30 +18,25 @@ import { mutable_source, set } from './sources.js';
* @returns {V} * @returns {V}
*/ */
export function store_get(store, store_name, stores) { export function store_get(store, store_name, stores) {
/** @type {StoreReferencesContainer[''] | undefined} */ const entry = (stores[store_name] ??= {
let entry = stores[store_name]; store: null,
const is_new = entry === undefined; source: mutable_source(UNINITIALIZED),
unsubscribe: noop
if (is_new) { });
entry = {
store: null,
last_value: null,
value: mutable_source(UNINITIALIZED),
unsubscribe: noop
};
stores[store_name] = entry;
}
if (is_new || entry.store !== store) { if (entry.store !== store) {
entry.unsubscribe(); entry.unsubscribe();
entry.store = store ?? null; entry.store = store ?? null;
entry.unsubscribe = connect_store_to_signal(store, entry.value);
if (store == null) {
set(entry.source, undefined);
entry.unsubscribe = noop;
} else {
entry.unsubscribe = subscribe_to_store(store, (v) => set(entry.source, v));
}
} }
const value = get(entry.value); return get(entry.source);
// This could happen if the store was cleaned up because the component was destroyed and there's a leak on the user side.
// In that case we don't want to fail with a cryptic Symbol error, but rather return the last value we got.
return value === UNINITIALIZED ? entry.last_value : value;
} }
/** /**
@ -65,20 +60,6 @@ export function store_unsub(store, store_name, stores) {
return store; return store;
} }
/**
* @template V
* @param {Store<V> | null | undefined} store
* @param {Source<V>} source
*/
function connect_store_to_signal(store, source) {
if (store == null) {
set(source, undefined);
return noop;
}
return subscribe_to_store(store, (v) => set(source, v));
}
/** /**
* Sets the new value of a store and returns that value. * Sets the new value of a store and returns that value.
* @template V * @template V
@ -96,24 +77,28 @@ export function store_set(store, value) {
* @param {string} store_name * @param {string} store_name
*/ */
export function invalidate_store(stores, store_name) { export function invalidate_store(stores, store_name) {
const store = stores[store_name]; var entry = stores[store_name];
if (store.store) { if (entry.store !== null) {
store_set(store.store, store.value.v); store_set(entry.store, entry.source.v);
} }
} }
/** /**
* Unsubscribes from all auto-subscribed stores on destroy * Unsubscribes from all auto-subscribed stores on destroy
* @param {StoreReferencesContainer} stores * @returns {StoreReferencesContainer}
*/ */
export function unsubscribe_on_destroy(stores) { export function setup_stores() {
on_destroy(() => { /** @type {StoreReferencesContainer} */
let store_name; const stores = {};
for (store_name in stores) {
teardown(() => {
for (var store_name in stores) {
const ref = stores[store_name]; const ref = stores[store_name];
ref.unsubscribe(); ref.unsubscribe();
} }
}); });
return stores;
} }
/** /**
@ -150,12 +135,3 @@ export function update_pre_store(store, store_value, d = 1) {
store.set(value); store.set(value);
return value; return value;
} }
/**
* Schedules a callback to run immediately before the component is unmounted.
* @param {() => any} fn
* @returns {void}
*/
function on_destroy(fn) {
effect(() => () => untrack(fn));
}

@ -148,9 +148,8 @@ export type StoreReferencesContainer = Record<
string, string,
{ {
store: Store<any> | null; store: Store<any> | null;
last_value: any;
unsubscribe: Function; unsubscribe: Function;
value: Source<any>; source: Source<any>;
} }
>; >;

Loading…
Cancel
Save