shelving
Version:
Toolkit for using data in JavaScript.
119 lines (118 loc) • 4.5 kB
JavaScript
import { RequiredError } from "../error/RequiredError.js";
import { ArrayStore } from "../store/ArrayStore.js";
import { BooleanStore } from "../store/BooleanStore.js";
import { getGetter } from "../util/class.js";
import { NONE } from "../util/constants.js";
import { getAfterQuery, getLimit } from "../util/query.js";
import { runSequence } from "../util/sequence.js";
/** Store a set of multiple items. */
export class QueryStore extends ArrayStore {
provider;
collection;
query;
busy = new BooleanStore();
limit;
/** Can more items be loaded after the current result. */
get hasMore() {
return this._hasMore;
}
_hasMore = false;
/** Get the first item in this store. */
get first() {
const first = this.optionalFirst;
if (!first)
throw new RequiredError(`First item does not exist in collection "${this.collection}"`, {
store: this,
provider: this.provider,
collection: this.collection,
query: this.query,
caller: getGetter(this, "first"),
});
return first;
}
/** Get the last item in this store. */
get last() {
const last = this.optionalLast;
if (!last)
throw new RequiredError(`Last item does not exist in collection "${this.collection}"`, {
store: this,
provider: this.provider,
collection: this.collection,
query: this.query,
caller: getGetter(this, "first"),
});
return last;
}
constructor(collection, query, provider, memory) {
const time = memory?.getQueryTime(collection, query);
const items = memory?.getQuery(collection, query) || [];
super(typeof time === "number" || items.length ? items : NONE, time); // Use the value if it was definitely cached or is not empty.
if (memory)
this.starter = store => runSequence(store.through(memory.getCachedQuerySequence(collection, query)));
this.provider = provider;
this.collection = collection;
this.query = query;
this.limit = getLimit(query) ?? Number.POSITIVE_INFINITY;
// Start loading the value from the provider if it is not definitely cached.
if (typeof time !== "number")
this.refresh();
}
/** Refresh this store from the source provider. */
refresh(provider = this.provider) {
if (!this.busy.value)
void this._refresh(provider);
}
async _refresh(provider) {
this.busy.value = true;
this.reason = undefined; // Optimistically clear the error.
try {
const items = await provider.getQuery(this.collection, this.query);
this._hasMore = items.length >= this.limit; // If the query returned {limit} or more items, we can assume there are more items waiting to be queried.
this.value = items;
}
catch (thrown) {
this.reason = thrown;
}
finally {
this.busy.value = false;
}
}
/** Refresh this store if data in the cache is older than `maxAge` (in milliseconds). */
refreshStale(maxAge) {
if (this.age > maxAge)
this.refresh();
}
/** Subscribe this store to a provider. */
connect(provider = this.provider) {
return runSequence(this.through(provider.getQuerySequence(this.collection, this.query)));
}
/**
* Load more items after the last once.
* - Promise that needs to be handled.
*/
loadMore() {
if (!this.busy.value)
void this._loadMore();
}
async _loadMore() {
this.busy.value = true;
this.reason = undefined; // Optimistically clear the error.
try {
const last = this.last;
const query = last ? getAfterQuery(this.query, last) : this.query;
const items = await this.provider.getQuery(this.collection, query);
this.value = [...this.value, ...items];
this._hasMore = items.length >= this.limit; // If the query returned {limit} or more items, we can assume there are more items waiting to be queried.
}
catch (thrown) {
this.reason = thrown;
}
finally {
this.busy.value = false;
}
}
// Implement `Iteratable`
[Symbol.iterator]() {
return this.value.values();
}
}