UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

214 lines (213 loc) 7.22 kB
import { StringSchema } from "../../schema/StringSchema.js"; 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 { DBProvider } from "./DBProvider.js"; /** * Fast in-memory store for data. * - Extremely fast (ideal for caching!), but does not persist data after the browser window is closed. * - `getItem()` etc return the exact same instance of an object that's passed into `setItem()` */ export class MemoryDBProvider extends DBProvider { /** List of tables in `{ name: MemoryTable }` format. */ _tables = {}; /** Get a table for a collection. */ getTable(collection) { return (this._tables[collection.name] ||= new MemoryTable(collection)); } async getItem(collection, id) { return this.getTable(collection).getItem(id); } async *getItemSequence(collection, id) { yield* this.getTable(collection).getItemSequence(id); } async addItem(collection, data) { return this.getTable(collection).addItem(data); } async setItem(collection, id, data) { this.getTable(collection).setItem(id, data); } async updateItem(collection, id, updates) { this.getTable(collection).updateItem(id, updates); } async deleteItem(collection, id) { this.getTable(collection).deleteItem(id); } async countQuery(collection, query) { return this.getTable(collection).countQuery(query); } async getQuery(collection, query) { return this.getTable(collection).getQuery(query); } async *getQuerySequence(collection, query) { return yield* this.getTable(collection).getQuerySequence(query); } async setQuery(collection, query, data) { this.getTable(collection).setQuery(query, data); } async updateQuery(collection, query, updates) { this.getTable(collection).updateQuery(query, updates); } async deleteQuery(collection, query) { this.getTable(collection).deleteQuery(query); } setItems(collection, items) { this.getTable(collection).setItems(items); } } /** An individual table of data. */ export class MemoryTable { /** Actual data in this table. */ _data = new Map(); /** Deferred sequence of next values. */ next = new DeferredSequence(); collection; constructor(collection) { this.collection = collection; } getItem(id) { return this._data.get(id); } /** * Subscribe to all changes for this item key. * - Emits the current item immediately, including `undefined` when absent. * - Wakes on every table change, but only yields when this item's value actually changed. */ 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; } } } /** Generate a unique ID for a new item in this table. */ generateUniqueID() { const gen = (this.collection.id instanceof StringSchema ? getRandomKey : getRandom); 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.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) return; const nextItem = updateData(oldItem, updates); if (this._data.get(id) !== nextItem) { this._data.set(id, nextItem); this.next.resolve(); } } deleteItem(id) { if (this._data.has(id)) { this._data.delete(id); this.next.resolve(); } } 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()); } /** * Subscribe to the live result of a query. * - Emits the current query result immediately, even if empty. * - Wakes on every table change, but only yields when the computed query result changed. */ 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; } } } setQuery(query, data) { let changed = false; for (const { id } of queryWritableItems(this._data.values(), query)) { const item = getItem(id, data); if (this._data.get(id) !== item) { this._data.set(id, item); changed = true; } } if (changed) this.next.resolve(); } updateQuery(query, updates) { let changed = false; for (const { id } of queryWritableItems(this._data.values(), query)) { const oldItem = this._data.get(id); if (!oldItem) continue; const nextItem = updateData(oldItem, updates); if (this._data.get(id) !== nextItem) { this._data.set(id, nextItem); changed = true; } } if (changed) this.next.resolve(); } deleteQuery(query) { let changed = false; for (const { id } of queryWritableItems(this._data.values(), query)) { if (this._data.has(id)) { this._data.delete(id); changed = true; } } if (changed) this.next.resolve(); } setItems(items) { let changed = false; for (const item of items) { if (this._data.get(item.id) !== item) { this._data.set(item.id, item); changed = true; } } if (changed) this.next.resolve(); } async *setItemsSequence(sequence) { for await (const items of sequence) { this.setItems(items); yield items; } } }