shelving
Version:
Toolkit for using data in JavaScript.
75 lines (74 loc) • 2.35 kB
JavaScript
import { MINUTE, NONE } from "../util/constants.js";
import { isDeepEqual } from "../util/equal.js";
import { Store } from "./Store.js";
/**
* Store object that loads a result from an API endpoint and manages its state.
*
* @todo Needs support for `EndpointOptions` to set headers etc.
*/
export class EndpointStore extends Store {
static CANCELLED = Symbol("CANCELLED");
endpoint;
_payload;
_abort = undefined;
get payload() {
return this._payload;
}
set payload(next) {
const current = this._payload;
// Did the payload actually change?
if (!isDeepEqual(current, next)) {
this._payload = next;
// If there's already a fetch in progress, cancel it.
if (this._abort) {
this._abort.abort(EndpointStore.CANCELLED);
this._abort = undefined;
}
// Trigger a fetch.
this._call();
}
}
/** Maximum age data can be before a fetch is triggered (defaults to 5 minutes). */
maxAge = 5 * MINUTE;
// Override to possibly trigger a fetch when value is read.
get value() {
// Queue `this.call()` if...
// 1. Value is still loading.
// 2. Value is stale (older than maxAge).
if (this.loading || this.age > this.maxAge)
if (!this.reason)
this._call();
return super.value;
}
set value(value) {
super.value = value;
}
constructor(endpoint, payload) {
super(NONE);
this.endpoint = endpoint;
this.payload = payload;
}
refetch() {
return this._call();
}
/** Call the API now to fetch the data. */
async _call() {
if (this._abort)
return; // Already fetching.
this.reason = undefined; // Optimistically clear any error.
try {
this._abort = new AbortController();
const value = await this.endpoint.fetch(this._payload, { signal: this._abort.signal });
this.value = value;
}
catch (thrown) {
console.error(thrown);
if (thrown === EndpointStore.CANCELLED)
return; // Cancelled on purpose.
this.reason = thrown;
}
finally {
this._abort = undefined;
}
}
}