nodejs-poolcontroller 7.6.0 → 7.6.1

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 (85) hide show
  1. package/.eslintrc.json +44 -44
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +52 -52
  3. package/CONTRIBUTING.md +74 -74
  4. package/Changelog +215 -215
  5. package/Dockerfile +17 -17
  6. package/Gruntfile.js +40 -40
  7. package/LICENSE +661 -661
  8. package/README.md +191 -186
  9. package/app.ts +0 -0
  10. package/config/Config.ts +24 -2
  11. package/config/VersionCheck.ts +27 -12
  12. package/config copy.json +299 -299
  13. package/controller/Constants.ts +0 -0
  14. package/controller/Equipment.ts +2459 -2405
  15. package/controller/Errors.ts +180 -180
  16. package/controller/Lockouts.ts +436 -422
  17. package/controller/State.ts +51 -26
  18. package/controller/boards/BoardFactory.ts +45 -45
  19. package/controller/boards/EasyTouchBoard.ts +2653 -2537
  20. package/controller/boards/IntelliCenterBoard.ts +4230 -4034
  21. package/controller/boards/IntelliComBoard.ts +63 -63
  22. package/controller/boards/IntelliTouchBoard.ts +241 -241
  23. package/controller/boards/NixieBoard.ts +1675 -1660
  24. package/controller/boards/SystemBoard.ts +4697 -4463
  25. package/controller/comms/Comms.ts +138 -1
  26. package/controller/comms/messages/Messages.ts +3 -5
  27. package/controller/comms/messages/config/ChlorinatorMessage.ts +1 -1
  28. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  29. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  30. package/controller/comms/messages/config/ConfigMessage.ts +0 -0
  31. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  32. package/controller/comms/messages/config/CustomNameMessage.ts +30 -30
  33. package/controller/comms/messages/config/EquipmentMessage.ts +0 -0
  34. package/controller/comms/messages/config/ExternalMessage.ts +9 -7
  35. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  36. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  37. package/controller/comms/messages/config/HeaterMessage.ts +0 -20
  38. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  39. package/controller/comms/messages/config/OptionsMessage.ts +25 -1
  40. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  41. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  42. package/controller/comms/messages/config/ScheduleMessage.ts +347 -342
  43. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  44. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  45. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  46. package/controller/comms/messages/status/EquipmentStateMessage.ts +1 -1
  47. package/controller/comms/messages/status/HeaterStateMessage.ts +86 -86
  48. package/controller/comms/messages/status/IntelliChemStateMessage.ts +445 -397
  49. package/controller/comms/messages/status/IntelliValveStateMessage.ts +35 -35
  50. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  51. package/controller/comms/messages/status/VersionMessage.ts +0 -0
  52. package/controller/nixie/Nixie.ts +162 -162
  53. package/controller/nixie/NixieEquipment.ts +103 -103
  54. package/controller/nixie/bodies/Body.ts +120 -120
  55. package/controller/nixie/bodies/Filter.ts +135 -135
  56. package/controller/nixie/chemistry/ChemController.ts +2498 -2398
  57. package/controller/nixie/chemistry/Chlorinator.ts +314 -314
  58. package/controller/nixie/circuits/Circuit.ts +248 -245
  59. package/controller/nixie/heaters/Heater.ts +648 -600
  60. package/controller/nixie/pumps/Pump.ts +661 -661
  61. package/controller/nixie/schedules/Schedule.ts +257 -257
  62. package/controller/nixie/valves/Valve.ts +170 -170
  63. package/defaultConfig.json +286 -286
  64. package/issue_template.md +51 -51
  65. package/logger/DataLogger.ts +448 -448
  66. package/logger/Logger.ts +0 -0
  67. package/package.json +56 -56
  68. package/tsconfig.json +25 -25
  69. package/web/Server.ts +2 -2
  70. package/web/bindings/influxDB.json +1021 -981
  71. package/web/bindings/mqtt.json +654 -654
  72. package/web/bindings/mqttAlt.json +684 -684
  73. package/web/bindings/rulesManager.json +54 -54
  74. package/web/bindings/smartThings-Hubitat.json +31 -31
  75. package/web/bindings/valveRelays.json +20 -20
  76. package/web/bindings/vera.json +25 -25
  77. package/web/interfaces/baseInterface.ts +136 -136
  78. package/web/interfaces/httpInterface.ts +124 -124
  79. package/web/interfaces/influxInterface.ts +245 -241
  80. package/web/interfaces/mqttInterface.ts +475 -475
  81. package/web/services/config/Config.ts +10 -108
  82. package/web/services/config/ConfigSocket.ts +0 -0
  83. package/web/services/state/State.ts +71 -4
  84. package/web/services/state/StateSocket.ts +0 -0
  85. package/web/services/utilities/Utilities.ts +42 -42
@@ -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(err); }
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(err); }
107
- }
108
- return arr;
109
- }
110
- catch (err) { logger.error(err); }
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(err); }
239
- }
240
- return arr;
241
- }
242
- catch (err) { logger.error(err); }
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(err); }
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(err); }
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(err); }
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(err); }
107
+ }
108
+ return arr;
109
+ }
110
+ catch (err) { logger.error(err); }
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(err); }
239
+ }
240
+ return arr;
241
+ }
242
+ catch (err) { logger.error(err); }
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(err); }
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(err); }
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
+ }