Skip to content

Commit

Permalink
feat(storage): force save abd exist hook to prevent data lost
Browse files Browse the repository at this point in the history
  • Loading branch information
AliMD committed Sep 10, 2022
1 parent e794d0d commit e327d65
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 21 deletions.
1 change: 1 addition & 0 deletions packages/core/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"url": "https://github.com/AliMD/alwatr/issues"
},
"dependencies": {
"exit-hook": "^3.1.0",
"tslib": "^2.3.1"
}
}
50 changes: 37 additions & 13 deletions packages/core/storage/src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {resolve} from 'node:path';

import {alwatrRegisteredList, createLogger} from '@alwatr/logger';
import {exitHook} from 'exit-hook';

import {readJsonFile, writeJsonFile} from './util.js';

import type {DocumentObject, DocumentListStorage, AlwatrStorageConfig} from './type.js';
import type {AlwatrLogger} from '@alwatr/logger';


export {DocumentObject, DocumentListStorage, AlwatrStorageConfig as Config};

Expand Down Expand Up @@ -34,12 +35,22 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
/**
* Storage name like database table name.
*/
readonly name: string;
readonly name;

/**
* Storage file full path.
*/
readonly storagePath: string;
readonly storagePath;

/**
* Save debounce timeout for minimal disk iops usage.
*/
readonly saveDebounce;

/**
* Write pretty formatted JSON file.
*/
readonly saveBeautiful;

/**
* Ready promise resolved when the storage is ready.
Expand All @@ -53,7 +64,7 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
* const user = userStorage.get('user-1');
* ```
*/
readyPromise: Promise<void>;
readyPromise;

/**
* Ready state set to true when the storage is ready and readyPromise resolved.
Expand All @@ -63,7 +74,7 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
}

protected _readyState = false;
protected _logger: AlwatrLogger;
protected _logger;
protected _storage: DocumentListStorage<DocumentType> = {};
protected _keys: Array<string> | null = null;

Expand All @@ -89,8 +100,11 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
constructor(config: AlwatrStorageConfig) {
this._logger = createLogger(`alwatr-storage:${config.name}`);
this._logger.logMethodArgs('constructor', config);
this.forceSave = this.forceSave.bind(this);
this.name = config.name;
this.storagePath = resolve(`${config.path ?? './db'}/${config.name}.json`);
this.saveDebounce = config.saveDebounce ?? 100;
this.saveBeautiful = config.saveBeautiful || false;
this.readyPromise = this._load();
}

Expand All @@ -102,6 +116,7 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
this._logger.logMethod('_load');
this._storage = (await readJsonFile<DocumentListStorage<DocumentType>>(this.storagePath)) ?? {};
this._readyState = true;
exitHook(this.forceSave);
this._logger.logProperty('readyState', this.readyState);
}

Expand Down Expand Up @@ -228,25 +243,34 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
* Save the storage to disk.
*/
save(): void {
this._logger.logMethod('save.request');
this._logger.logMethod('save');
if (this._readyState !== true) throw new Error('storage_not_ready');

if (this._saveTimer != null) return; // save already requested
this._saveTimer = setTimeout(this.forceSave, this.saveDebounce);
}

this._saveTimer = setTimeout(() => {
this._logger.logMethod('save.action');
/**
* Save the storage to disk without any debounce.
*/
forceSave(): void {
this._logger.logMethod('forceSave');
if (this._saveTimer != null) {
clearTimeout(this._saveTimer);
this._saveTimer = null;
// TODO: catch errors
writeJsonFile(this.storagePath, this._storage);
}, 100);
}

writeJsonFile(this.storagePath, this._storage, this.saveBeautiful ? 2 : 0);
}

/**
* Unload storage data and free ram usage.
*/
unload(): void {
this._logger.logMethod('unload');
this._readyState = false;
if (this._readyState) {
this._readyState = false;
this.forceSave();
}
this._storage = {};
this.readyPromise = Promise.reject(new Error('storage_unloaded'));
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/storage/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export type AlwatrStorageConfig = {
* @default './db'
*/
path?: string;

/**
* Save debounce timeout for minimal disk iops usage.
*/
saveDebounce?: number;

/**
* Write pretty formatted JSON file.
*/
saveBeautiful?: boolean;
};

export type AlwatrStorageProviderConfig = {
Expand Down
29 changes: 21 additions & 8 deletions packages/core/storage/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {existsSync, promises as fs} from 'fs';
import {existsSync, mkdirSync, readFileSync, writeFileSync, renameSync} from 'fs';
import {resolve, dirname} from 'node:path';

import type {JSON} from './type.js';
Expand All @@ -8,7 +8,7 @@ import type {JSON} from './type.js';
/**
* Enhanced read json file.
* @example
* const fileContent = await readJsonFile('./file.json');
* const fileContent = readJsonFile('./file.json');
*/
export async function readJsonFile<T extends JSON>(path: string): Promise<T | null> {
// Check the path is exist
Expand All @@ -19,7 +19,7 @@ export async function readJsonFile<T extends JSON>(path: string): Promise<T | nu

let fileContent;
try {
fileContent = await fs.readFile(path, {encoding: 'utf-8'});
fileContent = readFileSync(path, {encoding: 'utf-8'});
} catch (err) {
throw new Error('read_file_failed');
}
Expand All @@ -34,28 +34,41 @@ export async function readJsonFile<T extends JSON>(path: string): Promise<T | nu
/**
* Enhanced write json file.
* @example
* await writeJsonFile('./file.json', { a:1, b:2, c:3 });
* writeJsonFile('./file.json', { a:1, b:2, c:3 });
*/
export async function writeJsonFile<T extends JSON>(path: string, dataObject: T): Promise<void> {
export async function writeJsonFile<T extends JSON>(
path: string,
dataObject: T,
space?: string | number | undefined,
): Promise<void> {
// Check the path is exist
try {
path = resolve(path);
if (!existsSync(path)) {
await fs.mkdir(dirname(path), {recursive: true});
mkdirSync(dirname(path), {recursive: true});
}
} catch (err) {
throw new Error('make_dir_failed');
}

try {
if (existsSync(path)) {
renameSync(path, path + '.bk');
}
} catch (err) {
// @TODO: handle in forceSave and log with logger
console.error('cannot rename file!');
}

let jsonContent;
try {
jsonContent = JSON.stringify(dataObject, null, 2);
jsonContent = JSON.stringify(dataObject, null, space);
} catch (err) {
throw new Error('stringify_failed');
}

try {
await fs.writeFile(path, jsonContent, {encoding: 'utf-8', flag: 'w'});
writeFileSync(path, jsonContent, {encoding: 'utf-8', flag: 'w'});
} catch (err) {
throw new Error('write_file_failed');
}
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2893,6 +2893,11 @@ execa@^5.0.0:
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"

exit-hook@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-3.1.0.tgz#0ba691facf29637930ead13e727cdc61860331b9"
integrity sha512-KiF9SiLZsKhSutx4V9sG2InYb0v1+2sfKlGD18et8/aGg2m4ij6MJbUHy/cnqJf4ncE7rWjqchE2SNIi4Lgg4A==

expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
Expand Down

0 comments on commit e327d65

Please sign in to comment.