diff --git a/packages/core/src/storage/json-file.ts b/packages/core/src/storage/json-file.ts index 233cf6f0..419460b1 100644 --- a/packages/core/src/storage/json-file.ts +++ b/packages/core/src/storage/json-file.ts @@ -5,14 +5,59 @@ try { fs = require('fs') } catch (e) {} +let exitHook: any = null +try { + exitHook = require('exit-hook') +} catch (e) {} + export class JsonFileStorage extends JsonMemoryStorage { private readonly _filename: string + private readonly _safe: boolean + private readonly _cleanup: boolean - constructor(filename: string) { + private readonly _unsubscribe: () => void + + constructor( + filename: string, + params?: { + /** + * Whether to save file "safely", meaning that the file will first be saved + * to `${filename}.tmp`, and then renamed to `filename`, + * instead of writing directly to `filename`. + * + * This solves the issue with the storage being saved as + * a blank file because of the app being stopped while + * the storage is being written. + * + * Defaults to `true` + */ + safe?: boolean + + /** + * Whether to save file on process exit. + * Uses [`exit-hook`](https://www.npmjs.com/package/exit-hook) + * + * Defaults to `true` if `exit-hook` is installed, otherwise `false` + */ + cleanup?: boolean + } + ) { super() if (!fs) throw new Error('Node fs module is not available!') this._filename = filename + this._safe = params?.safe ?? true + this._cleanup = params?.cleanup ?? !!exitHook + + if (this._cleanup && !exitHook) { + throw new Error( + 'Cleanup on exit is supported through `exit-hook` library, install it first!' + ) + } + + if (this._cleanup) { + this._unsubscribe = exitHook(this._onProcessExit.bind(this)) + } } async load(): Promise { @@ -32,19 +77,45 @@ export class JsonFileStorage extends JsonMemoryStorage { save(): Promise { return new Promise((resolve, reject) => { - // calling writeFile immediately seems to destroy session when using debugger - setTimeout( - () => - fs.writeFile( - this._filename, - this._saveJson(), - (err?: Error) => { - if (err) reject(err) - else resolve() - } - ), - 0 + fs.writeFile( + this._safe ? this._filename + '.tmp' : this._filename, + this._saveJson(), + (err?: Error) => { + if (this._safe) { + fs.rename( + this._filename + '.tmp', + this._filename, + (err?: Error) => { + if (err) reject(err) + else resolve() + } + ) + } else { + if (err) reject(err) + else resolve() + } + } ) }) } + + private _onProcessExit(): void { + // on exit handler must be synchronous, thus we use sync methods here + + try { + fs.writeFileSync(this._filename, this._saveJson()) + } catch (e) {} + + if (this._safe) { + try { + fs.unlinkSync(this._filename + '.tmp') + } catch (e) {} + } + } + + destroy(): void { + if (this._cleanup) { + this._unsubscribe() + } + } }