nodejs-poolcontroller 7.3.1 → 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 -195
  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 +2 -0
  10. package/config/Config.ts +27 -2
  11. package/config/VersionCheck.ts +33 -14
  12. package/config copy.json +299 -299
  13. package/controller/Constants.ts +88 -0
  14. package/controller/Equipment.ts +2459 -2225
  15. package/controller/Errors.ts +180 -157
  16. package/controller/Lockouts.ts +437 -0
  17. package/controller/State.ts +364 -79
  18. package/controller/boards/BoardFactory.ts +45 -45
  19. package/controller/boards/EasyTouchBoard.ts +2653 -2489
  20. package/controller/boards/IntelliCenterBoard.ts +4230 -3973
  21. package/controller/boards/IntelliComBoard.ts +63 -63
  22. package/controller/boards/IntelliTouchBoard.ts +241 -167
  23. package/controller/boards/NixieBoard.ts +1675 -1105
  24. package/controller/boards/SystemBoard.ts +4697 -3201
  25. package/controller/comms/Comms.ts +222 -10
  26. package/controller/comms/messages/Messages.ts +13 -9
  27. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  28. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -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 +1 -0
  32. package/controller/comms/messages/config/CustomNameMessage.ts +30 -30
  33. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  34. package/controller/comms/messages/config/ExternalMessage.ts +53 -33
  35. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  36. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  37. package/controller/comms/messages/config/HeaterMessage.ts +14 -28
  38. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  39. package/controller/comms/messages/config/OptionsMessage.ts +38 -2
  40. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  41. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  42. package/controller/comms/messages/config/ScheduleMessage.ts +347 -331
  43. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  44. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  45. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  46. package/controller/comms/messages/status/EquipmentStateMessage.ts +79 -25
  47. package/controller/comms/messages/status/HeaterStateMessage.ts +86 -53
  48. package/controller/comms/messages/status/IntelliChemStateMessage.ts +445 -386
  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 -160
  53. package/controller/nixie/NixieEquipment.ts +103 -103
  54. package/controller/nixie/bodies/Body.ts +120 -117
  55. package/controller/nixie/bodies/Filter.ts +135 -135
  56. package/controller/nixie/chemistry/ChemController.ts +2498 -2395
  57. package/controller/nixie/chemistry/Chlorinator.ts +314 -313
  58. package/controller/nixie/circuits/Circuit.ts +248 -210
  59. package/controller/nixie/heaters/Heater.ts +649 -441
  60. package/controller/nixie/pumps/Pump.ts +661 -599
  61. package/controller/nixie/schedules/Schedule.ts +257 -256
  62. package/controller/nixie/valves/Valve.ts +170 -170
  63. package/defaultConfig.json +286 -271
  64. package/issue_template.md +51 -51
  65. package/logger/DataLogger.ts +448 -433
  66. package/logger/Logger.ts +0 -0
  67. package/package.json +56 -54
  68. package/tsconfig.json +25 -25
  69. package/web/Server.ts +522 -31
  70. package/web/bindings/influxDB.json +1022 -894
  71. package/web/bindings/mqtt.json +654 -543
  72. package/web/bindings/mqttAlt.json +684 -574
  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 -122
  79. package/web/interfaces/influxInterface.ts +245 -240
  80. package/web/interfaces/mqttInterface.ts +475 -464
  81. package/web/services/config/Config.ts +181 -152
  82. package/web/services/config/ConfigSocket.ts +0 -0
  83. package/web/services/state/State.ts +118 -7
  84. package/web/services/state/StateSocket.ts +18 -1
  85. package/web/services/utilities/Utilities.ts +42 -42
