-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrecovery.js
More file actions
132 lines (122 loc) · 4.42 KB
/
Copy pathrecovery.js
File metadata and controls
132 lines (122 loc) · 4.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
'use strict';
/**
* Crash-recovery utilities.
*
* Pure helpers that persist / load / validate the last-known timer state
* to disk, so a restart after a crash can offer to resume. No Electron
* imports — we accept the userData path as a parameter so this module
* is trivially testable under plain `node --test`.
*
* Stale threshold: 5 minutes. Anything older is discarded.
*/
const fs = require('fs');
const path = require('path');
const STATE_FILENAME = 'last-state.json';
const MAX_AGE_MS = 5 * 60 * 1000;
function getRecoveryStatePath(userDataPath) {
return path.join(userDataPath, STATE_FILENAME);
}
/**
* Write timer state to disk asynchronously — won't block the event loop
* during the 10s periodic save.
*
* @param {string} userDataPath
* @param {object} timerState — { totalSeconds, remainingSeconds, presetSeconds, isRunning }
* @param {object} [logger] — optional electron-log-like object with .error
* @returns {Promise<void>}
*/
function saveTimerStateToFile(userDataPath, timerState, logger) {
try {
const statePath = getRecoveryStatePath(userDataPath);
const data = JSON.stringify({
totalSeconds: timerState.totalSeconds,
remainingSeconds: timerState.remainingSeconds,
presetSeconds: timerState.presetSeconds,
isRunning: timerState.isRunning,
savedAt: Date.now()
});
return fs.promises.writeFile(statePath, data).catch(err => {
if (logger && logger.error) { logger.error('saveTimerStateToFile:', err); }
});
} catch (err) {
if (logger && logger.error) { logger.error('saveTimerStateToFile:', err); }
return Promise.resolve();
}
}
/**
* Write timer state to disk SYNCHRONOUSLY — for crash handlers
* (uncaughtException / unhandledRejection) where the process may terminate
* before an async write flushes. The periodic 10s save stays async (above).
*
* @param {string} userDataPath
* @param {object} timerState
* @param {object} [logger]
*/
function saveTimerStateToFileSync(userDataPath, timerState, logger) {
try {
const statePath = getRecoveryStatePath(userDataPath);
const data = JSON.stringify({
totalSeconds: timerState.totalSeconds,
remainingSeconds: timerState.remainingSeconds,
presetSeconds: timerState.presetSeconds,
isRunning: timerState.isRunning,
savedAt: Date.now()
});
fs.writeFileSync(statePath, data);
} catch (err) {
if (logger && logger.error) { logger.error('saveTimerStateToFileSync:', err); }
}
}
/**
* Load saved timer state from disk (sync — runs once at startup).
* Returns null if no file, stale, or unreadable. Deletes stale files.
*
* @param {string} userDataPath
* @param {object} [logger]
* @returns {object|null}
*/
function loadSavedTimerState(userDataPath, logger) {
try {
const statePath = getRecoveryStatePath(userDataPath);
if (!fs.existsSync(statePath)) { return null; }
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const ageMs = Date.now() - (parsed.savedAt || 0);
if (ageMs > MAX_AGE_MS) {
try { fs.unlinkSync(statePath); } catch { /* ignore */ }
return null;
}
return parsed;
} catch (err) {
if (logger && logger.warn) { logger.warn('loadSavedTimerState failed:', err); }
return null;
}
}
function clearSavedTimerState(userDataPath) {
try {
const statePath = getRecoveryStatePath(userDataPath);
if (fs.existsSync(statePath)) { fs.unlinkSync(statePath); }
} catch { /* ignore */ }
}
/**
* Pure validator — decides whether `data` describes a usable recovery
* snapshot at time `now`. No I/O, no side effects.
*
* Rules: object w/ finite savedAt in [now - 5m, now], presetSeconds ≥ 0.
*/
function isRecoveryValid(data, now) {
if (!data || typeof data !== 'object') { return false; }
if (typeof data.savedAt !== 'number' || !Number.isFinite(data.savedAt)) { return false; }
const age = (now || Date.now()) - data.savedAt;
if (age < 0 || age > MAX_AGE_MS) { return false; }
if (typeof data.presetSeconds !== 'number' || data.presetSeconds < 0) { return false; }
return true;
}
module.exports = {
getRecoveryStatePath,
saveTimerStateToFile,
saveTimerStateToFileSync,
loadSavedTimerState,
clearSavedTimerState,
isRecoveryValid,
MAX_AGE_MS
};