nodejs-poolcontroller 8.1.2 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -0
  7. package/.github/workflows/ghcr-publish.yml +67 -0
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -257
  11. package/Dockerfile +62 -19
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -191
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +57 -7
  23. package/config/VersionCheck.ts +63 -35
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3690
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1929
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1214
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1243
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1194 -996
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -347
  80. package/docker-compose.yml +32 -0
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -60
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -0
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
  106. package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
@@ -1,448 +1,448 @@
1
- import { Timestamp, utils } from '../controller/Constants';
2
- import * as extend from 'extend';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import { setTimeout } from 'timers';
6
- import * as util from 'util';
7
- import { logger } from './Logger';
8
- // One of the primary goals of the DataLogger is to keep the memory usage low when reading and writing large files. This should allow
9
- // the datalogger to manage the file without putting undue pressure on the file system or the heap. While some of these methods
10
- // read in the entire file, others are designed to keep only a single logger entry buffer in memory.
11
- export class DataLogger {
12
- // Creates a new entry from the type constructor. Javascript lacks any real way to factory up an object from a constructor so we
13
- // are using a workaround that points to the constructor for the object. Its lame but effective.
14
- public static createEntry<T>(type: (new () => T), line?: string): T {
15
- let entry = new type();
16
- if (typeof line !== 'undefined') (entry as unknown as DataLoggerEntry).parse(line);
17
- return entry;
18
- }
19
- // This reads the entire file into memory and is very expensive because of the buffer. The readFromStart/End methods should be used in most cases.
20
- public static readAll<T>(logFile: string, type: (new () => T)): T[] {
21
- try {
22
- let logPath = path.join(process.cwd(), '/logs');
23
- if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
24
- logPath += (`/${logFile}`);
25
- let lines = [];
26
- if (fs.existsSync(logPath)) {
27
- let buff = fs.readFileSync(logPath);
28
- lines = buff.toString().split('\n');
29
- }
30
- let arr: T[] = [];
31
- for (let i = 0; i < lines.length; i++) {
32
- try {
33
- let entry = DataLogger.createEntry<T>(type, lines[i]);
34
- arr.push(entry);
35
- } catch (err) { logger.error(`Skipping invalid dose history entry: ${err.message}`); }
36
- }
37
- return arr;
38
- } catch (err) { logger.error(`Skipping dose history ${err.message}`); }
39
- }
40
- // This method uses a callback to end the file read from the end of the file. If the callback returns false then the iteration through the
41
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
42
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
43
- //
44
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
45
- // of the file.
46
- public static async readFromEndAsync<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): Promise<T[]> {
47
- try {
48
- let logPath = DataLogger.makeLogFilePath(logFile);
49
- let newLines = ['\r', '\n'];
50
- let arr: T[] = [];
51
- if (fs.existsSync(logPath)) {
52
- console.log(`Reading logfile ${logPath}`);
53
- // Alright what we have created here is a method to read the data from the end of
54
- // a log file in reverse order (tail) that works for all os implementations. It is
55
- // really dumb that this isn't part of the actual file processing.
56
- try {
57
- let file = await new Promise<number>((resolve, reject) => {
58
- fs.open(logPath, 'r', (err, fileNo) => {
59
- if (err) reject(err);
60
- else resolve(fileNo);
61
- });
62
- });
63
- try {
64
- let stat = await new Promise<fs.Stats>((resolve, reject) => {
65
- fs.stat(logPath, (err, data) => {
66
- if (err) reject(err);
67
- else resolve(data);
68
- });
69
- });
70
- // The file is empty.
71
- if (stat.size !== 0) {
72
- let pos = stat.size - 1;
73
- let chars = [];
74
- while (pos >= 0) {
75
- // Read a character from the file
76
- let char = await new Promise<string>((resolve, reject) => {
77
- fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
78
- if (err) reject(err);
79
- else resolve(buff.toString());
80
- });
81
- });
82
- if (!newLines.includes(char)) chars.unshift(char);
83
- // If we hit the beginning of the file or a newline from a previous
84
- // record then we shoud save off the line and read the next record.
85
- if (newLines.includes(char) || pos === 0) {
86
- if (chars.length > 0) {
87
- try {
88
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
89
- if (typeof fn === 'function') {
90
- let rc = fn(arr.length + 1, entry, arr);
91
- if (rc === true) arr.push(entry);
92
- else if (rc === false) break;
93
- }
94
- else
95
- arr.push(entry);
96
- } catch (err) { logger.error(`Skipping invalid dose history entry: ${err.message}`); }
97
- }
98
- chars = [];
99
- }
100
- pos--;
101
- }
102
- }
103
- }
104
- catch (err) { return Promise.reject(err); }
105
- finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
106
- } catch (err) { logger.error(`readFromEndAsync: ${err.message}`); }
107
- }
108
- return arr;
109
- }
110
- catch (err) { logger.error(`readFromEndAsync: ${logFile} ${err.message}`); }
111
-
112
- }
113
- // This method uses a callback to end the file read from the end of the file. If the callback returns false then the iteration through the
114
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
115
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
116
- //
117
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
118
- // of the file.
119
- public static readFromEnd<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): T[] {
120
- let arr: T[] = [];
121
- try {
122
- let logPath = DataLogger.makeLogFilePath(logFile);
123
- let newLines = ['\r', '\n'];
124
- if (fs.existsSync(logPath)) {
125
- // Alright what we have created here is a method to read the data from the end of
126
- // a log file in reverse order (tail) that works for all os implementations. It is
127
- // really dumb that this isn't part of the actual file processing.
128
- let file;
129
- try {
130
- file = fs.openSync(logPath, 'r');
131
- if (file) {
132
- try {
133
- let stat = fs.statSync(logPath);
134
- // The file is empty.
135
- if (stat.size !== 0) {
136
- let pos = stat.size - 1;
137
- let chars = [];
138
- while (pos >= 0) {
139
- // Read a character from the file
140
- let buff = Buffer.allocUnsafe(1);
141
- let len = fs.readSync(file, buff, 0, 1, pos);
142
- if (len === 0) break;
143
- let char = buff.toString();
144
- if (!newLines.includes(char)) chars.unshift(char);
145
- // If we hit the beginning of the file or a newline from a previous
146
- // record then we shoud save off the line and read the next record.
147
- if (newLines.includes(char) || pos === 0) {
148
- if (chars.length > 0) {
149
- try {
150
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
151
- if (typeof fn === 'function') {
152
- let rc = fn(arr.length + 1, entry, arr);
153
- if (rc === true) arr.push(entry);
154
- else if (rc === false) break;
155
- }
156
- else
157
- arr.push(entry);
158
- } catch (err) { logger.error(`Skipping invalid dose history entry: ${err.message}`); }
159
- }
160
- chars = [];
161
- }
162
- pos--;
163
- }
164
- }
165
- }
166
- catch (err) { logger.error(`Error reading from ${logPath}: ${err.message}`); }
167
- }
168
- }
169
- finally { if (typeof file !== 'undefined') fs.closeSync(file); }
170
- }
171
- }
172
- catch (err) { logger.error(`Error reading file ${logFile}: ${err.message}`); }
173
- return arr;
174
- }
175
- // This method uses a callback to end the file read from the start of the file. If the callback returns false then the iteration through the
176
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
177
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
178
- //
179
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
180
- // of the file.
181
- public static async readFromStartAsync<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): Promise<T[]> {
182
- try {
183
- let logPath = DataLogger.makeLogFilePath(logFile);
184
- let newLines = ['\r', '\n'];
185
- let arr: T[] = [];
186
- if (fs.existsSync(logPath)) {
187
- // Alright what we have created here is a method to read the data from the end of
188
- // a log file in reverse order (tail) that works for all os implementations. It is
189
- // really dumb that this isn't part of the actual file processing.
190
- try {
191
- let file = await new Promise<number>((resolve, reject) => {
192
- fs.open(logPath, 'r', (err, fileNo) => {
193
- if (err) reject(err);
194
- else resolve(fileNo);
195
- });
196
- });
197
- try {
198
- let stat = await new Promise<fs.Stats>((resolve, reject) => {
199
- fs.stat(logPath, (err, data) => {
200
- if (err) reject(err);
201
- else resolve(data);
202
- });
203
- });
204
- // The file is empty.
205
- if (stat.size !== 0) {
206
- let pos = 0;
207
- let chars = [];
208
- while (pos < stat.size) {
209
- // Read a character from the file
210
- let char = await new Promise<string>((resolve, reject) => {
211
- fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
212
- if (err) reject(err);
213
- else resolve(buff.toString());
214
- });
215
- });
216
- if (!newLines.includes(char)) chars.push(char);
217
- // If we hit the beginning of the file or a newline from a previous
218
- // record then we shoud save off the line and read the next record.
219
- if (newLines.includes(char) || pos === 0) {
220
- if (chars.length > 0) {
221
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
222
- if (typeof fn === 'function') {
223
- let rc = fn(arr.length + 1, entry, arr);
224
- if (rc === true) arr.push(entry);
225
- else if (rc === false) break;
226
- }
227
- else
228
- arr.push(entry);
229
- }
230
- chars = [];
231
- }
232
- pos++;
233
- }
234
- }
235
- }
236
- catch (err) { return Promise.reject(err); }
237
- finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
238
- } catch (err) { logger.error(`readFromStart: ${err.message}`); }
239
- }
240
- return arr;
241
- }
242
- catch (err) { logger.error(`readFromStart ${logFile}: ${err.message}`); }
243
-
244
- }
245
-
246
- // This method uses a callback to end the file read from the start of the file. If the callback returns false then the iteration through the
247
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
248
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
249
- //
250
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
251
- // of the file.
252
- public static readFromStart<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): T[] {
253
- let arr: T[] = [];
254
- try {
255
- let logPath = DataLogger.makeLogFilePath(logFile);
256
- let newLines = ['\r', '\n'];
257
- if (fs.existsSync(logPath)) {
258
- // Alright what we have created here is a method to read the data from the end of
259
- // a log file in reverse order (tail) that works for all os implementations. It is
260
- // really dumb that this isn't part of the actual file processing.
261
- let file;
262
- try {
263
- file = fs.openSync(logPath, 'r');
264
- if (file) {
265
- try {
266
- let stat = fs.statSync(logPath);
267
- // The file is empty.
268
- if (stat.size !== 0) {
269
- let pos = 0;
270
- let chars = [];
271
- while (pos <= stat.size) {
272
- // Read a character from the file
273
- let buff = Buffer.allocUnsafe(1);
274
- let len = fs.readSync(file, buff, 0, 1, pos);
275
- if (len === 0) break;
276
- let char = buff.toString();
277
- if (!newLines.includes(char)) chars.push(char);
278
- // If we hit the beginning of the file or a newline from a previous
279
- // record then we shoud save off the line and read the next record.
280
- if (newLines.includes(char) || pos === 0) {
281
- if (chars.length > 0) {
282
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
283
- if (typeof fn === 'function') {
284
- let rc = fn(arr.length + 1, entry, arr);
285
- if (rc === true) arr.push(entry);
286
- else if (rc === false) break;
287
- }
288
- else
289
- arr.push(entry);
290
- }
291
- chars = [];
292
- }
293
- pos++;
294
- }
295
- }
296
- }
297
- catch (err) { logger.error(`Error reading from ${logPath}: ${err.message}`); }
298
- }
299
- }
300
- finally { if (typeof file !== 'undefined') fs.closeSync(file); }
301
- }
302
- }
303
- catch (err) { logger.error(`Error reading file ${logFile}: ${err.message}`); }
304
- return arr;
305
- }
306
- public static async writeStart(logFile: string, data: any) {
307
- try {
308
- let logPath = DataLogger.makeLogFilePath(logFile);
309
- let lines = [];
310
- if (fs.existsSync(logPath)) {
311
- let buff = fs.readFileSync(logPath);
312
- lines = buff.toString().split('\n');
313
- }
314
- if (typeof data === 'object')
315
- lines.unshift(JSON.stringify(data));
316
- else
317
- lines.unshift(data.toString());
318
- fs.writeFileSync(logPath, lines.join('\n'));
319
- } catch (err) { logger.error(`writeStart ${logFile}: ${err.message}`); }
320
- }
321
- public static writeEnd(logFile: string, entry: DataLoggerEntry) {
322
- try {
323
- let logPath = DataLogger.makeLogFilePath(logFile);
324
- fs.appendFileSync(logPath, entry.toLog());
325
- } catch (err) { logger.error(`Error writing ${logFile}: ${err.message}`); }
326
- }
327
- public static async writeEndAsync(logFile: string, data: any) {
328
- try {
329
- let logPath = DataLogger.makeLogFilePath(logFile);
330
- let s = typeof data === 'object' ? JSON.stringify(data) : data.toString();
331
- await new Promise<void>((resolve, reject) => {
332
- fs.appendFile(logPath, s, (err) => {
333
- if (err) reject(err);
334
- else resolve();
335
- });
336
- });
337
- } catch (err) { logger.error(`Error writing to file ${logFile}: ${err.message}`); }
338
- }
339
- // Reads the number of lines in reverse order from the start of the file.
340
- public static async readEnd(logFile: string, maxLines: number): Promise<string[]> {
341
- try {
342
- // Alright what we have created here is a method to read the data from the end of
343
- // a log file in reverse order (tail) that works for all os implementations. It is
344
- // really dumb that this isn't part of the actual file processing.
345
- let logPath = DataLogger.makeLogFilePath(logFile);
346
- let newLines = ['\r', '\n'];
347
- let lines = [];
348
- if (fs.existsSync(logPath)) {
349
- let stat = await new Promise<fs.Stats>((resolve, reject) => {
350
- fs.stat(logPath, (err, data) => {
351
- if (err) reject(err);
352
- else resolve(data);
353
- });
354
- });
355
- // The file is empty.
356
- if (stat.size === 0) return lines;
357
- let pos = stat.size;
358
- let file = await new Promise<number>((resolve, reject) => {
359
- fs.open(logPath, 'r', (err, fileNo) => {
360
- if (err) reject(err);
361
- else resolve(fileNo);
362
- });
363
- });
364
- try {
365
- let line = '';
366
- while (pos >= 0 && lines.length < maxLines) {
367
- // Read a character from the file
368
- let char = await new Promise<string>((resolve, reject) => {
369
- fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
370
- if (err) reject(err);
371
- else resolve(buff.toString());
372
- });
373
- });
374
- pos--;
375
- // If we hit the beginning of the file or a newline from a previous
376
- // record then we shoud save off the line and read the next record.
377
- if (newLines.includes(char) || pos === 0) {
378
- if (line.length > 0) lines.push(line);
379
- line = '';
380
- }
381
- else line += char;
382
- }
383
- } catch (err) { }
384
- finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
385
- }
386
- return lines;
387
- } catch (err) { logger.error(`readEnd ${logFile}: ${err.message}`); }
388
- }
389
- private static makeLogFilePath(logFile: string) { return `${DataLogger.ensureLogPath()}/${logFile}`; }
390
- private static ensureLogPath(): string {
391
- let logPath = path.join(process.cwd(), '/logs');
392
- if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
393
- return logPath;
394
- }
395
- }
396
- export interface IDataLoggerEntry<T> {
397
- createInstance(entry?: string): T,
398
- parse(entry?: string): T
399
- }
400
- export class DataLoggerEntry {
401
- private static dateTestISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
402
- private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
403
- constructor(entry?: string | object) {
404
- // Parse the data from the log entry if it exists.
405
- if (typeof entry === 'object') entry = JSON.stringify(entry);
406
- if (typeof entry === 'string') this.parse(entry);
407
- else {
408
- //console.log(`A DATALOGGER ENTRY DOES NOT HAVE A PROPER TYPE ${typeof entry} *************************************`);
409
- //console.log(entry);
410
- }
411
- }
412
- public static createInstance(entry?: string) { return new DataLoggerEntry(entry); }
413
- public parse(entry: string) {
414
- let obj = typeof entry !== 'undefined' ? JSON.parse(entry, this.dateParser) : {};
415
- if (typeof entry === 'undefined') {
416
- console.log(`A DATALOGGER ENTRY WAS NOT DEFINED *************************`);
417
- }
418
- else if (entry === '') {
419
- console.log(`THE INCOMING DATALOGGER ENTRY WAS EMPTY ***************************`)
420
- }
421
- let o = extend(true, this, obj);
422
- }
423
- protected dateParser(key, value) {
424
- if (typeof value === 'string') {
425
- let d = DataLoggerEntry.dateTestISO.exec(value);
426
- // By parsing the date and then creating a new date from that we will get
427
- // the date in the proper timezone.
428
- if (d) return new Date(Date.parse(value));
429
- d = DataLoggerEntry.dateTextAjax.exec(value);
430
- if (d) {
431
- // Not sure we will be seeing ajax dates but this is
432
- // something that we may see from external sources.
433
- let a = d[1].split(/[-+,.]/);
434
- return new Date(a[0] ? +a[0] : 0 - +a[1]);
435
- }
436
- }
437
- return value;
438
- }
439
- public toJSON() {
440
- return utils.replaceProps(this, (key, value) => {
441
- if (key.startsWith('_')) return undefined;
442
- if (typeof value === 'undefined' || value === null) return undefined;
443
- if (typeof value.getMonth === 'function') return Timestamp.toISOLocal(value);
444
- return value;
445
- });
446
- }
447
- public toLog(): string { return JSON.stringify(this) + '\n'; }
448
- }
1
+ import { Timestamp, utils } from '../controller/Constants';
2
+ import * as extend from 'extend';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { setTimeout } from 'timers';
6
+ import * as util from 'util';
7
+ import { logger } from './Logger';
8
+ // One of the primary goals of the DataLogger is to keep the memory usage low when reading and writing large files. This should allow
9
+ // the datalogger to manage the file without putting undue pressure on the file system or the heap. While some of these methods
10
+ // read in the entire file, others are designed to keep only a single logger entry buffer in memory.
11
+ export class DataLogger {
12
+ // Creates a new entry from the type constructor. Javascript lacks any real way to factory up an object from a constructor so we
13
+ // are using a workaround that points to the constructor for the object. Its lame but effective.
14
+ public static createEntry<T>(type: (new () => T), line?: string): T {
15
+ let entry = new type();
16
+ if (typeof line !== 'undefined') (entry as unknown as DataLoggerEntry).parse(line);
17
+ return entry;
18
+ }
19
+ // This reads the entire file into memory and is very expensive because of the buffer. The readFromStart/End methods should be used in most cases.
20
+ public static readAll<T>(logFile: string, type: (new () => T)): T[] {
21
+ try {
22
+ let logPath = path.join(process.cwd(), '/logs');
23
+ if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
24
+ logPath += (`/${logFile}`);
25
+ let lines = [];
26
+ if (fs.existsSync(logPath)) {
27
+ let buff = fs.readFileSync(logPath);
28
+ lines = buff.toString().split('\n');
29
+ }
30
+ let arr: T[] = [];
31
+ for (let i = 0; i < lines.length; i++) {
32
+ try {
33
+ let entry = DataLogger.createEntry<T>(type, lines[i]);
34
+ arr.push(entry);
35
+ } catch (err) { logger.error(`Skipping invalid dose history entry: ${err.message}`); }
36
+ }
37
+ return arr;
38
+ } catch (err) { logger.error(`Skipping dose history ${err.message}`); }
39
+ }
40
+ // This method uses a callback to end the file read from the end of the file. If the callback returns false then the iteration through the
41
+ // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
42
+ // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
43
+ //
44
+ // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
45
+ // of the file.
46
+ public static async readFromEndAsync<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): Promise<T[]> {
47
+ try {
48
+ let logPath = DataLogger.makeLogFilePath(logFile);
49
+ let newLines = ['\r', '\n'];
50
+ let arr: T[] = [];
51
+ if (fs.existsSync(logPath)) {
52
+ console.log(`Reading logfile ${logPath}`);
53
+ // Alright what we have created here is a method to read the data from the end of
54
+ // a log file in reverse order (tail) that works for all os implementations. It is
55
+ // really dumb that this isn't part of the actual file processing.
56
+ try {
57
+ let file = await new Promise<number>((resolve, reject) => {
58
+ fs.open(logPath, 'r', (err, fileNo) => {
59
+ if (err) reject(err);
60
+ else resolve(fileNo);
61
+ });
62
+ });
63
+ try {
64
+ let stat = await new Promise<fs.Stats>((resolve, reject) => {
65
+ fs.stat(logPath, (err, data) => {
66
+ if (err) reject(err);
67
+ else resolve(data);
68
+ });
69
+ });
70
+ // The file is empty.
71
+ if (stat.size !== 0) {
72
+ let pos = stat.size - 1;
73
+ let chars = [];
74
+ while (pos >= 0) {
75
+ // Read a character from the file
76
+ let char = await new Promise<string>((resolve, reject) => {
77
+ fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
78
+ if (err) reject(err);
79
+ else resolve(buff.toString());
80
+ });
81
+ });
82
+ if (!newLines.includes(char)) chars.unshift(char);
83
+ // If we hit the beginning of the file or a newline from a previous
84
+ // record then we shoud save off the line and read the next record.
85
+ if (newLines.includes(char) || pos === 0) {
86
+ if (chars.length > 0) {
87
+ try {
88
+ let entry = DataLogger.createEntry<T>(type, chars.join(''));
89
+ if (typeof fn === 'function') {
90
+ let rc = fn(arr.length + 1, entry, arr);
91
+ if (rc === true) arr.push(entry);
92
+ else if (rc === false) break;
93
+ }
94
+ else
95
+ arr.push(entry);
96
+ } catch (err) { logger.error(`Skipping invalid dose history entry: ${err.message}`); }
97
+ }
98
+ chars = [];
99
+ }
100
+ pos--;
101
+ }
102
+ }
103
+ }
104
+ catch (err) { return Promise.reject(err); }
105
+ finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
106
+ } catch (err) { logger.error(`readFromEndAsync: ${err.message}`); }
107
+ }
108
+ return arr;
109
+ }
110
+ catch (err) { logger.error(`readFromEndAsync: ${logFile} ${err.message}`); }
111
+
112
+ }
113
+ // This method uses a callback to end the file read from the end of the file. If the callback returns false then the iteration through the
114
+ // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
115
+ // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
116
+ //
117
+ // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
118
+ // of the file.
119
+ public static readFromEnd<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): T[] {
120
+ let arr: T[] = [];
121
+ try {
122
+ let logPath = DataLogger.makeLogFilePath(logFile);
123
+ let newLines = ['\r', '\n'];
124
+ if (fs.existsSync(logPath)) {
125
+ // Alright what we have created here is a method to read the data from the end of
126
+ // a log file in reverse order (tail) that works for all os implementations. It is
127
+ // really dumb that this isn't part of the actual file processing.
128
+ let file;
129
+ try {
130
+ file = fs.openSync(logPath, 'r');
131
+ if (file) {
132
+ try {
133
+ let stat = fs.statSync(logPath);
134
+ // The file is empty.
135
+ if (stat.size !== 0) {
136
+ let pos = stat.size - 1;
137
+ let chars = [];
138
+ while (pos >= 0) {
139
+ // Read a character from the file
140
+ let buff = Buffer.allocUnsafe(1);
141
+ let len = fs.readSync(file, buff, 0, 1, pos);
142
+ if (len === 0) break;
143
+ let char = buff.toString();
144
+ if (!newLines.includes(char)) chars.unshift(char);
145
+ // If we hit the beginning of the file or a newline from a previous
146
+ // record then we shoud save off the line and read the next record.
147
+ if (newLines.includes(char) || pos === 0) {
148
+ if (chars.length > 0) {
149
+ try {
150
+ let entry = DataLogger.createEntry<T>(type, chars.join(''));
151
+ if (typeof fn === 'function') {
152
+ let rc = fn(arr.length + 1, entry, arr);
153
+ if (rc === true) arr.push(entry);
154
+ else if (rc === false) break;
155
+ }
156
+ else
157
+ arr.push(entry);
158
+ } catch (err) { logger.error(`Skipping invalid dose history entry: ${err.message}`); }
159
+ }
160
+ chars = [];
161
+ }
162
+ pos--;
163
+ }
164
+ }
165
+ }
166
+ catch (err) { logger.error(`Error reading from ${logPath}: ${err.message}`); }
167
+ }
168
+ }
169
+ finally { if (typeof file !== 'undefined') fs.closeSync(file); }
170
+ }
171
+ }
172
+ catch (err) { logger.error(`Error reading file ${logFile}: ${err.message}`); }
173
+ return arr;
174
+ }
175
+ // This method uses a callback to end the file read from the start of the file. If the callback returns false then the iteration through the
176
+ // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
177
+ // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
178
+ //
179
+ // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
180
+ // of the file.
181
+ public static async readFromStartAsync<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): Promise<T[]> {
182
+ try {
183
+ let logPath = DataLogger.makeLogFilePath(logFile);
184
+ let newLines = ['\r', '\n'];
185
+ let arr: T[] = [];
186
+ if (fs.existsSync(logPath)) {
187
+ // Alright what we have created here is a method to read the data from the end of
188
+ // a log file in reverse order (tail) that works for all os implementations. It is
189
+ // really dumb that this isn't part of the actual file processing.
190
+ try {
191
+ let file = await new Promise<number>((resolve, reject) => {
192
+ fs.open(logPath, 'r', (err, fileNo) => {
193
+ if (err) reject(err);
194
+ else resolve(fileNo);
195
+ });
196
+ });
197
+ try {
198
+ let stat = await new Promise<fs.Stats>((resolve, reject) => {
199
+ fs.stat(logPath, (err, data) => {
200
+ if (err) reject(err);
201
+ else resolve(data);
202
+ });
203
+ });
204
+ // The file is empty.
205
+ if (stat.size !== 0) {
206
+ let pos = 0;
207
+ let chars = [];
208
+ while (pos < stat.size) {
209
+ // Read a character from the file
210
+ let char = await new Promise<string>((resolve, reject) => {
211
+ fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
212
+ if (err) reject(err);
213
+ else resolve(buff.toString());
214
+ });
215
+ });
216
+ if (!newLines.includes(char)) chars.push(char);
217
+ // If we hit the beginning of the file or a newline from a previous
218
+ // record then we shoud save off the line and read the next record.
219
+ if (newLines.includes(char) || pos === 0) {
220
+ if (chars.length > 0) {
221
+ let entry = DataLogger.createEntry<T>(type, chars.join(''));
222
+ if (typeof fn === 'function') {
223
+ let rc = fn(arr.length + 1, entry, arr);
224
+ if (rc === true) arr.push(entry);
225
+ else if (rc === false) break;
226
+ }
227
+ else
228
+ arr.push(entry);
229
+ }
230
+ chars = [];
231
+ }
232
+ pos++;
233
+ }
234
+ }
235
+ }
236
+ catch (err) { return Promise.reject(err); }
237
+ finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
238
+ } catch (err) { logger.error(`readFromStart: ${err.message}`); }
239
+ }
240
+ return arr;
241
+ }
242
+ catch (err) { logger.error(`readFromStart ${logFile}: ${err.message}`); }
243
+
244
+ }
245
+
246
+ // This method uses a callback to end the file read from the start of the file. If the callback returns false then the iteration through the
247
+ // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
248
+ // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
249
+ //
250
+ // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
251
+ // of the file.
252
+ public static readFromStart<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): T[] {
253
+ let arr: T[] = [];
254
+ try {
255
+ let logPath = DataLogger.makeLogFilePath(logFile);
256
+ let newLines = ['\r', '\n'];
257
+ if (fs.existsSync(logPath)) {
258
+ // Alright what we have created here is a method to read the data from the end of
259
+ // a log file in reverse order (tail) that works for all os implementations. It is
260
+ // really dumb that this isn't part of the actual file processing.
261
+ let file;
262
+ try {
263
+ file = fs.openSync(logPath, 'r');
264
+ if (file) {
265
+ try {
266
+ let stat = fs.statSync(logPath);
267
+ // The file is empty.
268
+ if (stat.size !== 0) {
269
+ let pos = 0;
270
+ let chars = [];
271
+ while (pos <= stat.size) {
272
+ // Read a character from the file
273
+ let buff = Buffer.allocUnsafe(1);
274
+ let len = fs.readSync(file, buff, 0, 1, pos);
275
+ if (len === 0) break;
276
+ let char = buff.toString();
277
+ if (!newLines.includes(char)) chars.push(char);
278
+ // If we hit the beginning of the file or a newline from a previous
279
+ // record then we shoud save off the line and read the next record.
280
+ if (newLines.includes(char) || pos === 0) {
281
+ if (chars.length > 0) {
282
+ let entry = DataLogger.createEntry<T>(type, chars.join(''));
283
+ if (typeof fn === 'function') {
284
+ let rc = fn(arr.length + 1, entry, arr);
285
+ if (rc === true) arr.push(entry);
286
+ else if (rc === false) break;
287
+ }
288
+ else
289
+ arr.push(entry);
290
+ }
291
+ chars = [];
292
+ }
293
+ pos++;
294
+ }
295
+ }
296
+ }
297
+ catch (err) { logger.error(`Error reading from ${logPath}: ${err.message}`); }
298
+ }
299
+ }
300
+ finally { if (typeof file !== 'undefined') fs.closeSync(file); }
301
+ }
302
+ }
303
+ catch (err) { logger.error(`Error reading file ${logFile}: ${err.message}`); }
304
+ return arr;
305
+ }
306
+ public static async writeStart(logFile: string, data: any) {
307
+ try {
308
+ let logPath = DataLogger.makeLogFilePath(logFile);
309
+ let lines = [];
310
+ if (fs.existsSync(logPath)) {
311
+ let buff = fs.readFileSync(logPath);
312
+ lines = buff.toString().split('\n');
313
+ }
314
+ if (typeof data === 'object')
315
+ lines.unshift(JSON.stringify(data));
316
+ else
317
+ lines.unshift(data.toString());
318
+ fs.writeFileSync(logPath, lines.join('\n'));
319
+ } catch (err) { logger.error(`writeStart ${logFile}: ${err.message}`); }
320
+ }
321
+ public static writeEnd(logFile: string, entry: DataLoggerEntry) {
322
+ try {
323
+ let logPath = DataLogger.makeLogFilePath(logFile);
324
+ fs.appendFileSync(logPath, entry.toLog());
325
+ } catch (err) { logger.error(`Error writing ${logFile}: ${err.message}`); }
326
+ }
327
+ public static async writeEndAsync(logFile: string, data: any) {
328
+ try {
329
+ let logPath = DataLogger.makeLogFilePath(logFile);
330
+ let s = typeof data === 'object' ? JSON.stringify(data) : data.toString();
331
+ await new Promise<void>((resolve, reject) => {
332
+ fs.appendFile(logPath, s, (err) => {
333
+ if (err) reject(err);
334
+ else resolve();
335
+ });
336
+ });
337
+ } catch (err) { logger.error(`Error writing to file ${logFile}: ${err.message}`); }
338
+ }
339
+ // Reads the number of lines in reverse order from the start of the file.
340
+ public static async readEnd(logFile: string, maxLines: number): Promise<string[]> {
341
+ try {
342
+ // Alright what we have created here is a method to read the data from the end of
343
+ // a log file in reverse order (tail) that works for all os implementations. It is
344
+ // really dumb that this isn't part of the actual file processing.
345
+ let logPath = DataLogger.makeLogFilePath(logFile);
346
+ let newLines = ['\r', '\n'];
347
+ let lines = [];
348
+ if (fs.existsSync(logPath)) {
349
+ let stat = await new Promise<fs.Stats>((resolve, reject) => {
350
+ fs.stat(logPath, (err, data) => {
351
+ if (err) reject(err);
352
+ else resolve(data);
353
+ });
354
+ });
355
+ // The file is empty.
356
+ if (stat.size === 0) return lines;
357
+ let pos = stat.size;
358
+ let file = await new Promise<number>((resolve, reject) => {
359
+ fs.open(logPath, 'r', (err, fileNo) => {
360
+ if (err) reject(err);
361
+ else resolve(fileNo);
362
+ });
363
+ });
364
+ try {
365
+ let line = '';
366
+ while (pos >= 0 && lines.length < maxLines) {
367
+ // Read a character from the file
368
+ let char = await new Promise<string>((resolve, reject) => {
369
+ fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
370
+ if (err) reject(err);
371
+ else resolve(buff.toString());
372
+ });
373
+ });
374
+ pos--;
375
+ // If we hit the beginning of the file or a newline from a previous
376
+ // record then we shoud save off the line and read the next record.
377
+ if (newLines.includes(char) || pos === 0) {
378
+ if (line.length > 0) lines.push(line);
379
+ line = '';
380
+ }
381
+ else line += char;
382
+ }
383
+ } catch (err) { }
384
+ finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
385
+ }
386
+ return lines;
387
+ } catch (err) { logger.error(`readEnd ${logFile}: ${err.message}`); }
388
+ }
389
+ private static makeLogFilePath(logFile: string) { return `${DataLogger.ensureLogPath()}/${logFile}`; }
390
+ private static ensureLogPath(): string {
391
+ let logPath = path.join(process.cwd(), '/logs');
392
+ if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
393
+ return logPath;
394
+ }
395
+ }
396
+ export interface IDataLoggerEntry<T> {
397
+ createInstance(entry?: string): T,
398
+ parse(entry?: string): T
399
+ }
400
+ export class DataLoggerEntry {
401
+ private static dateTestISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
402
+ private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
403
+ constructor(entry?: string | object) {
404
+ // Parse the data from the log entry if it exists.
405
+ if (typeof entry === 'object') entry = JSON.stringify(entry);
406
+ if (typeof entry === 'string') this.parse(entry);
407
+ else {
408
+ //console.log(`A DATALOGGER ENTRY DOES NOT HAVE A PROPER TYPE ${typeof entry} *************************************`);
409
+ //console.log(entry);
410
+ }
411
+ }
412
+ public static createInstance(entry?: string) { return new DataLoggerEntry(entry); }
413
+ public parse(entry: string) {
414
+ let obj = typeof entry !== 'undefined' ? JSON.parse(entry, this.dateParser) : {};
415
+ if (typeof entry === 'undefined') {
416
+ console.log(`A DATALOGGER ENTRY WAS NOT DEFINED *************************`);
417
+ }
418
+ else if (entry === '') {
419
+ console.log(`THE INCOMING DATALOGGER ENTRY WAS EMPTY ***************************`)
420
+ }
421
+ let o = extend(true, this, obj);
422
+ }
423
+ protected dateParser(key, value) {
424
+ if (typeof value === 'string') {
425
+ let d = DataLoggerEntry.dateTestISO.exec(value);
426
+ // By parsing the date and then creating a new date from that we will get
427
+ // the date in the proper timezone.
428
+ if (d) return new Date(Date.parse(value));
429
+ d = DataLoggerEntry.dateTextAjax.exec(value);
430
+ if (d) {
431
+ // Not sure we will be seeing ajax dates but this is
432
+ // something that we may see from external sources.
433
+ let a = d[1].split(/[-+,.]/);
434
+ return new Date(a[0] ? +a[0] : 0 - +a[1]);
435
+ }
436
+ }
437
+ return value;
438
+ }
439
+ public toJSON() {
440
+ return utils.replaceProps(this, (key, value) => {
441
+ if (key.startsWith('_')) return undefined;
442
+ if (typeof value === 'undefined' || value === null) return undefined;
443
+ if (typeof value.getMonth === 'function') return Timestamp.toISOLocal(value);
444
+ return value;
445
+ });
446
+ }
447
+ public toLog(): string { return JSON.stringify(this) + '\n'; }
448
+ }