UNPKG

svelte

Version:

Cybernetically enhanced web apps

199 lines (175 loc) • 5.66 kB
/** @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; } }