-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathStorageManager.ts
138 lines (122 loc) · 4.93 KB
/
StorageManager.ts
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
133
134
135
136
137
138
import { dao, store } from '../../inject';
import { print, printError } from '../utils/utils';
/**
* The StorageManager is the high-level code that actually loads the state when the app opens,
* and then continuously keeps track and saves the ongoing state changes.
*
* It uses the {@link storage} object to actually perform the load/save/delete operations,
* which may have been set up in {@link inject} to save to disk or to memory.
*/
export class StorageManager {
/** True when the storage manager is busy saving or loading information. */
private static isBusy: boolean = false;
/** True means the Portfolio changed, and must be saved. */
static ifPortfolioChanged: boolean = false;
/** Null when no interval is set. */
private static intervalId: NodeJS.Timeout | null = null;
/**
* Inits the storage management process. This method is idempotent, ensuring that
* repeated calls do not lead to redundant operations.
*
* What it does:
*
* 1. Load information: On its first invocation, the method loads the portfolio data.
* In the future, when more information needs to be loaded, it can be added here.
*
* 2. Periodic check and save: Sets up a 2-second interval to periodically check if the
* portfolio has been modified. If a change is detected and the manager is not busy,
* it triggers a save operation. In the future, when more information needs to be saved,
* it can be added here.
*
* 3. Concurrency Control: Utilizes the isBusy flag to avoid concurrent load and save
* operations and to prevent overlapping save operations during subsequent interval
* triggers.
*
* Note it's safe to call init multiple times, as subsequent calls will be ignored if an
* initialization process is already underway or completed. The method ensures that
* only one instance of the interval for checking and saving portfolio data is active
* at any time.
*
* Why don't we save changed information as soon as it changes? We want to avoid
* too many saves, which can make the app slow. Instead, we save periodically, every 2
* seconds, if something actually changed. This is a good compromise between saving
* too often and saving too little. Of course, this depends on the requirements of
* your app. Change, if necessary.
*/
static async init() {
print('Initializing the storage manager.');
if ((StorageManager.intervalId !== null) || (StorageManager.isBusy)) {
print('Was already initialized.');
return;
}
StorageManager.isBusy = true;
try {
await StorageManager.loadPortfolio();
StorageManager.ifPortfolioChanged = false;
} catch (error) {
// Should instead handle the error appropriately. Is the local information
// corrupted? We should probably delete the local information and recreate
// it. If the saved info is versioned, and the version is old, this is the
// moment to upgrade it.
print('Failed to load initial information.');
} finally {
StorageManager.isBusy = false;
}
// Set up the saving interval.
StorageManager.intervalId = setInterval(async () => {
await StorageManager.checkAndSavePortfolio();
}, 2000); // 2 seconds interval.
}
private static async checkAndSavePortfolio() {
if (StorageManager.ifPortfolioChanged && !StorageManager.isBusy) {
print('Saved changes to the local drive.');
StorageManager.isBusy = true;
try {
await StorageManager.savePortfolio();
StorageManager.ifPortfolioChanged = false;
} finally {
StorageManager.isBusy = false;
}
}
}
/**
* Stops the periodic save interval (and saves one more time, immediately).
*
* It's important to call this method before shutting down the application to
* ensure that no pending operations are left uncompleted.
*
* This method is idempotent, ensuring that repeated calls do not lead to
* redundant operations.
*/
static async stopTimerAndSaves() {
if (StorageManager.intervalId !== null) {
clearInterval(StorageManager.intervalId);
StorageManager.intervalId = null;
await StorageManager.checkAndSavePortfolio();
print('Save interval cancelled.');
StorageManager.ifPortfolioChanged = false;
}
}
static async savePortfolio(): Promise<void> {
try {
print('Saving Portfolio...');
await dao.savePortfolio(store.portfolio);
} catch (error) {
// Should instead handle the error appropriately.
printError('Failed to save portfolio', error);
}
}
static async loadPortfolio(): Promise<void> {
try {
print('Loading Portfolio...');
let loadedPortfolio = await dao.loadPortfolio();
store.portfolio.copyFrom(loadedPortfolio);
} catch (error) {
// Should instead handle the error appropriately.
printError('Failed to load portfolio', error);
}
}
static markPortfolioChanged() {
StorageManager.ifPortfolioChanged = true;
}
}