svelte
Version:
Cybernetically enhanced web apps
199 lines (175 loc) • 5.66 kB
JavaScript
/** @import { StoreReferencesContainer } from '#client' */
/** @import { Store } from '#shared' */
import { subscribe_to_store } from '../../../store/utils.js';
import { get as get_store } from '../../../store/shared/index.js';
import { define_property, noop } from '../../shared/utils.js';
import { get } from '../runtime.js';
import { teardown } from './effects.js';
import { mutable_source, set } from './sources.js';
/**
* Whether or not the prop currently being read is a store binding, as in
* `<Child bind:x={$y} />`. If it is, we treat the prop as mutable even in
* runes mode, and skip `binding_property_non_reactive` validation
*/
let is_store_binding = false;
let IS_UNMOUNTED = Symbol();
/**
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy
* signal that will be updated when the store is. The store references container is needed to
* track reassignments to stores and to track the correct component context.
* @template V
* @param {Store<V> | null | undefined} store
* @param {string} store_name
* @param {StoreReferencesContainer} stores
* @returns {V}
*/
export function store_get(store, store_name, stores) {
const entry = (stores[store_name] ??= {
store: null,
source: mutable_source(undefined),
unsubscribe: noop
});
// if the component that setup this is already unmounted we don't want to register a subscription
if (entry.store !== store && !(IS_UNMOUNTED in stores)) {
entry.unsubscribe();
entry.store = store ?? null;
if (store == null) {
entry.source.v = undefined; // see synchronous callback comment below
entry.unsubscribe = noop;
} else {
var is_synchronous_callback = true;
entry.unsubscribe = subscribe_to_store(store, (v) => {
if (is_synchronous_callback) {
// If the first updates to the store value (possibly multiple of them) are synchronously
// inside a derived, we will hit the `state_unsafe_mutation` error if we `set` the value
entry.source.v = v;
} else {
set(entry.source, v);
}
});
is_synchronous_callback = false;
}
}
// if the component that setup this stores is already unmounted the source will be out of sync
// so we just use the `get` for the stores, less performant but it avoids to create a memory leak
// and it will keep the value consistent
if (store && IS_UNMOUNTED in stores) {
return get_store(store);
}
return get(entry.source);
}
/**
* Unsubscribe from a store if it's not the same as the one in the store references container.
* We need this in addition to `store_get` because someone could unsubscribe from a store but
* then never subscribe to the new one (if any), causing the subscription to stay open wrongfully.
* @param {Store<any> | null | undefined} store
* @param {string} store_name
* @param {StoreReferencesContainer} stores
*/
export function store_unsub(store, store_name, stores) {
/** @type {StoreReferencesContainer[''] | undefined} */
let entry = stores[store_name];
if (entry && entry.store !== store) {
// Don't reset store yet, so that store_get above can resubscribe to new store if necessary
entry.unsubscribe();
entry.unsubscribe = noop;
}
return store;
}
/**
* Sets the new value of a store and returns that value.
* @template V
* @param {Store<V>} store
* @param {V} value
* @returns {V}
*/
export function store_set(store, value) {
store.set(value);
return value;
}
/**
* @param {StoreReferencesContainer} stores
* @param {string} store_name
*/
export function invalidate_store(stores, store_name) {
var entry = stores[store_name];
if (entry.store !== null) {
store_set(entry.store, entry.source.v);
}
}
/**
* Unsubscribes from all auto-subscribed stores on destroy
* @returns {[StoreReferencesContainer, ()=>void]}
*/
export function setup_stores() {
/** @type {StoreReferencesContainer} */
const stores = {};
function cleanup() {
teardown(() => {
for (var store_name in stores) {
const ref = stores[store_name];
ref.unsubscribe();
}
define_property(stores, IS_UNMOUNTED, {
enumerable: false,
value: true
});
});
}
return [stores, cleanup];
}
/**
* Updates a store with a new value.
* @param {Store<V>} store the store to update
* @param {any} expression the expression that mutates the store
* @param {V} new_value the new store value
* @template V
*/
export function store_mutate(store, expression, new_value) {
store.set(new_value);
return expression;
}
/**
* @param {Store<number>} store
* @param {number} store_value
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_store(store, store_value, d = 1) {
store.set(store_value + d);
return store_value;
}
/**
* @param {Store<number>} store
* @param {number} store_value
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_store(store, store_value, d = 1) {
const value = store_value + d;
store.set(value);
return value;
}
/**
* Called inside prop getters to communicate that the prop is a store binding
*/
export function mark_store_binding() {
is_store_binding = true;
}
/**
* Returns a tuple that indicates whether `fn()` reads a prop that is a store binding.
* Used to prevent `binding_property_non_reactive` validation false positives and
* ensure that these props are treated as mutable even in runes mode
* @template T
* @param {() => T} fn
* @returns {[T, boolean]}
*/
export function capture_store_binding(fn) {
var previous_is_store_binding = is_store_binding;
try {
is_store_binding = false;
return [fn(), is_store_binding];
} finally {
is_store_binding = previous_is_store_binding;
}
}