npmextra/ts/npmextra.classes.keyvaluestore.ts

197 lines
6.1 KiB
TypeScript
Raw Normal View History

2023-08-03 17:22:34 +00:00
import * as plugins from './npmextra.plugins.js';
import * as paths from './npmextra.paths.js';
2016-08-28 12:51:04 +00:00
2023-08-03 17:22:34 +00:00
import { Task } from '@push.rocks/taskbuffer';
2017-07-12 13:30:49 +00:00
2023-08-24 08:39:47 +00:00
export type TKeyValueStore = 'custom' | 'userHomeDir';
2016-09-24 14:44:48 +00:00
2024-06-12 18:04:04 +00:00
export interface IKvStoreConstructorOptions<T> {
2024-02-12 18:16:43 +00:00
typeArg: TKeyValueStore;
identityArg: string;
customPath?: string;
2024-06-12 18:04:04 +00:00
mandatoryKeys?: Array<keyof T>;
2024-02-12 18:16:43 +00:00
}
2017-07-09 17:05:03 +00:00
/**
2023-08-24 08:39:47 +00:00
* kvStore is a simple key value store to store data about projects between runs
2017-07-09 17:05:03 +00:00
*/
2024-02-12 18:16:43 +00:00
export class KeyValueStore<T = any> {
private dataObject: Partial<T> = {};
2024-06-12 18:04:04 +00:00
private deletedObject: Partial<T> = {};
private mandatoryKeys: Set<keyof T> = new Set();
public changeSubject = new plugins.smartrx.rxjs.Subject<Partial<T>>();
2024-02-07 17:16:40 +00:00
private storedStateString: string = '';
2019-05-10 15:05:04 +00:00
public syncTask = new Task({
2019-09-16 11:18:45 +00:00
name: 'syncTask',
2017-07-12 15:13:29 +00:00
buffered: true,
2021-01-27 21:00:49 +00:00
bufferMax: 1,
execDelay: 0,
2017-07-12 15:13:29 +00:00
taskFunction: async () => {
2019-05-10 15:03:07 +00:00
this.dataObject = {
...plugins.smartfile.fs.toObjectSync(this.filePath),
2021-01-27 21:00:49 +00:00
...this.dataObject,
2019-05-10 15:03:07 +00:00
};
2024-06-12 18:04:04 +00:00
for (const key of Object.keys(this.deletedObject) as Array<keyof T>) {
delete this.dataObject[key];
2017-07-12 15:22:22 +00:00
}
this.deletedObject = {};
2023-08-24 08:44:42 +00:00
await plugins.smartfile.memory.toFs(
2023-08-24 10:06:46 +00:00
plugins.smartjson.stringifyPretty(this.dataObject),
2023-08-24 08:44:42 +00:00
this.filePath
);
2024-02-07 17:16:40 +00:00
const newStateString = plugins.smartjson.stringify(this.dataObject);
// change detection
if (newStateString !== this.storedStateString) {
this.storedStateString = newStateString;
this.changeSubject.next(this.dataObject);
}
2021-01-27 21:00:49 +00:00
},
});
2023-08-24 08:44:42 +00:00
2019-09-16 11:18:45 +00:00
/**
2023-08-24 08:39:47 +00:00
* computes the identity and filePath
2019-09-16 11:18:45 +00:00
*/
private initFilePath = () => {
2023-08-24 08:44:42 +00:00
if (this.customPath) {
// Use custom path if provided
const absolutePath = plugins.smartpath.transform.makeAbsolute(this.customPath, paths.cwd);
2023-08-24 08:39:47 +00:00
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;
}
2023-08-24 08:44:42 +00:00
2019-09-16 11:18:45 +00:00
let baseDir: string;
2023-08-24 08:39:47 +00:00
if (this.type === 'userHomeDir') {
baseDir = paths.kvUserHomeDirBase;
} else {
throw new Error('kv type not supported');
2019-09-16 11:18:45 +00:00
}
this.filePath = plugins.path.join(baseDir, this.identity + '.json');
2023-08-24 08:39:47 +00:00
plugins.smartfile.fs.ensureDirSync(baseDir);
2019-09-16 11:18:45 +00:00
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
2019-09-16 11:19:42 +00:00
};
2019-09-16 11:18:45 +00:00
2023-08-24 08:39:47 +00:00
// 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
2017-07-12 15:13:29 +00:00
/**
* the constructor of keyvalue store
* @param typeArg
2023-08-24 08:39:47 +00:00
* @param identityArg
* @param customPath Optional custom path for the keyValue store
2017-07-12 15:13:29 +00:00
*/
2024-06-12 18:04:04 +00:00
constructor(optionsArg: IKvStoreConstructorOptions<T>) {
2024-02-12 18:16:43 +00:00
if (optionsArg.customPath && optionsArg.typeArg !== 'custom') {
2023-08-24 08:39:47 +00:00
throw new Error('customPath can only be provided if typeArg is custom');
}
2024-02-12 18:16:43 +00:00
if (optionsArg.typeArg === 'custom' && !optionsArg.customPath) {
2023-08-24 08:39:47 +00:00
throw new Error('customPath must be provided if typeArg is custom');
}
2024-02-12 18:16:43 +00:00
this.type = optionsArg.typeArg;
this.identity = optionsArg.identityArg;
this.customPath = optionsArg.customPath; // Store custom path if provided
this.initFilePath();
2024-02-12 18:16:43 +00:00
if (optionsArg.mandatoryKeys) {
this.setMandatoryKeys(optionsArg.mandatoryKeys);
2024-02-07 17:16:40 +00:00
}
2017-03-18 15:23:47 +00:00
}
2016-09-24 19:49:53 +00:00
2017-03-18 15:23:47 +00:00
/**
2017-07-12 13:30:49 +00:00
* reads all keyValue pairs at once and returns them
2017-03-18 15:23:47 +00:00
*/
2024-06-12 18:04:04 +00:00
public async readAll(): Promise<Partial<T>> {
2019-09-16 11:18:45 +00:00
await this.syncTask.trigger();
return this.dataObject;
2017-07-12 13:30:49 +00:00
}
2016-08-28 12:51:04 +00:00
2017-07-12 13:30:49 +00:00
/**
* reads a keyValueFile from disk
*/
2024-06-12 18:04:04 +00:00
public async readKey<K extends keyof T>(keyArg: K): Promise<T[K]> {
2019-09-16 11:18:45 +00:00
await this.syncTask.trigger();
2024-06-12 18:04:04 +00:00
return this.dataObject[keyArg] as T[K];
2017-03-18 15:23:47 +00:00
}
2016-08-28 12:51:04 +00:00
2017-03-18 15:23:47 +00:00
/**
2017-08-16 16:25:45 +00:00
* writes a specific key to the keyValueStore
2017-03-18 15:23:47 +00:00
*/
2024-06-12 18:04:04 +00:00
public async writeKey<K extends keyof T>(keyArg: K, valueArg: T[K]): Promise<void> {
2019-09-16 11:18:45 +00:00
await this.writeAll({
2021-01-27 21:00:49 +00:00
[keyArg]: valueArg,
2024-06-12 18:04:04 +00:00
} as unknown as Partial<T>);
2019-09-16 11:18:45 +00:00
}
2024-06-12 18:04:04 +00:00
public async deleteKey<K extends keyof T>(keyArg: K): Promise<void> {
2019-09-16 11:18:45 +00:00
this.deletedObject[keyArg] = this.dataObject[keyArg];
await this.syncTask.trigger();
2017-08-16 16:25:45 +00:00
}
/**
* writes all keyValue pairs in the object argument
*/
2024-06-12 18:04:04 +00:00
public async writeAll(keyValueObject: Partial<T>): Promise<void> {
2019-09-16 11:19:42 +00:00
this.dataObject = { ...this.dataObject, ...keyValueObject };
2019-09-16 11:18:45 +00:00
await this.syncTask.trigger();
2017-03-18 15:23:47 +00:00
}
2016-08-28 12:51:04 +00:00
2017-03-18 15:23:47 +00:00
/**
* wipes a key value store from disk
*/
2024-06-12 18:04:04 +00:00
public async wipe(): Promise<void> {
2019-09-16 11:18:45 +00:00
this.dataObject = {};
await plugins.smartfile.fs.remove(this.filePath);
2017-03-18 15:23:47 +00:00
}
2023-08-24 08:44:42 +00:00
/**
* resets the KeyValueStore to the initial state by syncing first, deleting all keys, and then triggering a sync again
*/
2024-06-12 18:04:04 +00:00
public async reset(): Promise<void> {
2023-08-24 08:44:42 +00:00
await this.syncTask.trigger(); // Sync to get the latest state
// Delete all keys from the dataObject and add them to deletedObject
2024-06-12 18:04:04 +00:00
for (const key of Object.keys(this.dataObject) as Array<keyof T>) {
2023-08-24 08:44:42 +00:00
this.deletedObject[key] = this.dataObject[key];
delete this.dataObject[key];
}
await this.syncTask.trigger(); // Sync again to reflect the deletion
}
2024-02-07 17:16:40 +00:00
2024-06-12 18:04:04 +00:00
private setMandatoryKeys(keys: Array<keyof T>) {
2024-02-07 17:16:40 +00:00
keys.forEach(key => this.mandatoryKeys.add(key));
}
2024-06-12 18:04:04 +00:00
public async getMissingMandatoryKeys(): Promise<Array<keyof T>> {
2024-04-14 00:09:41 +00:00
await this.readAll();
2024-02-07 17:16:40 +00:00
return Array.from(this.mandatoryKeys).filter(key => !(key in this.dataObject));
}
2024-06-12 18:04:04 +00:00
public async waitForKeysPresent<K extends keyof T>(keysArg: K[]): Promise<void> {
2024-02-07 17:16:40 +00:00
const missingKeys = keysArg.filter(keyArg => !this.dataObject[keyArg]);
if (missingKeys.length === 0) {
return;
}
2024-06-12 18:04:04 +00:00
return new Promise<void>((resolve, reject) => {
2024-02-07 17:16:40 +00:00
const subscription = this.changeSubject.subscribe(() => {
const missingKeys = keysArg.filter(keyArg => !this.dataObject[keyArg]);
if (missingKeys.length === 0) {
subscription.unsubscribe();
resolve();
}
});
});
}
2024-06-12 18:04:04 +00:00
public async waitForAndGetKey<K extends keyof T>(keyArg: K): Promise<T[K] | undefined> {
await this.waitForKeysPresent([keyArg]);
return this.readKey(keyArg);
}
}