@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
JavaScript
/**
* 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 };