rename package from @push.rocks/npmextra to @push.rocks/smartconfig
- Rename all source files from npmextra.* to simpler names (classes.appdata.ts, etc.) - Rename Npmextra class to Smartconfig - Config file changed from npmextra.json to smartconfig.json - KV store path changed from ~/.npmextra/kv to ~/.smartconfig/kv - Update all imports, tests, and metadata
This commit is contained in:
222
ts/classes.keyvaluestore.ts
Normal file
222
ts/classes.keyvaluestore.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
|
||||
import { Task } from '@push.rocks/taskbuffer';
|
||||
|
||||
export type TKeyValueStore = 'custom' | 'userHomeDir' | 'ephemeral';
|
||||
|
||||
export interface IKvStoreConstructorOptions<T> {
|
||||
typeArg: TKeyValueStore;
|
||||
identityArg: string;
|
||||
customPath?: string;
|
||||
mandatoryKeys?: Array<keyof T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: Partial<T> = {};
|
||||
private mandatoryKeys: Set<keyof T> = new Set();
|
||||
public changeSubject = new plugins.smartrx.rxjs.Subject<Partial<T>>();
|
||||
|
||||
private storedStateString: string = '';
|
||||
public syncTask = new Task({
|
||||
name: 'syncTask',
|
||||
buffered: true,
|
||||
bufferMax: 1,
|
||||
execDelay: 0,
|
||||
taskFunction: async () => {
|
||||
if (this.type !== 'ephemeral') {
|
||||
this.dataObject = {
|
||||
...plugins.smartfile.fs.toObjectSync(this.filePath),
|
||||
...this.dataObject,
|
||||
};
|
||||
for (const key of Object.keys(this.deletedObject) as Array<keyof T>) {
|
||||
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.type === 'ephemeral') {
|
||||
// No file path is needed for ephemeral type
|
||||
return;
|
||||
}
|
||||
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<T>) {
|
||||
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(): Promise<Partial<T>> {
|
||||
await this.syncTask.trigger();
|
||||
return this.dataObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* reads a keyValueFile from disk
|
||||
*/
|
||||
public async readKey<K extends keyof T>(keyArg: K): Promise<T[K]> {
|
||||
await this.syncTask.trigger();
|
||||
return this.dataObject[keyArg] as T[K];
|
||||
}
|
||||
|
||||
/**
|
||||
* writes a specific key to the keyValueStore
|
||||
*/
|
||||
public async writeKey<K extends keyof T>(
|
||||
keyArg: K,
|
||||
valueArg: T[K],
|
||||
): Promise<void> {
|
||||
await this.writeAll({
|
||||
[keyArg]: valueArg,
|
||||
} as unknown as Partial<T>);
|
||||
}
|
||||
|
||||
public async deleteKey<K extends keyof T>(keyArg: K): Promise<void> {
|
||||
this.deletedObject[keyArg] = this.dataObject[keyArg];
|
||||
await this.syncTask.trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* writes all keyValue pairs in the object argument
|
||||
*/
|
||||
public async writeAll(keyValueObject: Partial<T>): Promise<void> {
|
||||
this.dataObject = { ...this.dataObject, ...keyValueObject };
|
||||
await this.syncTask.trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* wipes a key value store from disk
|
||||
*/
|
||||
public async wipe(): Promise<void> {
|
||||
this.dataObject = {};
|
||||
if (this.type !== 'ephemeral') {
|
||||
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(): Promise<void> {
|
||||
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) as Array<keyof T>) {
|
||||
this.deletedObject[key] = this.dataObject[key];
|
||||
delete this.dataObject[key];
|
||||
}
|
||||
|
||||
await this.syncTask.trigger(); // Sync again to reflect the deletion
|
||||
}
|
||||
|
||||
private setMandatoryKeys(keys: Array<keyof T>) {
|
||||
keys.forEach((key) => this.mandatoryKeys.add(key));
|
||||
}
|
||||
|
||||
public async getMissingMandatoryKeys(): Promise<Array<keyof T>> {
|
||||
await this.readAll();
|
||||
return Array.from(this.mandatoryKeys).filter(
|
||||
(key) => !(key in this.dataObject),
|
||||
);
|
||||
}
|
||||
|
||||
public async waitForKeysPresent<K extends keyof T>(
|
||||
keysArg: K[],
|
||||
): Promise<void> {
|
||||
const missingKeys = keysArg.filter((keyArg) => !this.dataObject[keyArg]);
|
||||
if (missingKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const subscription = this.changeSubject.subscribe(() => {
|
||||
const missingKeys = keysArg.filter(
|
||||
(keyArg) => !this.dataObject[keyArg],
|
||||
);
|
||||
if (missingKeys.length === 0) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async waitForAndGetKey<K extends keyof T>(
|
||||
keyArg: K,
|
||||
): Promise<T[K] | undefined> {
|
||||
await this.waitForKeysPresent([keyArg]);
|
||||
return this.readKey(keyArg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user