193 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			193 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from './npmextra.plugins.js';
 | |
| import * as paths from './npmextra.paths.js';
 | |
| 
 | |
| import { Task } from '@push.rocks/taskbuffer';
 | |
| 
 | |
| export type TKeyValueStore = 'custom' | 'userHomeDir';
 | |
| 
 | |
| export interface IKvStoreConstructorOptions {
 | |
|   typeArg: TKeyValueStore;
 | |
|   identityArg: string;
 | |
|   customPath?: string;
 | |
|   mandatoryKeys?: string[];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * kvStore is a simple key value store to store data about projects between runs
 | |
|  */
 | |
| export class KeyValueStore<T = any> {
 | |
|   private dataObject: Partial<T> = {};
 | |
|   private deletedObject: any = {};
 | |
|   private mandatoryKeys: Set<string> = new Set();
 | |
|   public changeSubject = new plugins.smartrx.rxjs.Subject();
 | |
|   
 | |
|   private storedStateString: string = '';
 | |
|   public syncTask = new Task({
 | |
|     name: 'syncTask',
 | |
|     buffered: true,
 | |
|     bufferMax: 1,
 | |
|     execDelay: 0,
 | |
|     taskFunction: async () => {
 | |
|       
 | |
|       this.dataObject = {
 | |
|         ...plugins.smartfile.fs.toObjectSync(this.filePath),
 | |
|         ...this.dataObject,
 | |
|       };
 | |
|       for (const key of Object.keys(this.deletedObject)) {
 | |
|         delete this.dataObject[key];
 | |
|       }
 | |
|       this.deletedObject = {};
 | |
|       await plugins.smartfile.memory.toFs(
 | |
|         plugins.smartjson.stringifyPretty(this.dataObject),
 | |
|         this.filePath
 | |
|       );
 | |
|       const newStateString = plugins.smartjson.stringify(this.dataObject);
 | |
| 
 | |
|       // change detection
 | |
|       if (newStateString !== this.storedStateString) {
 | |
|         this.storedStateString = newStateString;
 | |
|         this.changeSubject.next(this.dataObject);
 | |
|       }
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * computes the identity and filePath
 | |
|    */
 | |
|   private initFilePath = () => {
 | |
|     if (this.customPath) {
 | |
|       // Use custom path if provided
 | |
|       const absolutePath = plugins.smartpath.transform.makeAbsolute(this.customPath, paths.cwd);
 | |
|       this.filePath = absolutePath;
 | |
|       if (plugins.smartfile.fs.isDirectorySync(this.filePath)) {
 | |
|         this.filePath = plugins.path.join(this.filePath, this.identity + '.json');
 | |
|       }
 | |
|       plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let baseDir: string;
 | |
|     if (this.type === 'userHomeDir') {
 | |
|       baseDir = paths.kvUserHomeDirBase;
 | |
|     } else {
 | |
|       throw new Error('kv type not supported');
 | |
|     }
 | |
|     this.filePath = plugins.path.join(baseDir, this.identity + '.json');
 | |
|     plugins.smartfile.fs.ensureDirSync(baseDir);
 | |
|     plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
 | |
|   };
 | |
| 
 | |
|   // if no custom path is provided, try to store at home directory
 | |
|   public type: TKeyValueStore;
 | |
|   public identity: string;
 | |
|   public filePath: string;
 | |
|   private customPath?: string; // Optionally allow custom path
 | |
| 
 | |
|   /**
 | |
|    * the constructor of keyvalue store
 | |
|    * @param typeArg
 | |
|    * @param identityArg
 | |
|    * @param customPath Optional custom path for the keyValue store
 | |
|    */
 | |
|   constructor(optionsArg: IKvStoreConstructorOptions) {
 | |
|     if (optionsArg.customPath && optionsArg.typeArg !== 'custom') {
 | |
|       throw new Error('customPath can only be provided if typeArg is custom');
 | |
|     }
 | |
|     if (optionsArg.typeArg === 'custom' && !optionsArg.customPath) {
 | |
|       throw new Error('customPath must be provided if typeArg is custom');
 | |
|     }
 | |
|     this.type = optionsArg.typeArg;
 | |
|     this.identity = optionsArg.identityArg;
 | |
|     this.customPath = optionsArg.customPath; // Store custom path if provided
 | |
|     this.initFilePath();
 | |
|     if (optionsArg.mandatoryKeys) {
 | |
|       this.setMandatoryKeys(optionsArg.mandatoryKeys);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * reads all keyValue pairs at once and returns them
 | |
|    */
 | |
|   public async readAll() {
 | |
|     await this.syncTask.trigger();
 | |
|     return this.dataObject;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * reads a keyValueFile from disk
 | |
|    */
 | |
|   public async readKey(keyArg: string) {
 | |
|     await this.syncTask.trigger();
 | |
|     return this.dataObject[keyArg];
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * writes a specific key to the keyValueStore
 | |
|    */
 | |
|   public async writeKey(keyArg: string, valueArg: any) {
 | |
|     await this.writeAll({
 | |
|       [keyArg]: valueArg,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   public async deleteKey(keyArg: string) {
 | |
|     this.deletedObject[keyArg] = this.dataObject[keyArg];
 | |
|     await this.syncTask.trigger();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * writes all keyValue pairs in the object argument
 | |
|    */
 | |
|   public async writeAll(keyValueObject: { [key: string]: any }) {
 | |
|     this.dataObject = { ...this.dataObject, ...keyValueObject };
 | |
|     await this.syncTask.trigger();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * wipes a key value store from disk
 | |
|    */
 | |
|   public async wipe() {
 | |
|     this.dataObject = {};
 | |
|     await plugins.smartfile.fs.remove(this.filePath);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * resets the KeyValueStore to the initial state by syncing first, deleting all keys, and then triggering a sync again
 | |
|    */
 | |
|   public async reset() {
 | |
|     await this.syncTask.trigger(); // Sync to get the latest state
 | |
| 
 | |
|     // Delete all keys from the dataObject and add them to deletedObject
 | |
|     for (const key of Object.keys(this.dataObject)) {
 | |
|       this.deletedObject[key] = this.dataObject[key];
 | |
|       delete this.dataObject[key];
 | |
|     }
 | |
| 
 | |
|     await this.syncTask.trigger(); // Sync again to reflect the deletion
 | |
|   }
 | |
| 
 | |
|   private setMandatoryKeys(keys: string[]) {
 | |
|     keys.forEach(key => this.mandatoryKeys.add(key));
 | |
|   }
 | |
| 
 | |
|   public getMissingMandatoryKeys(): string[] {
 | |
|     return Array.from(this.mandatoryKeys).filter(key => !(key in this.dataObject));
 | |
|   }
 | |
| 
 | |
|   public async waitForKeysPresent(keysArg: string[]): Promise<void> {
 | |
|     const missingKeys = keysArg.filter(keyArg => !this.dataObject[keyArg]);
 | |
|     if (missingKeys.length === 0) {
 | |
|       return;
 | |
|     }
 | |
|     return new Promise((resolve, reject) => {
 | |
|       const subscription = this.changeSubject.subscribe(() => {
 | |
|         const missingKeys = keysArg.filter(keyArg => !this.dataObject[keyArg]);
 | |
|         if (missingKeys.length === 0) {
 | |
|           subscription.unsubscribe();
 | |
|           resolve();
 | |
|         }
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| }
 |