UNPKG

@amadeus-it-group/tansu

Version:

tansu is a lightweight, push-based framework-agnostic state management library. It borrows the ideas and APIs originally designed and implemented by Svelte stores and extends them with computed and batch.

1,085 lines (1,068 loc) 36.5 kB
/** * Default implementation of the equal function used by tansu when a store * changes, to know if listeners need to be notified. * Returns false if `a` is a function or an object, or if `a` and `b` * are different according to `Object.is`. Otherwise, returns true. * * @param a - First value to compare. * @param b - Second value to compare. * @returns true if a and b are considered equal. */ const equal = (a, b) => Object.is(a, b) && (!a || typeof a !== 'object') && typeof a !== 'function'; const subscribersQueue = []; let willProcessQueue = false; /** * Batches multiple changes to stores while calling the provided function, * preventing derived stores from updating until the function returns, * to avoid unnecessary recomputations. * * @remarks * * If a store is updated multiple times in the provided function, existing * subscribers of that store will only be called once when the provided * function returns. * * Note that even though the computation of derived stores is delayed in most * cases, some computations of derived stores will still occur inside * the function provided to batch if a new subscriber is added to a store, because * calling {@link SubscribableStore.subscribe | subscribe} always triggers a * synchronous call of the subscriber and because tansu always provides up-to-date * values when calling subscribers. Especially, calling {@link get} on a store will * always return the correct up-to-date value and can trigger derived store * intermediate computations, even inside batch. * * It is possible to have nested calls of batch, in which case only the first * (outer) call has an effect, inner calls only call the provided function. * * @param fn - a function that can update stores. Its returned value is * returned by the batch function. * * @example * Using batch in the following example prevents logging the intermediate "Sherlock Lupin" value. * * ```typescript * const firstName = writable('Arsène'); * const lastName = writable('Lupin'); * const fullName = derived([firstName, lastName], ([a, b]) => `${a} ${b}`); * fullName.subscribe((name) => console.log(name)); // logs any change to fullName * batch(() => { * firstName.set('Sherlock'); * lastName.set('Holmes'); * }); * ``` */ const batch = (fn) => { const needsProcessQueue = !willProcessQueue; willProcessQueue = true; let success = true; let res; let error; try { res = fn(); } finally { if (needsProcessQueue) { while (subscribersQueue.length > 0) { const consumer = subscribersQueue.shift(); try { consumer.notify(); } catch (e) { // an error in one consumer should not impact others if (success) { // will throw the first error success = false; error = e; } } } willProcessQueue = false; } } if (success) { return res; } throw error; }; const updateLinkProducerValue = (link) => { try { link.skipMarkDirty = true; link.producer.updateValue(); // Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!) // there should be no way to trigger this error. /* v8 ignore next 3 */ if (link.producer.flags & 16 /* RawStoreFlags.DIRTY */) { throw new Error('assert failed: store still dirty after updating it'); } } finally { link.skipMarkDirty = false; } }; const noop = () => { }; const bind = (object, fnName) => { const fn = object ? object[fnName] : null; return typeof fn === 'function' ? fn.bind(object) : noop; }; const noopSubscriber = { next: noop, pause: noop, resume: noop, }; const toSubscriberObject = (subscriber) => ({ next: typeof subscriber === 'function' ? subscriber.bind(null) : bind(subscriber, 'next'), pause: bind(subscriber, 'pause'), resume: bind(subscriber, 'resume'), }); class SubscribeConsumer { constructor(producer, subscriber) { this.dirtyCount = 1; this.subscriber = toSubscriberObject(subscriber); this.link = producer.registerConsumer(producer.newLink(this)); this.notify(true); } unsubscribe() { if (this.subscriber !== noopSubscriber) { this.subscriber = noopSubscriber; this.link.producer.unregisterConsumer(this.link); } } markDirty() { this.dirtyCount++; subscribersQueue.push(this); if (this.dirtyCount === 1) { this.subscriber.pause(); } } notify(first = false) { this.dirtyCount--; if (this.dirtyCount === 0 && this.subscriber !== noopSubscriber) { const link = this.link; const producer = link.producer; updateLinkProducerValue(link); if (producer.isLinkUpToDate(link) && !first) { this.subscriber.resume(); } else { // note that the following line can throw const value = producer.updateLink(link); this.subscriber.next(value); } } } } let activeConsumer = null; const setActiveConsumer = (consumer) => { const prevConsumer = activeConsumer; activeConsumer = consumer; return prevConsumer; }; /** * Stops the tracking of dependencies made by {@link computed} and calls the provided function. * After the function returns, the tracking of dependencies continues as before. * * @param fn - function to be called * @returns the value returned by the given function */ const untrack = (fn) => { let output; const prevActiveConsumer = setActiveConsumer(null); try { output = fn(); } finally { setActiveConsumer(prevActiveConsumer); } return output; }; let notificationPhase = false; const checkNotInNotificationPhase = () => { if (notificationPhase) { throw new Error('Reading or writing a signal is forbidden during the notification phase.'); } }; let epoch = 0; class RawStoreWritable { constructor(value) { this.value = value; this.flags = 0 /* RawStoreFlags.NONE */; this.version = 0; this.equalFn = (equal); this.equalCache = null; this.consumerLinks = []; } newLink(consumer) { return { version: -1, value: undefined, producer: this, indexInProducer: 0, consumer, skipMarkDirty: false, }; } isLinkUpToDate(link) { if (link.version === this.version) { return true; } if (link.version === this.version - 1 || link.version < 0) { return false; } let equalCache = this.equalCache; if (!equalCache) { equalCache = {}; this.equalCache = equalCache; } let res = equalCache[link.version]; if (res === undefined) { res = this.equal(link.value, this.value); equalCache[link.version] = res; } return res; } updateLink(link) { link.value = this.value; link.version = this.version; return this.readValue(); } registerConsumer(link) { const consumerLinks = this.consumerLinks; const indexInProducer = consumerLinks.length; link.indexInProducer = indexInProducer; consumerLinks[indexInProducer] = link; return link; } unregisterConsumer(link) { const consumerLinks = this.consumerLinks; const index = link.indexInProducer; // Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!) // there should be no way to trigger this error. /* v8 ignore next 3 */ if (consumerLinks[index] !== link) { throw new Error('assert failed: invalid indexInProducer'); } // swap with the last item to avoid shifting the array const lastConsumerLink = consumerLinks.pop(); const isLast = link === lastConsumerLink; if (!isLast) { consumerLinks[index] = lastConsumerLink; lastConsumerLink.indexInProducer = index; } else if (index === 0) { this.checkUnused(); } } checkUnused() { } updateValue() { } equal(a, b) { const equalFn = this.equalFn; return equalFn(a, b); } increaseEpoch() { epoch++; this.markConsumersDirty(); } set(newValue) { checkNotInNotificationPhase(); const same = this.equal(this.value, newValue); if (!same) { batch(() => { this.value = newValue; this.version++; this.equalCache = null; this.increaseEpoch(); }); } } update(updater) { this.set(updater(this.value)); } markConsumersDirty() { const prevNotificationPhase = notificationPhase; notificationPhase = true; try { const consumerLinks = this.consumerLinks; for (let i = 0, l = consumerLinks.length; i < l; i++) { const link = consumerLinks[i]; if (link.skipMarkDirty) continue; link.consumer.markDirty(); } } finally { notificationPhase = prevNotificationPhase; } } get() { checkNotInNotificationPhase(); return activeConsumer ? activeConsumer.addProducer(this) : this.readValue(); } readValue() { return this.value; } subscribe(subscriber) { checkNotInNotificationPhase(); const subscription = new SubscribeConsumer(this, subscriber); const unsubscriber = () => subscription.unsubscribe(); unsubscriber.unsubscribe = unsubscriber; return unsubscriber; } } let flushUnusedQueue = null; let inFlushUnused = false; const flushUnused = () => { // Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!) // there should be no way to trigger this error. /* v8 ignore next 3 */ if (inFlushUnused) { throw new Error('assert failed: recursive flushUnused call'); } inFlushUnused = true; try { const queue = flushUnusedQueue; if (queue) { flushUnusedQueue = null; for (let i = 0, l = queue.length; i < l; i++) { const producer = queue[i]; producer.flags &= ~4 /* RawStoreFlags.FLUSH_PLANNED */; producer.checkUnused(); } } } finally { inFlushUnused = false; } }; class RawStoreTrackingUsage extends RawStoreWritable { constructor() { super(...arguments); this.extraUsages = 0; } updateValue() { const flags = this.flags; if (!(flags & 2 /* RawStoreFlags.START_USE_CALLED */)) { // Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!) // there should be no way to trigger this error. /* v8 ignore next 3 */ if (!this.extraUsages && !this.consumerLinks.length) { throw new Error('assert failed: untracked producer usage'); } this.flags |= 2 /* RawStoreFlags.START_USE_CALLED */; untrack(() => this.startUse()); } } checkUnused() { const flags = this.flags; if (flags & 2 /* RawStoreFlags.START_USE_CALLED */ && !this.extraUsages && !this.consumerLinks.length) { if (inFlushUnused || flags & 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */) { this.flags &= ~2 /* RawStoreFlags.START_USE_CALLED */; untrack(() => this.endUse()); } else if (!(flags & 4 /* RawStoreFlags.FLUSH_PLANNED */)) { this.flags |= 4 /* RawStoreFlags.FLUSH_PLANNED */; if (!flushUnusedQueue) { flushUnusedQueue = []; queueMicrotask(flushUnused); } flushUnusedQueue.push(this); } } } get() { checkNotInNotificationPhase(); if (activeConsumer) { return activeConsumer.addProducer(this); } else { this.extraUsages++; try { this.updateValue(); // Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!) // there should be no way to trigger this error. /* v8 ignore next 3 */ if (this.flags & 16 /* RawStoreFlags.DIRTY */) { throw new Error('assert failed: store still dirty after updating it'); } return this.readValue(); } finally { const extraUsages = --this.extraUsages; if (extraUsages === 0) { this.checkUnused(); } } } } } const noopUnsubscribe = () => { }; noopUnsubscribe.unsubscribe = noopUnsubscribe; const normalizeUnsubscribe = (unsubscribe) => { if (!unsubscribe) { return noopUnsubscribe; } if (unsubscribe.unsubscribe === unsubscribe) { return unsubscribe; } const res = typeof unsubscribe === 'function' ? () => unsubscribe() : () => unsubscribe.unsubscribe(); res.unsubscribe = res; return res; }; class RawSubscribableWrapper extends RawStoreTrackingUsage { constructor(subscribable) { super(undefined); this.subscribable = subscribable; this.subscriber = this.createSubscriber(); this.unsubscribe = null; this.flags = 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */; } createSubscriber() { const subscriber = (value) => this.set(value); subscriber.next = subscriber; subscriber.pause = () => { this.markConsumersDirty(); }; return subscriber; } startUse() { this.unsubscribe = normalizeUnsubscribe(this.subscribable.subscribe(this.subscriber)); } endUse() { const unsubscribe = this.unsubscribe; if (unsubscribe) { this.unsubscribe = null; unsubscribe(); } } } /** * Symbol used in {@link InteropObservable} allowing any object to expose an observable. */ const symbolObservable = (typeof Symbol === 'function' && Symbol.observable) || '@@observable'; const returnThis = function () { return this; }; const rawStoreSymbol = Symbol(); const rawStoreMap = new WeakMap(); const getRawStore = (storeInput) => { const rawStore = storeInput[rawStoreSymbol]; if (rawStore) { return rawStore; } let res = rawStoreMap.get(storeInput); if (!res) { let subscribable = storeInput; if (!('subscribe' in subscribable)) { subscribable = subscribable[symbolObservable](); } res = new RawSubscribableWrapper(subscribable); rawStoreMap.set(storeInput, res); } return res; }; const exposeRawStore = (rawStore, extraProp) => { const get = rawStore.get.bind(rawStore); if (extraProp) { Object.assign(get, extraProp); } get.get = get; get.subscribe = rawStore.subscribe.bind(rawStore); get[symbolObservable] = returnThis; get[rawStoreSymbol] = rawStore; return get; }; const MAX_CHANGE_RECOMPUTES = 1000; const COMPUTED_UNSET = Symbol('UNSET'); const COMPUTED_ERRORED = Symbol('ERRORED'); const isComputedSpecialValue = (value) => value === COMPUTED_UNSET || value === COMPUTED_ERRORED; class RawStoreComputedOrDerived extends RawStoreTrackingUsage { constructor() { super(...arguments); this.flags = 16 /* RawStoreFlags.DIRTY */; } equal(a, b) { if (isComputedSpecialValue(a) || isComputedSpecialValue(b)) { return false; } return super.equal(a, b); } markDirty() { if (!(this.flags & 16 /* RawStoreFlags.DIRTY */)) { this.flags |= 16 /* RawStoreFlags.DIRTY */; this.markConsumersDirty(); } } readValue() { const value = this.value; if (value === COMPUTED_ERRORED) { throw this.error; } // Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!) // there should be no way to trigger this error. /* v8 ignore next 3 */ if (value === COMPUTED_UNSET) { throw new Error('assert failed: computed value is not set'); } return value; } updateValue() { if (this.flags & 8 /* RawStoreFlags.COMPUTING */) { throw new Error('recursive computed'); } super.updateValue(); if (!(this.flags & 16 /* RawStoreFlags.DIRTY */)) { return; } this.flags |= 8 /* RawStoreFlags.COMPUTING */; const prevActiveConsumer = setActiveConsumer(null); try { let iterations = 0; do { do { iterations++; this.flags &= ~16 /* RawStoreFlags.DIRTY */; if (this.areProducersUpToDate()) { return; } } while (this.flags & 16 /* RawStoreFlags.DIRTY */ && iterations < MAX_CHANGE_RECOMPUTES); this.recompute(); } while (this.flags & 16 /* RawStoreFlags.DIRTY */ && iterations < MAX_CHANGE_RECOMPUTES); if (this.flags & 16 /* RawStoreFlags.DIRTY */) { this.flags &= ~16 /* RawStoreFlags.DIRTY */; this.error = new Error('reached maximum number of store changes in one shot'); this.set(COMPUTED_ERRORED); } } finally { setActiveConsumer(prevActiveConsumer); this.flags &= ~8 /* RawStoreFlags.COMPUTING */; } } } class RawStoreComputed extends RawStoreComputedOrDerived { constructor(computeFn) { super(COMPUTED_UNSET); this.computeFn = computeFn; this.producerIndex = 0; this.producerLinks = []; this.epoch = -1; } increaseEpoch() { // do nothing } updateValue() { const flags = this.flags; if (flags & 2 /* RawStoreFlags.START_USE_CALLED */ && this.epoch === epoch) { return; } super.updateValue(); this.epoch = epoch; } get() { if (!activeConsumer && !notificationPhase && this.epoch === epoch && (!(this.flags & 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */) || this.flags & 2 /* RawStoreFlags.START_USE_CALLED */)) { return this.readValue(); } return super.get(); } addProducer(producer) { const producerLinks = this.producerLinks; const producerIndex = this.producerIndex; let link = producerLinks[producerIndex]; if (link?.producer !== producer) { if (link) { producerLinks.push(link); // push the existing link at the end (to be removed later) } link = producer.registerConsumer(producer.newLink(this)); } producerLinks[producerIndex] = link; this.producerIndex = producerIndex + 1; updateLinkProducerValue(link); if (producer.flags & 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */) { this.flags |= 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */; } return producer.updateLink(link); } startUse() { const producerLinks = this.producerLinks; for (let i = 0, l = producerLinks.length; i < l; i++) { const link = producerLinks[i]; link.producer.registerConsumer(link); } this.flags |= 16 /* RawStoreFlags.DIRTY */; } endUse() { const producerLinks = this.producerLinks; for (let i = 0, l = producerLinks.length; i < l; i++) { const link = producerLinks[i]; link.producer.unregisterConsumer(link); } } areProducersUpToDate() { if (this.value === COMPUTED_UNSET) { return false; } const producerLinks = this.producerLinks; for (let i = 0, l = producerLinks.length; i < l; i++) { const link = producerLinks[i]; const producer = link.producer; updateLinkProducerValue(link); if (!producer.isLinkUpToDate(link)) { return false; } } return true; } recompute() { let value; const prevActiveConsumer = setActiveConsumer(this); try { this.producerIndex = 0; this.flags &= ~1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */; const computeFn = this.computeFn; value = computeFn(); this.error = null; } catch (error) { value = COMPUTED_ERRORED; this.error = error; } finally { setActiveConsumer(prevActiveConsumer); } // Remove unused producers: const producerLinks = this.producerLinks; const producerIndex = this.producerIndex; if (producerIndex < producerLinks.length) { for (let i = 0, l = producerLinks.length - producerIndex; i < l; i++) { const link = producerLinks.pop(); link.producer.unregisterConsumer(link); } } this.set(value); } } class RawStoreConst { constructor(value) { this.value = value; this.flags = 0 /* RawStoreFlags.NONE */; } newLink(_consumer) { return { producer: this, }; } registerConsumer(link) { return link; } unregisterConsumer(_link) { } updateValue() { } isLinkUpToDate(_link) { return true; } updateLink(_link) { return this.value; } get() { checkNotInNotificationPhase(); return this.value; } subscribe(subscriber) { checkNotInNotificationPhase(); if (typeof subscriber === 'function') { subscriber(this.value); } else { subscriber?.next?.(this.value); } return noopUnsubscribe; } } class RawStoreDerived extends RawStoreComputedOrDerived { constructor(producers, initialValue) { super(initialValue); this.producerLinks = null; this.cleanUpFn = null; this.flags = 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */ | 16 /* RawStoreFlags.DIRTY */; const arrayMode = Array.isArray(producers); this.arrayMode = arrayMode; this.producers = (arrayMode ? producers : [producers]).map(getRawStore); } callCleanUpFn() { const cleanUpFn = this.cleanUpFn; if (cleanUpFn) { this.cleanUpFn = null; cleanUpFn(); } } startUse() { this.producerLinks = this.producers.map((producer) => producer.registerConsumer(producer.newLink(this))); this.flags |= 16 /* RawStoreFlags.DIRTY */; } endUse() { this.callCleanUpFn(); const producerLinks = this.producerLinks; this.producerLinks = null; if (producerLinks) { for (let i = 0, l = producerLinks.length; i < l; i++) { const link = producerLinks[i]; link.producer.unregisterConsumer(link); } } } areProducersUpToDate() { const producerLinks = this.producerLinks; let alreadyUpToDate = this.value !== COMPUTED_UNSET; for (let i = 0, l = producerLinks.length; i < l; i++) { const link = producerLinks[i]; const producer = link.producer; updateLinkProducerValue(link); if (!producer.isLinkUpToDate(link)) { alreadyUpToDate = false; } } return alreadyUpToDate; } recompute() { try { this.callCleanUpFn(); const values = this.producerLinks.map((link) => link.producer.updateLink(link)); this.cleanUpFn = normalizeUnsubscribe(this.derive(this.arrayMode ? values : values[0])); } catch (error) { this.error = error; this.set(COMPUTED_ERRORED); } } } class RawStoreDerivedStore extends RawStoreDerived { constructor(stores, initialValue, derive) { super(stores, initialValue); this.derive = derive; } } class RawStoreSyncDerived extends RawStoreDerived { constructor(stores, _initialValue, deriveFn) { super(stores, COMPUTED_UNSET); this.deriveFn = deriveFn; } derive(values) { const deriveFn = this.deriveFn; this.set(deriveFn(values)); } } const createOnUseArg = (store) => { const setFn = store.set.bind(store); setFn.set = setFn; setFn.update = store.update.bind(store); return setFn; }; class RawStoreAsyncDerived extends RawStoreDerived { constructor(stores, initialValue, deriveFn) { super(stores, initialValue); this.deriveFn = deriveFn; this.setFn = createOnUseArg(this); } derive(values) { const deriveFn = this.deriveFn; return deriveFn(values, this.setFn); } } class RawStoreWithOnUse extends RawStoreTrackingUsage { constructor(value, onUseFn) { super(value); this.onUseFn = onUseFn; this.cleanUpFn = null; this.flags = 1 /* RawStoreFlags.HAS_VISIBLE_ONUSE */; } startUse() { this.cleanUpFn = normalizeUnsubscribe(this.onUseFn()); } endUse() { const cleanUpFn = this.cleanUpFn; if (cleanUpFn) { this.cleanUpFn = null; cleanUpFn(); } } } /** * tansu is a lightweight, push-based state management library. * It borrows the ideas and APIs originally designed and implemented by {@link https://github.com/sveltejs/rfcs/blob/master/text/0002-reactive-stores.md | Svelte stores}. * * @packageDocumentation */ function asReadable(store, extraProp) { return exposeRawStore(getRawStore(store), extraProp); } const defaultUpdate = function (updater) { this.set(updater(untrack(() => this.get()))); }; function asWritable(store, setOrExtraProps) { return asReadable(store, typeof setOrExtraProps === 'function' ? { set: setOrExtraProps, update: defaultUpdate } : { ...setOrExtraProps, set: setOrExtraProps?.set ?? noop, update: setOrExtraProps?.update ?? (setOrExtraProps?.set ? defaultUpdate : noop), }); } /** * A utility function to get the current value from a given store. * It works by subscribing to a store, capturing the value (synchronously) and unsubscribing just after. * * @param store - a store from which the current value is retrieved. * * @example * ```typescript * const myStore = writable(1); * console.log(get(myStore)); // logs 1 * ``` */ const get = (store) => getRawStore(store).get(); /** * Base class that can be extended to easily create a custom {@link Readable} store. * * @example * ```typescript * class CounterStore extends Store { * constructor() { * super(1); // initial value * } * * reset() { * this.set(0); * } * * increment() { * this.update(value => value + 1); * } * } * * const store = new CounterStore(1); * * // logs 1 (initial value) upon subscription * const unsubscribe = store.subscribe((value) => { * console.log(value); * }); * store.increment(); // logs 2 * store.reset(); // logs 0 * * unsubscribe(); // stops notifications and corresponding logging * ``` */ class Store { /** * * @param value - Initial value of the store */ constructor(value) { let rawStore; if (value instanceof RawStoreWritable) { rawStore = value; } else { const onUse = this.onUse; rawStore = onUse ? new RawStoreWithOnUse(value, onUse.bind(this)) : new RawStoreWritable(value); rawStore.equalFn = (a, b) => this.equal(a, b); } this[rawStoreSymbol] = rawStore; } /** * Compares two values and returns true if they are equal. * It is called when setting a new value to avoid doing anything * (such as notifying subscribers) if the value did not change. * The default logic is to return false if `a` is a function or an object, * or if `a` and `b` are different according to `Object.is`. * This method can be overridden by subclasses to change the logic. * * @remarks * For backward compatibility, the default implementation calls the * deprecated {@link Store.notEqual} method and returns the negation * of its return value. * * @param a - First value to compare. * @param b - Second value to compare. * @returns true if a and b are considered equal. */ equal(a, b) { return !this.notEqual(a, b); } /** * Compares two values and returns true if they are different. * It is called when setting a new value to avoid doing anything * (such as notifying subscribers) if the value did not change. * The default logic is to return true if `a` is a function or an object, * or if `a` and `b` are different according to `Object.is`. * This method can be overridden by subclasses to change the logic. * * @remarks * This method is only called by the default implementation of * {@link Store.equal}, so overriding {@link Store.equal} takes * precedence over overriding notEqual. * * @deprecated Use {@link Store.equal} instead * @param a - First value to compare. * @param b - Second value to compare. * @returns true if a and b are considered different. */ notEqual(a, b) { return !equal(a, b); } /** * Replaces store's state with the provided value. * Equivalent of {@link Writable.set}, but internal to the store. * * @param value - value to be used as the new state of a store. */ set(value) { this[rawStoreSymbol].set(value); } get() { return this[rawStoreSymbol].get(); } /** * Updates store's state by using an {@link Updater} function. * Equivalent of {@link Writable.update}, but internal to the store. * * @param updater - a function that takes the current state as an argument and returns the new state. */ update(updater) { this[rawStoreSymbol].update(updater); } /** * Default Implementation of the {@link SubscribableStore.subscribe}, not meant to be overridden. * @param subscriber - see {@link SubscribableStore.subscribe} */ subscribe(subscriber) { return this[rawStoreSymbol].subscribe(subscriber); } [symbolObservable]() { return this; } } const createStoreWithOnUse = (initValue, onUse) => { const store = new RawStoreWithOnUse(initValue, () => onUse(setFn)); const setFn = createOnUseArg(store); return store; }; const applyStoreOptions = (store, options) => { if (options) { const { equal, notEqual } = options; if (equal) { store.equalFn = equal; } else if (notEqual) { store.equalFn = (a, b) => !notEqual(a, b); } } return store; }; /** * A convenience function to create {@link Readable} store instances. * @param value - Initial value of a readable store. * @param options - Either an object with {@link StoreOptions | store options}, or directly the onUse function. * * The onUse function is a function called when the number of subscribers changes from 0 to 1 * (but not called when the number of subscribers changes from 1 to 2, ...). * * If a function is returned, it will be called when the number of subscribers changes from 1 to 0. * * @example Sample with an onUse function * ```typescript * const clock = readable("00:00", setState => { * const intervalID = setInterval(() => { * const date = new Date(); * setState(`${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`); * }, 1000); * * return () => clearInterval(intervalID); * }); * ``` */ function readable(value, options) { if (typeof options === 'function') { options = { onUse: options }; } const onUse = options?.onUse; return exposeRawStore(onUse ? applyStoreOptions(createStoreWithOnUse(value, onUse), options) : new RawStoreConst(value)); } /** * A convenience function to create {@link Writable} store instances. * @param value - initial value of a new writable store. * @param options - Either an object with {@link StoreOptions | store options}, or directly the onUse function. * * The onUse function is a function called when the number of subscribers changes from 0 to 1 * (but not called when the number of subscribers changes from 1 to 2, ...). * * If a function is returned, it will be called when the number of subscribers changes from 1 to 0. * * @example * ```typescript * const x = writable(0); * * x.update(v => v + 1); // increment * x.set(0); // reset back to the default value * ``` */ function writable(value, options) { if (typeof options === 'function') { options = { onUse: options }; } const onUse = options?.onUse; const store = applyStoreOptions(onUse ? createStoreWithOnUse(value, onUse) : new RawStoreWritable(value), options); const res = exposeRawStore(store); res.set = store.set.bind(store); res.update = store.update.bind(store); return res; } function isSyncDeriveFn(fn) { return fn.length <= 1; } class DerivedStore extends Store { constructor(stores, initialValue) { const rawStore = new RawStoreDerivedStore(stores, initialValue, (values) => this.derive(values)); rawStore.equalFn = (a, b) => this.equal(a, b); super(rawStore); } } function derived(stores, options, initialValue) { if (typeof options === 'function') { options = { derive: options }; } const { derive, ...opts } = options; const Derived = isSyncDeriveFn(derive) ? RawStoreSyncDerived : RawStoreAsyncDerived; return exposeRawStore(applyStoreOptions(new Derived(stores, initialValue, derive), opts)); } /** * Creates a store whose value is computed by the provided function. * * @remarks * * The computation function is first called the first time the store is used. * * It can use the value of other stores or observables and the computation function is called again if the value of those dependencies * changed, as long as the store is still used. * * Dependencies are detected automatically as the computation function gets their value either by calling the stores * as a function (as it is possible with stores implementing {@link ReadableSignal}), or by calling the {@link get} function * (with a store or any observable). If some calls made by the function should not be tracked as dependencies, it is possible * to wrap them in a call to {@link untrack}. * * Note that dependencies can change between calls of the computation function. Internally, tansu will subscribe to new dependencies * when they are used and unsubscribe from dependencies that are no longer used after the call of the computation function. * * @param fn - computation function that returns the value of the store * @param options - store option. Allows to define the {@link StoreOptions.equal|equal} function, if needed * @returns store containing the value returned by the computation function */ function computed(fn, options) { return exposeRawStore(applyStoreOptions(new RawStoreComputed(fn), options)); } export { DerivedStore, Store, asReadable, asWritable, batch, computed, derived, equal, get, readable, symbolObservable, untrack, writable };