shelving
Version:
Toolkit for using data in JavaScript.
109 lines (108 loc) • 4.28 kB
JavaScript
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);
}
}