shelving
Version:
Toolkit for using data in JavaScript.
214 lines (213 loc) • 7.22 kB
JavaScript
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;
}
}
}