import * as plugins from './logcontext.plugins.js'; export class AsyncStore { private static idCounter = 0; private id: number; private parentStore?: AsyncStore; private deletedKeys: string[] = []; private dataObject: { [key: string]: any } = {}; constructor(parentStore?: AsyncStore) { this.parentStore = parentStore; this.id = AsyncStore.idCounter++; } /** * Logs debug info if process.env.DEBUG is set. */ private logDebug(functionName: string, before: Record, after: Record) { if (process.env.DEBUG) { console.log(`Store ID: ${this.id}`); console.log(`Function: ${functionName}`); console.log('--- Before ---'); console.log(before); console.log('--- After ---'); console.log(after); console.log('-----------------------'); } } /** * Cleans up the deleted keys if they no longer exist in any parent store. */ private cleanUp() { for (const key of this.deletedKeys) { if (this.parentStore && this.parentStore.get(key)) { // Parent still has it, so keep in deletedKeys } else { const index = this.deletedKeys.indexOf(key); if (index !== -1) { this.deletedKeys.splice(index, 1); } } } } /** * Adds or updates a value under a specific key in this store. */ public add(keyArg: string, objectArg: any) { // capture the before state const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] }; this.cleanUp(); // If this key was previously deleted, remove it from deletedKeys. if (this.deletedKeys.includes(keyArg)) { this.deletedKeys = this.deletedKeys.filter((key) => key !== keyArg); } this.dataObject[keyArg] = objectArg; // capture the after state const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys] }; this.logDebug('add', before, after); } /** * Deletes a key from the current store. * If a parent store has the key, we record it in `deletedKeys` so the child store "shadows" it. */ public delete(paramName: string) { // capture the before state const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] }; this.cleanUp(); if (this.parentStore?.get(paramName)) { // The parent store has this key; let's mark it as deleted in the child this.deletedKeys.push(paramName); } delete this.dataObject[paramName]; // capture the after state const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys] }; this.logDebug('delete', before, after); } /** * Gets the value of a key, checking this store first, then the parent store if necessary. * Will log the store state before/after for debugging. */ public get(paramName: string) { // capture the before state const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] }; this.cleanUp(); // figure out if paramName is deleted or present let result: any; if (this.deletedKeys.includes(paramName)) { result = undefined; } else { result = this.dataObject[paramName] ?? this.parentStore?.get(paramName); } // capture the after state; we can also show the `result` in the log const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys], retrievedKey: paramName, result }; this.logDebug('get', before, after); return result; } /** * Returns all keys and values, merged with the parent store, but * does NOT include keys that are "deleted" in the child. * Child store should override parent if the same key exists in both. */ public getAll() { this.cleanUp(); // first, get parent's data as a shallow copy const parentData = { ...(this.parentStore?.getAll() || {}) }; // remove keys from parent data that this child has deleted for (const key of this.deletedKeys) { delete parentData[key]; } // child's data overrides parent data for any matching keys return { ...parentData, ...this.dataObject }; } }