UNPKG

timer-db

Version:

A library for cubing timer results storage with CouchDB-based synchronization.

147 lines (131 loc) 4.64 kB
// https://www.npmjs.com/package/@collectable/red-black-tree import { fromPairsWithStringKeys, RedBlackTreeStructure, iterateFromFirst, iterateValuesFromLast, remove, set, size, valueAt, } from "@collectable/red-black-tree"; import { StoredAttempt } from "../data/Attempt"; import { PouchDBStorage } from "./PouchDBStorage"; import { AttemptUUID, SessionUUID } from "../UUID"; import { Storage } from "./storage"; const MIN_SIZE_CAP = 1000; const MAX_SIZE_CAP = 1050; type RedBlackTree = RedBlackTreeStructure<string, StoredAttempt>; // The cache will contain at least `size` entries. // If it gets > minSize or < maxSize entries, it will be adjusted to midSize. export class AttemptCache { private latestSolves: Promise<RedBlackTree>; constructor( private storage: Storage, private sessionID: SessionUUID, private minSize: number = 100, private midSize: number = minSize + 25, private maxSize: number = minSize + 50 ) { if (minSize > midSize || midSize > maxSize) { throw new Error("AttemptCache initialized with invalid size parameters."); } if (this.minSize > MIN_SIZE_CAP) { throw new Error(`AttemptCache min size is capped at ${MIN_SIZE_CAP}`); } if (this.maxSize > MAX_SIZE_CAP) { throw new Error(`AttemptCache max size is capped at ${MAX_SIZE_CAP}.`); } this.latestSolves = this.getLatestFromDB(this.midSize); } /******** Functions that don't access `this.latestSolves`. ********/ async getLatestFromDB(n: number): Promise<RedBlackTree> { const attempts: StoredAttempt[] = await this.storage.latestAttempts( this.sessionID, n ); const pairs: [AttemptUUID, StoredAttempt][] = attempts.map((attempt) => [ attempt._id, attempt, ]); return fromPairsWithStringKeys<StoredAttempt>(pairs); } private growToMidSize(tree: RedBlackTree): Promise<RedBlackTree> { return this.getLatestFromDB(this.midSize); // TODO: Only get the ones we need. } private shrinkToMidSize(tree: RedBlackTree): Promise<RedBlackTree> { // TODO: Is calling `this.getLatest` about as efficient? const newPairs: [AttemptUUID, StoredAttempt][] = []; let i = 0; for (const attempt of iterateValuesFromLast<string, StoredAttempt>(tree)) { newPairs.push([attempt._id, attempt]); i++; if (i >= this.midSize) { break; } } return Promise.resolve(fromPairsWithStringKeys<StoredAttempt>(newPairs)); } // TODO: This will constantly try to grow if there are fewer than minSize // attempts. We can avoid that by keeping count of the number of attempts, and // avoid size maintenance operations until there are at least minSize attempts // in the session. private async maintainSize(tree: RedBlackTree): Promise<RedBlackTree> { const prevSize = await size<string, StoredAttempt>(tree); if (prevSize < this.minSize) { return this.growToMidSize(tree); } else if (prevSize > this.maxSize) { return this.shrinkToMidSize(tree); } else { return tree; } } /******** Functions that DO access `this.latestSolves`. ********/ async debugCurrentSize(): Promise<number> { return size(await this.latestSolves); } async debugPrint(): Promise<void> { const l = []; for (const entry of iterateFromFirst<string, StoredAttempt>( await this.latestSolves )) { l.push([entry.key, entry.value.resultTotalMs]); } console.table(l); } // TODO: Use a lock to synchronize subbsequent calls in case the caller doesn't `await`? async set(storedAttempt: StoredAttempt): Promise<void> { const prevLatestSolves = await this.latestSolves; this.latestSolves = (async () => { return this.maintainSize( set(storedAttempt._id, storedAttempt, prevLatestSolves) ); })(); } // Does nothing if the attempt doesn't exist. async delete(id: AttemptUUID): Promise<void> { const prevLatestSolves = await this.latestSolves; this.latestSolves = (async () => { return this.maintainSize(remove(id, prevLatestSolves)); })(); } async kthMostRecent(k: number): Promise<StoredAttempt | null> { const latestSolves = await this.latestSolves; return valueAt(-k - 1, latestSolves) ?? null; } async nMostRecent(n: number): Promise<StoredAttempt[]> { const l: StoredAttempt[] = []; let i = 0; const iterator = iterateValuesFromLast<string, StoredAttempt>( await this.latestSolves ); for (const attempt of iterator) { l.push(attempt); i++; if (i >= n) { break; } } return l; } }