import { MaybeNumber, MaybeString, Playback } from 'ontime-types'; import { JSONFile } from 'lowdb/node'; import { deepEqual } from 'fast-equals'; import { publicFiles } from '../setup/index.js'; export type RestorePoint = { playback: Playback; selectedEventId: MaybeString; startedAt: MaybeNumber; addedTime: number; pausedAt: MaybeNumber; firstStart: MaybeNumber; blockStartAt: MaybeNumber; }; /** * Utility validates a RestorePoint * @param obj * @return boolean */ export function isRestorePoint(obj: unknown): obj is RestorePoint { if (!obj) { return false; } const restorePoint = obj as RestorePoint; if (typeof restorePoint.playback !== 'string' || !Object.values(Playback).includes(restorePoint.playback)) { return false; } if (typeof restorePoint.selectedEventId !== 'string' && restorePoint.selectedEventId !== null) { return false; } if (typeof restorePoint.startedAt !== 'number' && restorePoint.startedAt !== null) { return false; } if (typeof restorePoint.addedTime !== 'number') { return false; } if (typeof restorePoint.pausedAt !== 'number' && restorePoint.pausedAt !== null) { return false; } if (typeof restorePoint.firstStart !== 'number' && restorePoint.firstStart !== null) { return false; } if (typeof restorePoint.blockStartAt !== 'number' && restorePoint.blockStartAt !== null) { return false; } return true; } /** * Utility interface to allow dependency injection during test */ /** * Service manages saving of application state * that can then be restored when reopening */ export class RestoreService { private readonly filePath: MaybeString; private readonly file: JSONFile; private failedCreateAttempts: number; private savedState: RestorePoint | null; constructor(filePath: string) { this.filePath = filePath; this.savedState = null; this.file = new JSONFile(this.filePath); this.failedCreateAttempts = 0; } /** * Utility, reads from file * @private */ private async read() { return this.file.read(); } /** * Utility writes payload to file * @throws * @param stringifiedState */ private async write(data: RestorePoint) { await this.file.write(data); } /** * Saves runtime data to restore file * @param newState RestorePoint */ async save(newState: RestorePoint) { // after three failed attempts, mark the service as unavailable if (this.failedCreateAttempts > 3) { return; } if (deepEqual(newState, this.savedState)) { return; } try { await this.write(newState); this.savedState = { ...newState }; this.failedCreateAttempts = 0; } catch (_error) { this.failedCreateAttempts += 1; } } /** * Attempts reading a restore point from a given file path * Returns null if none found, restore point otherwise */ async load(): Promise { try { const maybeRestorePoint = await this.read(); if (isRestorePoint(maybeRestorePoint)) { return maybeRestorePoint; } } catch (_error) { // no need to notify the user } return null; } /** * Clears the restore file */ async clear() { try { await this.file.write(null); } catch (_error) { // nothing to do } } } export const restoreService = new RestoreService(publicFiles.restoreFile);