shelving
Version:
Toolkit for using data in JavaScript.
249 lines (248 loc) • 8.33 kB
JavaScript
import { DeferredSequence } from "../sequence/DeferredSequence.js";
import { requireArray } from "../util/array.js";
import { isArrayEqual } from "../util/equal.js";
import { getItem } from "../util/item.js";
import { countItems } from "../util/iterate.js";
import { queryItems, queryWritableItems } from "../util/query.js";
import { getRandom, getRandomKey } from "../util/random.js";
import { updateData } from "../util/update.js";
import { Provider } from "./Provider.js";
/**
* Fast in-memory store for data.
* - Extremely fast (ideal for caching!), but does not persist data after the browser window is closed.
* - `get()` etc return the exact same instance of an object that's passed into `set()`
*/
export class MemoryProvider extends Provider {
/** List of tables in `{ collection: Table }` format. */
_tables = {};
/** Get a table for a collection. */
getTable(collection) {
// biome-ignore lint/suspicious/noAssignInExpressions: This is convenient.
return (this._tables[collection] ||= new MemoryTable());
}
getItemTime(collection, id) {
return this.getTable(collection).getItemTime(id);
}
getItem(collection, id) {
return this.getTable(collection).getItem(id);
}
getItemSequence(collection, id) {
return this.getTable(collection).getItemSequence(id);
}
getCachedItemSequence(collection, id) {
return this.getTable(collection).getCachedItemSequence(id);
}
addItem(collection, data) {
return this.getTable(collection).addItem(data);
}
setItem(collection, id, data) {
this.getTable(collection).setItem(id, data);
}
setItemSequence(collection, id, sequence) {
return this.getTable(collection).setItemSequence(id, sequence);
}
updateItem(collection, id, updates) {
this.getTable(collection).updateItem(id, updates);
}
deleteItem(collection, id) {
this.getTable(collection).deleteItem(id);
}
getQueryTime(collection, query) {
return this.getTable(collection).getQueryTime(query);
}
countQuery(collection, query) {
return this.getTable(collection).countQuery(query);
}
getQuery(collection, query) {
return this.getTable(collection).getQuery(query);
}
getQuerySequence(collection, query) {
return this.getTable(collection).getQuerySequence(query);
}
getCachedQuerySequence(collection, query) {
return this.getTable(collection).getCachedQuerySequence(query);
}
setQuery(collection, query, data) {
this.getTable(collection).setQuery(query, data);
}
updateQuery(collection, query, updates) {
this.getTable(collection).updateQuery(query, updates);
}
deleteQuery(collection, query) {
this.getTable(collection).deleteQuery(query);
}
setItems(collection, items, query) {
this.getTable(collection).setItems(items, query);
}
setItemsSequence(collection, sequence, query) {
return this.getTable(collection).setItemsSequence(sequence, query);
}
}
/** An individual table of data. */
export class MemoryTable {
/** Actual data in this table. */
_data = new Map();
/** Times data was last updated. */
_times = new Map();
/** Deferred sequence of next values. */
next = new DeferredSequence();
getItemTime(id) {
return this._times.get(id);
}
getItem(id) {
return this._data.get(id);
}
async *getItemSequence(id) {
let lastValue = this.getItem(id);
yield lastValue;
while (true) {
await this.next;
const nextValue = this.getItem(id);
if (nextValue !== lastValue) {
yield nextValue;
lastValue = nextValue;
}
}
}
async *getCachedItemSequence(id) {
let lastTime = this._times.get(id);
if (typeof lastTime === "number")
yield this.getItem(id);
while (true) {
await this.next;
const nextTime = this._times.get(id);
if (nextTime !== lastTime) {
if (typeof nextTime === "number")
yield this.getItem(id);
lastTime = nextTime;
}
}
}
generateUniqueID() {
const gen = typeof this._data.keys().next().value === "number" ? getRandom : getRandomKey;
let id = gen();
while (this._data.has(id))
id = gen(); // Regenerate ID until unique.
return id;
}
addItem(data) {
const id = this.generateUniqueID();
this.setItem(id, data);
return id;
}
setItem(id, data) {
const item = getItem(id, data);
if (this._data.get(id) !== item) {
this._data.set(id, item);
this._times.set(id, Date.now());
this.next.resolve();
}
}
async *setItemSequence(id, sequence) {
for await (const item of sequence) {
item ? this.setItem(id, item) : this.deleteItem(id);
yield item;
}
}
updateItem(id, updates) {
const oldItem = this._data.get(id);
if (oldItem)
this.setItem(id, updateData(oldItem, updates));
}
deleteItem(id) {
if (this._data.has(id)) {
this._data.delete(id);
this._times.set(id, Date.now());
this.next.resolve();
}
}
getQueryTime(query) {
return this._times.get(_getQueryKey(query));
}
countQuery(query) {
return query ? countItems(queryItems(this._data.values(), query)) : this._data.size;
}
getQuery(query) {
return requireArray(query ? queryItems(this._data.values(), query) : this._data.values());
}
async *getQuerySequence(query) {
let lastItems = this.getQuery(query);
yield lastItems;
while (true) {
await this.next;
const nextItems = this.getQuery(query);
if (!isArrayEqual(lastItems, nextItems)) {
yield nextItems;
lastItems = nextItems;
}
}
}
async *getCachedQuerySequence(query) {
const key = _getQueryKey(query);
let lastTime = this._times.get(key);
if (typeof lastTime === "number")
yield this.getQuery(query);
while (true) {
await this.next;
const nextTime = this._times.get(key);
if (lastTime !== nextTime) {
if (typeof nextTime === "number")
yield this.getQuery(query);
lastTime = nextTime;
}
}
}
setQuery(query, data) {
let changed = 0;
for (const { id } of queryWritableItems(this._data.values(), query)) {
this.setItem(id, data);
changed++;
}
if (changed) {
const key = _getQueryKey(query);
this._times.set(key, Date.now());
this.next.resolve();
}
}
updateQuery(query, updates) {
let count = 0;
for (const { id } of queryWritableItems(this._data.values(), query)) {
this.updateItem(id, updates);
count++;
}
if (count) {
const key = _getQueryKey(query);
this._times.set(key, Date.now());
this.next.resolve();
}
}
deleteQuery(query) {
let count = 0;
for (const { id } of queryWritableItems(this._data.values(), query)) {
this.deleteItem(id);
count++;
}
if (count) {
const key = _getQueryKey(query);
this._times.set(key, Date.now());
this.next.resolve();
}
}
setItems(items, query) {
for (const item of items)
this.setItem(item.id, item);
if (query) {
const key = _getQueryKey(query);
this._times.set(key, Date.now());
this.next.resolve();
}
}
async *setItemsSequence(sequence, query) {
for await (const items of sequence) {
this.setItems(items, query);
yield items;
}
}
}
// Queries that have no limit don't care about sorting either.
const _getQueryKey = (query) => (query ? JSON.stringify(query) : "{}");