UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

109 lines (108 loc) 4.28 kB
import { DeferredSequence } from "../sequence/DeferredSequence.js"; import { NONE } from "../util/constants.js"; import { isDeepEqual } from "../util/equal.js"; import { getStarter } from "../util/start.js"; /** * Store that retains its most recent value and is async-iterable to allow values to be observed. * - Current value can be read at `store.value` and `store.data` * - Stores also send their most-recent value to any new subscribers immediately when a new subscriber is added. * - Stores can also be in a loading store where they do not have a current value. * * @param initial The initial value for the store, a `Promise` that resolves to the initial value, a source `Subscribable` to subscribe to, or another `Store` instance to take the initial value from and subscribe to. * - To set the store to be loading, use the `NONE` constant or a `Promise` value. * - To set the store to an explicit value, use that value or another `Store` instance with a value. * */ export class Store { /** Deferred sequence this store uses to issue values as they change. */ next = new DeferredSequence(); /** Current value of the store (or throw a promise that resolves when this store receives its next value or error). */ get value() { if (this._reason !== undefined) throw this._reason; if (this._value === NONE) throw this.next; return this._value; } set value(value) { this._reason = undefined; if (value === NONE) { this._time = undefined; this._value = value; this.next.cancel(); } else if (this._value === NONE || !this.equal(value, this._value)) { this._time = Date.now(); this._value = value; this.next.resolve(value); } } _value = NONE; /** Is there a current value, or is it still loading. */ get loading() { return this._value === NONE; } /** Time (in milliseconds) this store was last updated with a new value, or `undefined` if this store is currently loading. */ get time() { return this._value === NONE ? undefined : this._time; } _time; /** How old this store's value is (in milliseconds). */ get age() { const time = this.time; return typeof time === "number" ? Date.now() - time : Number.POSITIVE_INFINITY; } /** Current error of this store (or `undefined` if there is no reason). */ get reason() { return this._reason; } set reason(reason) { this._reason = reason; if (reason !== undefined) { this.next.reject(reason); } } _reason = undefined; /** Set a starter for this store to allow a function to execute when this store has subscribers or not. */ set starter(start) { if (this._starter) this._starter.stop(); // Stop the current starter. this._starter = getStarter(start); if (this._iterating) this._starter.start(this); // Start the new starter if we're already iterating. } _starter; /** Store is initiated with an initial store. */ constructor(value, time = Date.now()) { this._value = value; this._time = time; } /** Set the value of the store as values are pulled from a sequence. */ async *through(sequence) { for await (const value of sequence) { this.value = value; yield value; } } // Implement `AsyncIterable` // Issues the current value of the store first, then any subsequent values that are issued. async *[Symbol.asyncIterator]() { await Promise.resolve(); // Introduce a slight delay, i.e. don't immediately yield `this.value` in case it is changed synchronously. this._starter?.start(this); this._iterating++; try { if (!this.loading) yield this.value; yield* this.next; } finally { this._iterating--; if (this._iterating < 1) this._starter?.stop(); } } _iterating = 0; /** Compare two values for this store and return whether they are equal. */ equal(a, b) { return isDeepEqual(a, b); } }