@@ -1,433 +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
- let entry = DataLogger.createEntry<T>(type, lines[i]);
33
- arr.push(entry);
34
- }
35
- return arr;
36
- } catch (err) { logger.error(err); }
37
- }
38
- // 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
39
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
40
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
41
- //
42
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
43
- // of the file.
44
- public static async readFromEndAsync<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): Promise<T[]> {
45
- try {
46
- let logPath = DataLogger.makeLogFilePath(logFile);
47
- let newLines = ['\r', '\n'];
48
- let arr: T[] = [];
49
- if (fs.existsSync(logPath)) {
50
- // Alright what we have created here is a method to read the data from the end of
51
- // a log file in reverse order (tail) that works for all os implementations. It is
52
- // really dumb that this isn't part of the actual file processing.
53
- try {
54
- let file = await new Promise<number>((resolve, reject) => {
55
- fs.open(logPath, 'r', (err, fileNo) => {
56
- if (err) reject(err);
57
- else resolve(fileNo);
58
- });
59
- });
60
- try {
61
- let stat = await new Promise<fs.Stats>((resolve, reject) => {
62
- fs.stat(logPath, (err, data) => {
63
- if (err) reject(err);
64
- else resolve(data);
65
- });
66
- });
67
- // The file is empty.
68
- if (stat.size !== 0) {
69
- let pos = stat.size - 1;
70
- let chars = [];
71
- while (pos >= 0) {
72
- // Read a character from the file
73
- let char = await new Promise<string>((resolve, reject) => {
74
- fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
75
- if (err) reject(err);
76
- else resolve(buff.toString());
77
- });
78
- });
79
- if (!newLines.includes(char)) chars.unshift(char);
80
- // If we hit the beginning of the file or a newline from a previous
81
- // record then we shoud save off the line and read the next record.
82
- if (newLines.includes(char) || pos === 0) {
83
- if (chars.length > 0) {
84
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
85
- if (typeof fn === 'function') {
86
- let rc = fn(arr.length + 1, entry, arr);
87
- console.log(rc);
88
- if (rc === true) arr.push(entry);
89
- else if (rc === false) break;
90
- }
91
- else
92
- arr.push(entry);
93
- }
94
- chars = [];
95
- }
96
- pos--;
97
- }
98
- }
99
- }
100
- catch (err) { return Promise.reject(err); }
101
- finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
102
- } catch (err) { logger.error(err); }
103
- }
104
- return arr;
105
- }
106
- catch (err) { logger.error(err); }
107
-
108
- }
109
- // 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
110
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
111
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
112
- //
113
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
114
- // of the file.
115
- public static readFromEnd<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): T[] {
116
- let arr: T[] = [];
117
- try {
118
- let logPath = DataLogger.makeLogFilePath(logFile);
119
- let newLines = ['\r', '\n'];
120
- if (fs.existsSync(logPath)) {
121
- // Alright what we have created here is a method to read the data from the end of
122
- // a log file in reverse order (tail) that works for all os implementations. It is
123
- // really dumb that this isn't part of the actual file processing.
124
- let file;
125
- try {
126
- file = fs.openSync(logPath, 'r');
127
- if (file) {
128
- try {
129
- let stat = fs.statSync(logPath);
130
- // The file is empty.
131
- if (stat.size !== 0) {
132
- let pos = stat.size - 1;
133
- let chars = [];
134
- while (pos >= 0) {
135
- // Read a character from the file
136
- let buff = Buffer.allocUnsafe(1);
137
- let len = fs.readSync(file, buff, 0, 1, pos);
138
- if (len === 0) break;
139
- let char = buff.toString();
140
- if (!newLines.includes(char)) chars.unshift(char);
141
- // If we hit the beginning of the file or a newline from a previous
142
- // record then we shoud save off the line and read the next record.
143
- if (newLines.includes(char) || pos === 0) {
144
- if (chars.length > 0) {
145
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
146
- if (typeof fn === 'function') {
147
- let rc = fn(arr.length + 1, entry, arr);
148
- if (rc === true) arr.push(entry);
149
- else if (rc === false) break;
150
- }
151
- else
152
- arr.push(entry);
153
- }
154
- chars = [];
155
- }
156
- pos--;
157
- }
158
- }
159
- }
160
- catch (err) { logger.error(`Error reading from ${logPath}: ${err.message}`); }
161
- }
162
- }
163
- finally { if (typeof file !== 'undefined') fs.closeSync(file); }
164
- }
165
- }
166
- catch (err) { logger.error(`Error reading file ${logFile}: ${err.message}`); }
167
- return arr;
168
- }
169
- // 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
170
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
171
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
172
- //
173
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
174
- // of the file.
175
- public static async readFromStartAsync<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): Promise<T[]> {
176
- try {
177
- let logPath = DataLogger.makeLogFilePath(logFile);
178
- let newLines = ['\r', '\n'];
179
- let arr: T[] = [];
180
- if (fs.existsSync(logPath)) {
181
- // Alright what we have created here is a method to read the data from the end of
182
- // a log file in reverse order (tail) that works for all os implementations. It is
183
- // really dumb that this isn't part of the actual file processing.
184
- try {
185
- let file = await new Promise<number>((resolve, reject) => {
186
- fs.open(logPath, 'r', (err, fileNo) => {
187
- if (err) reject(err);
188
- else resolve(fileNo);
189
- });
190
- });
191
- try {
192
- let stat = await new Promise<fs.Stats>((resolve, reject) => {
193
- fs.stat(logPath, (err, data) => {
194
- if (err) reject(err);
195
- else resolve(data);
196
- });
197
- });
198
- // The file is empty.
199
- if (stat.size !== 0) {
200
- let pos = 0;
201
- let chars = [];
202
- while (pos < stat.size) {
203
- // Read a character from the file
204
- let char = await new Promise<string>((resolve, reject) => {
205
- fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
206
- if (err) reject(err);
207
- else resolve(buff.toString());
208
- });
209
- });
210
- if (!newLines.includes(char)) chars.push(char);
211
- // If we hit the beginning of the file or a newline from a previous
212
- // record then we shoud save off the line and read the next record.
213
- if (newLines.includes(char) || pos === 0) {
214
- if (chars.length > 0) {
215
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
216
- if (typeof fn === 'function') {
217
- let rc = fn(arr.length + 1, entry, arr);
218
- if (rc === true) arr.push(entry);
219
- else if (rc === false) break;
220
- }
221
- else
222
- arr.push(entry);
223
- }
224
- chars = [];
225
- }
226
- pos++;
227
- }
228
- }
229
- }
230
- catch (err) { return Promise.reject(err); }
231
- finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
232
- } catch (err) { logger.error(err); }
233
- }
234
- return arr;
235
- }
236
- catch (err) { logger.error(err); }
237
-
238
- }
239
-
240
- // 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
241
- // file will end and the log entries will be returned. If the callback returns true then the entry will be added to the
242
- // array and the iteration will continue. If the callback returns undefined then the entry is ignored and the iteration continues.
243
- //
244
- // This allows for efficient filtering of dataLogger files from the end of the file that reduces writes by appending data and allowing efficient pruning
245
- // of the file.
246
- public static readFromStart<T>(logFile: string, type: (new () => T), fn?: (lineNumber: number, entry?: T, arr?: T[]) => boolean): T[] {
247
- let arr: T[] = [];
248
- try {
249
- let logPath = DataLogger.makeLogFilePath(logFile);
250
- let newLines = ['\r', '\n'];
251
- if (fs.existsSync(logPath)) {
252
- // Alright what we have created here is a method to read the data from the end of
253
- // a log file in reverse order (tail) that works for all os implementations. It is
254
- // really dumb that this isn't part of the actual file processing.
255
- let file;
256
- try {
257
- file = fs.openSync(logPath, 'r');
258
- if (file) {
259
- try {
260
- let stat = fs.statSync(logPath);
261
- // The file is empty.
262
- if (stat.size !== 0) {
263
- let pos = 0;
264
- let chars = [];
265
- while (pos <= stat.size) {
266
- // Read a character from the file
267
- let buff = Buffer.allocUnsafe(1);
268
- let len = fs.readSync(file, buff, 0, 1, pos);
269
- if (len === 0) break;
270
- let char = buff.toString();
271
- if (!newLines.includes(char)) chars.push(char);
272
- // If we hit the beginning of the file or a newline from a previous
273
- // record then we shoud save off the line and read the next record.
274
- if (newLines.includes(char) || pos === 0) {
275
- if (chars.length > 0) {
276
- let entry = DataLogger.createEntry<T>(type, chars.join(''));
277
- if (typeof fn === 'function') {
278
- let rc = fn(arr.length + 1, entry, arr);
279
- console.log(rc);
280
- if (rc === true) arr.push(entry);
281
- else if (rc === false) break;
282
- }
283
- else
284
- arr.push(entry);
285
- }
286
- chars = [];
287
- }
288
- pos++;
289
- }
290
- }
291
- }
292
- catch (err) { logger.error(`Error reading from ${logPath}: ${err.message}`); }
293
- }
294
- }
295
- finally { if (typeof file !== 'undefined') fs.closeSync(file); }
296
- }
297
- }
298
- catch (err) { logger.error(`Error reading file ${logFile}: ${err.message}`); }
299
- return arr;
300
- }
301
- public static async writeStart(logFile: string, data: any) {
302
- try {
303
- let logPath = DataLogger.makeLogFilePath(logFile);
304
- let lines = [];
305
- if (fs.existsSync(logPath)) {
306
- let buff = fs.readFileSync(logPath);
307
- lines = buff.toString().split('\n');
308
- }
309
- if (typeof data === 'object')
310
- lines.unshift(JSON.stringify(data));
311
- else
312
- lines.unshift(data.toString());
313
- fs.writeFileSync(logPath, lines.join('\n'));
314
- } catch (err) { logger.error(err); }
315
- }
316
- public static writeEnd(logFile: string, entry: DataLoggerEntry) {
317
- try {
318
- let logPath = DataLogger.makeLogFilePath(logFile);
319
- fs.appendFileSync(logPath, entry.toLog());
320
- } catch (err) { logger.error(`Error writing ${logFile}: ${err.message}`); }
321
- }
322
- public static async writeEndAsync(logFile: string, data: any) {
323
- try {
324
- let logPath = DataLogger.makeLogFilePath(logFile);
325
- let s = typeof data === 'object' ? JSON.stringify(data) : data.toString();
326
- await new Promise<void>((resolve, reject) => {
327
- fs.appendFile(logPath, s, (err) => {
328
- if (err) reject(err);
329
- else resolve();
330
- });
331
- });
332
- } catch (err) { logger.error(`Error writing to file ${logFile}: ${err.message}`); }
333
- }
334
- // Reads the number of lines in reverse order from the start of the file.
335
- public static async readEnd(logFile: string, maxLines: number): Promise<string[]> {
336
- try {
337
- // Alright what we have created here is a method to read the data from the end of
338
- // a log file in reverse order (tail) that works for all os implementations. It is
339
- // really dumb that this isn't part of the actual file processing.
340
- let logPath = DataLogger.makeLogFilePath(logFile);
341
- let newLines = ['\r', '\n'];
342
- let lines = [];
343
- if (fs.existsSync(logPath)) {
344
- let stat = await new Promise<fs.Stats>((resolve, reject) => {
345
- fs.stat(logPath, (err, data) => {
346
- if (err) reject(err);
347
- else resolve(data);
348
- });
349
- });
350
- // The file is empty.
351
- if (stat.size === 0) return lines;
352
- let pos = stat.size;
353
- let file = await new Promise<number>((resolve, reject) => {
354
- fs.open(logPath, 'r', (err, fileNo) => {
355
- if (err) reject(err);
356
- else resolve(fileNo);
357
- });
358
- });
359
- try {
360
- let line = '';
361
- while (pos >= 0 && lines.length < maxLines) {
362
- // Read a character from the file
363
- let char = await new Promise<string>((resolve, reject) => {
364
- fs.read(file, Buffer.allocUnsafe(1), 0, 1, pos, (err, bytesRead, buff) => {
365
- if (err) reject(err);
366
- else resolve(buff.toString());
367
- });
368
- });
369
- pos--;
370
- // If we hit the beginning of the file or a newline from a previous
371
- // record then we shoud save off the line and read the next record.
372
- if (newLines.includes(char) || pos === 0) {
373
- if (line.length > 0) lines.push(line);
374
- line = '';
375
- }
376
- else line += char;
377
- }
378
- } catch (err) { }
379
- finally { if (typeof file !== 'undefined') await new Promise<boolean>((resolve, reject) => fs.close(file, (err) => { if (err) reject(err); else resolve(true); })); }
380
- }
381
- return lines;
382
- } catch (err) { logger.error(err); }
383
- }
384
- private static makeLogFilePath(logFile: string) { return `${DataLogger.ensureLogPath()}/${logFile}`; }
385
- private static ensureLogPath(): string {
386
- let logPath = path.join(process.cwd(), '/logs');
387
- if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
388
- return logPath;
389
- }
390
- }
391
- export interface IDataLoggerEntry<T> {
392
- createInstance(entry?: string): T,
393
- parse(entry?: string): T
394
- }
395
- export class DataLoggerEntry {
396
- private static dateTestISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
397
- private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
398
- constructor(entry?: string | object) {
399
- // Parse the data from the log entry if it exists.
400
- if (typeof entry === 'object') entry = JSON.stringify(entry);
401
- if (typeof entry === 'string') this.parse(entry);
402
- }
403
- public createInstance(entry?: string) { return new DataLoggerEntry(entry); }
404
- public parse(entry: string) {
405
- let obj = typeof entry !== 'undefined' ? JSON.parse(entry, this.dateParser) : {};
406
- extend(true, this, obj);
407
- }
408
- protected dateParser(key, value) {
409
- if (typeof value === 'string') {
410
- let d = DataLoggerEntry.dateTestISO.exec(value);
411
- // By parsing the date and then creating a new date from that we will get
412
- // the date in the proper timezone.
413
- if (d) return new Date(Date.parse(value));
414
- d = DataLoggerEntry.dateTextAjax.exec(value);
415
- if (d) {
416
- // Not sure we will be seeing ajax dates but this is
417
- // something that we may see from external sources.
418
- let a = d[1].split(/[-+,.]/);
419
- return new Date(a[0] ? +a[0] : 0 - +a[1]);
420
- }
421
- }
422
- return value;
423
- }
424
- public toJSON() {
425
- return utils.replaceProps(this, (key, value) => {
426
- if (key.startsWith('_')) return undefined;
427
- if (typeof value === 'undefined' || value === null) return undefined;
428
- if (typeof value.getMonth === 'function') return Timestamp.toISOLocal(value);
429
- return value;
430
- });
431
- }
432
- public toLog(): string { return JSON.stringify(this) + '\n'; }
433
- }
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
+ }