UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

244 lines (243 loc) 8.14 kB
import { DeferredSequence } from "../sequence/DeferredSequence.js"; import { getArray } 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 { 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; } } } addItem(data) { let id = getRandomKey(); while (this._data.has(id)) id = getRandomKey(); // Regenerate ID until unique. 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 getArray(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) : "{}");