@vbarbarosh/node-helpers 3.70.1 → 3.72.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.js +289 -13
  3. package/package.json +8 -2
  4. package/src/HeartbeatServer.js +38 -10
  5. package/src/HeartbeatServer.test.js +31 -0
  6. package/src/array_gcd.js +3 -10
  7. package/src/array_gcd.md +2 -0
  8. package/src/array_lcm.js +8 -0
  9. package/src/array_lcm.md +2 -0
  10. package/src/array_lcm.test.js +12 -0
  11. package/src/array_max.md +2 -1
  12. package/src/array_min.md +2 -1
  13. package/src/cli-apps/watchdog.d/connect_close.js +17 -0
  14. package/src/cli-apps/watchdog.d/connect_close2.js +20 -0
  15. package/src/cli-apps/watchdog.d/connect_close3.js +25 -0
  16. package/src/cli-apps/watchdog.d/connect_no_ping.js +13 -0
  17. package/src/cli-apps/watchdog.d/early_exit.js +7 -0
  18. package/src/cli-apps/watchdog.d/flood.js +13 -0
  19. package/src/cli-apps/watchdog.d/flood2.js +15 -0
  20. package/src/cli-apps/watchdog.d/hang.js +8 -0
  21. package/src/cli-apps/watchdog.d/huge_delay.js +18 -0
  22. package/src/cli-apps/watchdog.d/ignore_sigterm.js +29 -0
  23. package/src/cli-apps/watchdog.d/late_heartbeat.js +21 -0
  24. package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +16 -0
  25. package/src/cli-apps/watchdog.d/my-happy-path.js +16 -0
  26. package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +22 -0
  27. package/src/cli-apps/watchdog.d/my-never-ping.js +15 -0
  28. package/src/cli-apps/watchdog.d/no_heartbeat.js +5 -0
  29. package/src/cli-apps/watchdog.d/ok.js +17 -0
  30. package/src/cli-apps/watchdog.d/spawn_children.js +14 -0
  31. package/src/cli-apps/watchdog.js +146 -0
  32. package/src/cli.js +11 -3
  33. package/src/errors/ExitCodeError.js +9 -0
  34. package/src/fs_assert_file_socket.js +11 -0
  35. package/src/http_get_stream.js +2 -2
  36. package/src/math_gcd.js +10 -0
  37. package/src/math_gcd.md +2 -0
  38. package/src/math_lcm.js +8 -0
  39. package/src/math_lcm.md +2 -0
  40. package/src/parse_hms.js +43 -0
  41. package/src/parse_hms.md +9 -0
  42. package/src/parse_hms.test.js +32 -0
  43. package/src/pgid_exists.js +14 -0
  44. package/src/pgid_kill_grace.js +55 -0
  45. package/src/pid_exists.js +1 -0
  46. package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +33 -0
  47. package/src/pid_kill_grace.d/ignore-sigterm.js +22 -0
  48. package/src/pid_kill_grace.js +35 -9
  49. package/src/pid_kill_grace.test.js +63 -0
  50. package/src/ping_socket.js +8 -8
  51. package/src/ping_socket.test.js +58 -0
  52. package/src/range.js +44 -0
  53. package/src/range.md +15 -0
  54. package/src/range.test.js +41 -0
  55. package/src/shell_spawn.js +26 -1
  56. package/src/strtr.js +15 -0
package/README.md CHANGED
@@ -20,7 +20,7 @@ npm install @vbarbarosh/node-helpers
20
20
  A subset of `@vbarbarosh/node-helpers` is available for browser:
21
21
 
22
22
  ```html
23
- <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.70.1/dist/browser.js"></script>
23
+ <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.72.0/dist/browser.js"></script>
24
24
  ```
25
25
 
26
26
  ⚠️ All browser functions are exposed globally.
package/dist/browser.js CHANGED
@@ -84,6 +84,7 @@ var map = {
84
84
  "./array_group.js": "./src/array_group.js",
85
85
  "./array_group_map.js": "./src/array_group_map.js",
86
86
  "./array_index.js": "./src/array_index.js",
87
+ "./array_lcm.js": "./src/array_lcm.js",
87
88
  "./array_max.js": "./src/array_max.js",
88
89
  "./array_min.js": "./src/array_min.js",
89
90
  "./array_permutations.js": "./src/array_permutations.js",
@@ -93,6 +94,8 @@ var map = {
93
94
  "./array_sum.js": "./src/array_sum.js",
94
95
  "./array_unique.js": "./src/array_unique.js",
95
96
  "./array_unique_last.js": "./src/array_unique_last.js",
97
+ "./cli-apps/watchdog.d/ignore_sigterm.js": "./src/cli-apps/watchdog.d/ignore_sigterm.js",
98
+ "./cli-apps/watchdog.d/my-ignore-sigterm.js": "./src/cli-apps/watchdog.d/my-ignore-sigterm.js",
96
99
  "./date_add_hours.js": "./src/date_add_hours.js",
97
100
  "./date_add_milliseconds.js": "./src/date_add_milliseconds.js",
98
101
  "./date_add_minutes.js": "./src/date_add_minutes.js",
@@ -143,6 +146,8 @@ var map = {
143
146
  "./http_put_utf8.js": "./src/http_put_utf8.js",
144
147
  "./identity.js": "./src/identity.js",
145
148
  "./ignore.js": "./src/ignore.js",
149
+ "./pid_kill_grace.d/ignore-sigterm-for-10ms.js": "./src/pid_kill_grace.d/ignore-sigterm-for-10ms.js",
150
+ "./pid_kill_grace.d/ignore-sigterm.js": "./src/pid_kill_grace.d/ignore-sigterm.js",
146
151
  "./plural.js": "./src/plural.js",
147
152
  "./random_int.js": "./src/random_int.js",
148
153
  "./urlmod.js": "./src/urlmod.js",
@@ -206,21 +211,14 @@ module.exports = array_chunk;
206
211
  /*!**************************!*\
207
212
  !*** ./src/array_gcd.js ***!
208
213
  \**************************/
209
- /***/ ((module) => {
214
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
210
215
 
211
- // https://stackoverflow.com/a/39764792/1478566
212
- function array_gcd(array, fn = Number)
213
- {
214
- return array.reduce((a,b) => gcd(fn(a), fn(b)));
215
- }
216
+ const math_gcd = __webpack_require__(/*! ./math_gcd */ "./src/math_gcd.js");
216
217
 
217
218
  // https://stackoverflow.com/a/39764792/1478566
218
- function gcd(a, b)
219
+ function array_gcd(array, fn = Number)
219
220
  {
220
- if (b) {
221
- return gcd(b, a % b);
222
- }
223
- return a;
221
+ return array.reduce((a,b) => math_gcd(fn(a), fn(b)));
224
222
  }
225
223
 
226
224
  module.exports = array_gcd;
@@ -311,6 +309,24 @@ function array_index(array, fn = identity)
311
309
  module.exports = array_index;
312
310
 
313
311
 
312
+ /***/ }),
313
+
314
+ /***/ "./src/array_lcm.js":
315
+ /*!**************************!*\
316
+ !*** ./src/array_lcm.js ***!
317
+ \**************************/
318
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
319
+
320
+ const math_lcm = __webpack_require__(/*! ./math_lcm */ "./src/math_lcm.js");
321
+
322
+ function array_lcm(array, fn = Number)
323
+ {
324
+ return array.reduce((a,b) => math_lcm(fn(a), fn(b)));
325
+ }
326
+
327
+ module.exports = array_lcm;
328
+
329
+
314
330
  /***/ }),
315
331
 
316
332
  /***/ "./src/array_max.js":
@@ -632,6 +648,116 @@ function array_unique_last(array, fn = identity)
632
648
  module.exports = array_unique_last;
633
649
 
634
650
 
651
+ /***/ }),
652
+
653
+ /***/ "./src/cli-apps/watchdog.d/ignore_sigterm.js":
654
+ /*!***************************************************!*\
655
+ !*** ./src/cli-apps/watchdog.d/ignore_sigterm.js ***!
656
+ \***************************************************/
657
+ /***/ (() => {
658
+
659
+ //#!/usr/bin/env node
660
+
661
+ // 1️⃣ Client that ignores SIGTERM
662
+ // Goal: process stays alive after SIGTERM and only dies on SIGKILL (exactly what pid_kill_grace should handle).
663
+ //
664
+ // What this tests for watchdog:
665
+ // - watchdog sends SIGTERM → process prints “ignored” and keeps running.
666
+ // - pid_kill_grace waits grace_timeout_ms, sees PID still alive, sends SIGKILL.
667
+ // - Process must die after SIGKILL.
668
+ // - Watchdog should log the whole sequence correctly.
669
+ // This is a realistic scenario.
670
+
671
+ console.log('PID:', process.pid);
672
+
673
+ // Ignore SIGTERM
674
+ process.on('SIGTERM', function () {
675
+ console.log('SIGTERM received, but intentionally ignored');
676
+ });
677
+
678
+ // Still react to SIGINT (Ctrl+C) so you can stop it manually if needed
679
+ process.on('SIGINT', function () {
680
+ console.log('SIGINT received, exiting');
681
+ process.exit(130);
682
+ });
683
+
684
+ // Keep process alive
685
+ setInterval(() => {
686
+ // simulate work
687
+ }, 1000);
688
+
689
+
690
+ /***/ }),
691
+
692
+ /***/ "./src/cli-apps/watchdog.d/my-ignore-sigterm.js":
693
+ /*!******************************************************!*\
694
+ !*** ./src/cli-apps/watchdog.d/my-ignore-sigterm.js ***!
695
+ \******************************************************/
696
+ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
697
+
698
+ //#!/usr/bin/env node
699
+
700
+ const Promise = __webpack_require__(/*! bluebird */ "bluebird");
701
+ const cli = __webpack_require__(/*! @vbarbarosh/node-helpers/src/cli */ "./src/cli.js");
702
+ const now_human = __webpack_require__(/*! @vbarbarosh/node-helpers/src/now_human */ "./src/now_human.js");
703
+
704
+ cli(main);
705
+
706
+ async function main()
707
+ {
708
+ process.on('SIGTERM', function () {
709
+ console.log(`[${now_human()}][ignore-sigterm] SIGTERM, ignoring...`);
710
+ });
711
+ process.on('SIGINT', function () {
712
+ console.log(`[${now_human()}][ignore-sigterm] SIGINT, ignoring...`);
713
+ });
714
+
715
+ for (let iter = 1, last = 100; iter <= last; ++iter) {
716
+ console.log(`[${now_human()}][ignore-sigterm] ${iter} of ${last}`);
717
+ await Promise.delay(100);
718
+ }
719
+ }
720
+
721
+
722
+ /***/ }),
723
+
724
+ /***/ "./src/cli.js":
725
+ /*!********************!*\
726
+ !*** ./src/cli.js ***!
727
+ \********************/
728
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
729
+
730
+ const ExitCodeError = __webpack_require__(/*! ./errors/ExitCodeError */ "./src/errors/ExitCodeError.js");
731
+ const Promise = __webpack_require__(/*! bluebird */ "bluebird");
732
+
733
+ /**
734
+ * Entry point for Node CLI apps
735
+ */
736
+ function cli(main, report = error => console.error(error))
737
+ {
738
+ // https://stackoverflow.com/a/46916601/1478566
739
+ const timer = setInterval(v => v, 1E9);
740
+
741
+ Promise.resolve(main()).then(resolve, reject);
742
+
743
+ function resolve() {
744
+ clearInterval(timer);
745
+ }
746
+ function reject(error) {
747
+ clearInterval(timer);
748
+ report(error);
749
+ if (error instanceof ExitCodeError) {
750
+ process.exit(error.exit_code);
751
+ }
752
+ else {
753
+ process.exit(1);
754
+ }
755
+ }
756
+ }
757
+
758
+ module.exports = cli;
759
+
760
+
635
761
  /***/ }),
636
762
 
637
763
  /***/ "./src/date_add_hours.js":
@@ -775,6 +901,25 @@ function date_is_leap_year(d)
775
901
  module.exports = date_is_leap_year;
776
902
 
777
903
 
904
+ /***/ }),
905
+
906
+ /***/ "./src/errors/ExitCodeError.js":
907
+ /*!*************************************!*\
908
+ !*** ./src/errors/ExitCodeError.js ***!
909
+ \*************************************/
910
+ /***/ ((module) => {
911
+
912
+ class ExitCodeError extends Error
913
+ {
914
+ constructor(exit_code, message = '') {
915
+ super(message);
916
+ this.exit_code = exit_code;
917
+ }
918
+ }
919
+
920
+ module.exports = ExitCodeError;
921
+
922
+
778
923
  /***/ }),
779
924
 
780
925
  /***/ "./src/factorize_ms.js":
@@ -2036,6 +2181,137 @@ const json_stringify_safe = __webpack_require__(/*! json-stringify-safe */ "./no
2036
2181
  module.exports = json_stringify_safe;
2037
2182
 
2038
2183
 
2184
+ /***/ }),
2185
+
2186
+ /***/ "./src/math_gcd.js":
2187
+ /*!*************************!*\
2188
+ !*** ./src/math_gcd.js ***!
2189
+ \*************************/
2190
+ /***/ ((module) => {
2191
+
2192
+ // https://stackoverflow.com/a/39764792/1478566
2193
+ function math_gcd(a, b)
2194
+ {
2195
+ if (b) {
2196
+ return math_gcd(b, a % b);
2197
+ }
2198
+ return a;
2199
+ }
2200
+
2201
+ module.exports = math_gcd;
2202
+
2203
+
2204
+ /***/ }),
2205
+
2206
+ /***/ "./src/math_lcm.js":
2207
+ /*!*************************!*\
2208
+ !*** ./src/math_lcm.js ***!
2209
+ \*************************/
2210
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
2211
+
2212
+ const math_gcd = __webpack_require__(/*! ./math_gcd */ "./src/math_gcd.js");
2213
+
2214
+ function math_lcm(a, b)
2215
+ {
2216
+ return Math.abs(a*b) / math_gcd(a, b);
2217
+ }
2218
+
2219
+ module.exports = math_lcm;
2220
+
2221
+
2222
+ /***/ }),
2223
+
2224
+ /***/ "./src/now_human.js":
2225
+ /*!**************************!*\
2226
+ !*** ./src/now_human.js ***!
2227
+ \**************************/
2228
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
2229
+
2230
+ const format_date_human = __webpack_require__(/*! ./format_date_human */ "./src/format_date_human.js");
2231
+
2232
+ function now_human()
2233
+ {
2234
+ return format_date_human(new Date());
2235
+ }
2236
+
2237
+ module.exports = now_human;
2238
+
2239
+
2240
+ /***/ }),
2241
+
2242
+ /***/ "./src/pid_kill_grace.d/ignore-sigterm-for-10ms.js":
2243
+ /*!*********************************************************!*\
2244
+ !*** ./src/pid_kill_grace.d/ignore-sigterm-for-10ms.js ***!
2245
+ \*********************************************************/
2246
+ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
2247
+
2248
+ //#!/usr/bin/env node
2249
+
2250
+ const Promise = __webpack_require__(/*! bluebird */ "bluebird");
2251
+ const cli = __webpack_require__(/*! ../cli */ "./src/cli.js");
2252
+ const now_human = __webpack_require__(/*! ../now_human */ "./src/now_human.js");
2253
+
2254
+ // Child ignores SIGTERM for 10 ms, but dies naturally before the grace loop ends
2255
+
2256
+ cli(main);
2257
+
2258
+ async function main()
2259
+ {
2260
+ let timer;
2261
+
2262
+ process.on('SIGTERM', function () {
2263
+ console.log(`[${now_human()}][ignore-sigterm-for-10ms] SIGTERM_ignoring...`);
2264
+ timer ??= setTimeout(terminate, 10);
2265
+ });
2266
+ process.on('SIGINT', function () {
2267
+ console.log(`[${now_human()}][ignore-sigterm-for-10ms] SIGINT_ignoring...`);
2268
+ });
2269
+
2270
+ for (let iter = 1, last = 100; iter <= last; ++iter) {
2271
+ console.log(`[${now_human()}][ignore-sigterm-for-10ms] ${iter} of ${last}`);
2272
+ await Promise.delay(10);
2273
+ }
2274
+ }
2275
+
2276
+ function terminate()
2277
+ {
2278
+ console.log(`[${now_human()}][ignore-sigterm-for-10ms] TERMINATE_AFTER_10MS`);
2279
+ process.exit(0);
2280
+ }
2281
+
2282
+
2283
+ /***/ }),
2284
+
2285
+ /***/ "./src/pid_kill_grace.d/ignore-sigterm.js":
2286
+ /*!************************************************!*\
2287
+ !*** ./src/pid_kill_grace.d/ignore-sigterm.js ***!
2288
+ \************************************************/
2289
+ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
2290
+
2291
+ //#!/usr/bin/env node
2292
+
2293
+ const Promise = __webpack_require__(/*! bluebird */ "bluebird");
2294
+ const cli = __webpack_require__(/*! ../cli */ "./src/cli.js");
2295
+ const now_human = __webpack_require__(/*! ../now_human */ "./src/now_human.js");
2296
+
2297
+ cli(main);
2298
+
2299
+ async function main()
2300
+ {
2301
+ process.on('SIGTERM', function () {
2302
+ console.log(`[${now_human()}][ignore-sigterm] SIGTERM_ignoring...`);
2303
+ });
2304
+ process.on('SIGINT', function () {
2305
+ console.log(`[${now_human()}][ignore-sigterm] SIGINT_ignoring...`);
2306
+ });
2307
+
2308
+ for (let iter = 1, last = 100; iter <= last; ++iter) {
2309
+ console.log(`[${now_human()}][ignore-sigterm] ${iter} of ${last}`);
2310
+ await Promise.delay(10);
2311
+ }
2312
+ }
2313
+
2314
+
2039
2315
  /***/ }),
2040
2316
 
2041
2317
  /***/ "./src/plural.js":
@@ -2223,10 +2499,10 @@ var __webpack_exports__ = {};
2223
2499
  \******************************/
2224
2500
  const ns = new URL(document.currentScript.src).searchParams.get('var') ?? 'h';
2225
2501
  if (typeof window[ns] !== 'undefined') {
2226
- console.log(`❌ @vbarbarosh/node-helpers@${"3.70.1"} was not injected — window.${ns} is already in use`);
2502
+ console.log(`❌ @vbarbarosh/node-helpers@${"3.72.0"} was not injected — window.${ns} is already in use`);
2227
2503
  }
2228
2504
  else {
2229
- console.log(`🎉 @vbarbarosh/node-helpers@${"3.70.1"} successfully exposed as window.${ns}`);
2505
+ console.log(`🎉 @vbarbarosh/node-helpers@${"3.72.0"} successfully exposed as window.${ns}`);
2230
2506
  window[ns] = {};
2231
2507
  // https://github.com/webpack/webpack/issues/625
2232
2508
  // https://webpack.js.org/guides/dependency-management/#require-context
package/package.json CHANGED
@@ -2,11 +2,14 @@
2
2
  "author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
3
3
  "license": "MIT",
4
4
  "name": "@vbarbarosh/node-helpers",
5
- "version": "3.70.1",
5
+ "version": "3.72.0",
6
6
  "description": "A set of helpers for JavaScript/Node.js",
7
7
  "unpkg": "dist/browser.js",
8
8
  "jsdelivr": "dist/browser.js",
9
9
  "homepage": "https://vbarbarosh.github.io/node-helpers/",
10
+ "bin": {
11
+ "watchdog": "src/cli-apps/watchdog.js"
12
+ },
10
13
  "files": [
11
14
  "dist",
12
15
  "src"
@@ -28,6 +31,7 @@
28
31
  "url": "https://github.com/vbarbarosh/node-helpers/issues"
29
32
  },
30
33
  "peerDependencies": {
34
+ "@paralleldrive/cuid2": "^3.0.4",
31
35
  "@vbarbarosh/type-helpers": "^0.2.0",
32
36
  "axios": "^1.7.9",
33
37
  "bluebird": "^3.7.2",
@@ -38,7 +42,9 @@
38
42
  "json-stringify-safe": "^5.0.1",
39
43
  "mime-types": "^2.1.35",
40
44
  "sanitize-filename": "^1.6.3",
41
- "split": "^1.0.1",
45
+ "split": "^1.0.1"
46
+ },
47
+ "optionalDependencies": {
42
48
  "ssh2": "^1.16.0"
43
49
  },
44
50
  "devDependencies": {
@@ -1,4 +1,5 @@
1
1
  const EventEmitter = require('events');
2
+ const ExitCodeError = require('./errors/ExitCodeError');
2
3
  const Promise = require('bluebird');
3
4
  const fs = require('fs');
4
5
  const fs_path_join = require('./fs_path_join');
@@ -7,6 +8,7 @@ const net = require('net');
7
8
  const now_fs = require('./now_fs');
8
9
  const os = require('os');
9
10
 
11
+ // 💎 Only the lack of a valid PING within WATCHDOG_INTERVAL is fatal
10
12
  class HeartbeatServer extends EventEmitter
11
13
  {
12
14
  #socket_path;
@@ -37,36 +39,52 @@ class HeartbeatServer extends EventEmitter
37
39
  this.#reject = reject;
38
40
  });
39
41
 
40
- this.#server = net.createServer(function (socket) {
41
- socket.setEncoding('utf8');
42
- socket.on('error', function (error) {
43
- _this.#reject(new Error(`Client Socket Failed: ${error.message}`));
42
+ this.#server = net.createServer(function (client) {
43
+ let bytes_received = 0;
44
+ client.setEncoding('utf8');
45
+ client.on('error', function (error) {
46
+ if (!_this.#server) {
47
+ return;
48
+ }
49
+ _this.emit('warning', error);
44
50
  });
45
- socket.on('end', function () {
51
+ client.on('end', function () {
52
+ if (!_this.#server) {
53
+ return;
54
+ }
46
55
  _this.#last_ping = Date.now();
47
56
  _this.emit('heartbeat');
48
57
  });
49
- socket.on('data', function () {
58
+ client.on('data', function (buf) {
59
+ if (!_this.#server) {
60
+ return;
61
+ }
50
62
  // Not really interested in any data
63
+ bytes_received += buf.length;
64
+ if (bytes_received > 1024*1024) {
65
+ _this.emit('warning', new Error(`Client floods. Bytes recieved: ${bytes_received}`));
66
+ client.destroy();
67
+ }
51
68
  });
52
- socket.end('OK');
69
+ client.end('OK');
53
70
  });
54
71
  this.#server.on('error', function (error) {
55
72
  _this.#reject(new Error(`Server Failed: ${error.message}`));
56
73
  });
57
74
  this.#server.on('close', function () {
58
- _this.#reject(new Error(`Server Closed`));
75
+ _this.#reject(new Error('Server Closed'));
59
76
  });
60
77
  this.#server.listen(this.#socket_path, function () {
61
78
  // started = true;
62
79
  });
63
80
  }
64
81
 
65
- #tick() {
82
+ async #tick() {
66
83
  if (this.#last_ping + this.#interval_ms < Date.now()) {
67
84
  clearInterval(this.#timer);
68
85
  this.#timer = null;
69
- this.#reject(new Error(`No heartbeat for the last ${this.#interval_ms}ms`));
86
+ this.#reject(new ExitCodeError(124, `No heartbeat for the last ${this.#interval_ms}ms`));
87
+ await this.dispose();
70
88
  }
71
89
  }
72
90
 
@@ -78,20 +96,30 @@ class HeartbeatServer extends EventEmitter
78
96
  return this.#promise;
79
97
  }
80
98
 
99
+ // - Stops the watchdog
100
+ // - Closes socket
101
+ // - Removes socket file
102
+ // - Rejects the internal promise with a "Server Closed" error
103
+ // - Safe to call multiple times (idempotent)
81
104
  async dispose() {
82
105
  if (this.#timer) {
83
106
  clearInterval(this.#timer);
84
107
  this.#timer = null;
85
108
  }
109
+ if (!this.#server) {
110
+ return;
111
+ }
86
112
  try {
87
113
  const _this = this;
88
114
  await new Promise(function (resolve, reject) {
89
115
  _this.#server.removeAllListeners();
90
116
  _this.#server.close(error => error ? reject(error) : resolve());
117
+ _this.#server = null;
91
118
  });
92
119
  }
93
120
  finally {
94
121
  await fs_rmf(this.#socket_path);
122
+ this.#socket_path = null;
95
123
  }
96
124
  }
97
125
  }
@@ -0,0 +1,31 @@
1
+ const HeartbeatServer = require('./HeartbeatServer');
2
+ const assert = require('assert');
3
+ const fs_assert_file_socket = require('./fs_assert_file_socket');
4
+ const ping_socket = require('./ping_socket');
5
+
6
+ // 💎 Only the lack of a valid PING within WATCHDOG_INTERVAL is fatal
7
+
8
+ // 📕 Notes by ChatGPT
9
+ // If listen() fails (EADDRINUSE, perms, etc.), constructor will throw,
10
+ // and heartbeat_server will be undefined in finally { await heartbeat_server.dispose() }.
11
+
12
+ describe('HeartbeatServer', function () {
13
+ it('Happy Path', async function () {
14
+ const server = new HeartbeatServer();
15
+ try {
16
+ let counter = 0;
17
+ server.on('heartbeat', () => counter++);
18
+ await fs_assert_file_socket(server.socket_path);
19
+ await ping_socket(server.socket_path);
20
+ await ping_socket(server.socket_path);
21
+ assert.strictEqual(counter, 2);
22
+ }
23
+ finally {
24
+ await server.dispose();
25
+ }
26
+ });
27
+
28
+ // should emit a "warning" event on any client errors
29
+ // should not send any heartbeat signals after rejection (decision to kill a child process)
30
+ // should reject when no heartbeat received in an expected interval
31
+ });
package/src/array_gcd.js CHANGED
@@ -1,16 +1,9 @@
1
- // https://stackoverflow.com/a/39764792/1478566
2
- function array_gcd(array, fn = Number)
3
- {
4
- return array.reduce((a,b) => gcd(fn(a), fn(b)));
5
- }
1
+ const math_gcd = require('./math_gcd');
6
2
 
7
3
  // https://stackoverflow.com/a/39764792/1478566
8
- function gcd(a, b)
4
+ function array_gcd(array, fn = Number)
9
5
  {
10
- if (b) {
11
- return gcd(b, a % b);
12
- }
13
- return a;
6
+ return array.reduce((a,b) => math_gcd(fn(a), fn(b)));
14
7
  }
15
8
 
16
9
  module.exports = array_gcd;
@@ -0,0 +1,2 @@
1
+ Returns the greatest common divisor (GCD) of all elements in the array.
2
+ `fn(item)` is applied to each element before computing.
@@ -0,0 +1,8 @@
1
+ const math_lcm = require('./math_lcm');
2
+
3
+ function array_lcm(array, fn = Number)
4
+ {
5
+ return array.reduce((a,b) => math_lcm(fn(a), fn(b)));
6
+ }
7
+
8
+ module.exports = array_lcm;
@@ -0,0 +1,2 @@
1
+ Returns the least common multiple (LCM) of all elements in the array.
2
+ `fn(item)` is applied to each element before computing.
@@ -0,0 +1,12 @@
1
+ const array_lcm = require('./array_lcm');
2
+ const assert = require('assert');
3
+
4
+ describe('array_lcm', function () {
5
+ it('should handle basic input', function () {
6
+ assert.deepStrictEqual(array_lcm([1,2,3,4,5]), 60);
7
+ assert.deepStrictEqual(array_lcm([15,25]), 75);
8
+ assert.deepStrictEqual(array_lcm([4,6,8]), 24);
9
+ assert.deepStrictEqual(array_lcm([4,8,16]), 16);
10
+ assert.deepStrictEqual(array_lcm([300,100,200,300]), 600);
11
+ });
12
+ });
package/src/array_max.md CHANGED
@@ -1 +1,2 @@
1
- Returns the first element in the array with the maximal weight.
1
+ Returns the first element of the array for which `fn(item)` is maximized.
2
+ If multiple elements have the same maximal weight, the earliest one is returned.
package/src/array_min.md CHANGED
@@ -1 +1,2 @@
1
- Returns the first element in the array with the minimal weight.
1
+ Returns the first element of the array for which `fn(item)` is minimized.
2
+ If multiple elements have the same minimal weight, the earliest one is returned.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ // 9. Client that connects but sends absolutely NO DATA.
4
+ // Watchdog must NOT treat this as a PING.
5
+
6
+ const net = require('net');
7
+
8
+ const SOCKET = process.env.WATCHDOG_SOCKET;
9
+ if (!SOCKET) throw new Error('WATCHDOG_SOCKET missing');
10
+
11
+ // Connect → immediately destroy the socket → NO write events
12
+ const socket = net.connect(SOCKET, () => {
13
+ console.log('connected (no ping), closing immediately');
14
+ socket.destroy(); // IMPORTANT: destroy() does not flush anything
15
+ });
16
+
17
+ setInterval(() => {}, 1000);
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ // 9. Client that connects but sends absolutely NO DATA.
4
+ // Watchdog must NOT treat this as a PING.
5
+
6
+ const net = require('net');
7
+
8
+ const SOCKET = process.env.WATCHDOG_SOCKET;
9
+ if (!SOCKET) throw new Error('WATCHDOG_SOCKET missing');
10
+
11
+ setInterval(ping, 1000);
12
+
13
+ function ping()
14
+ {
15
+ // Connect → immediately destroy the socket → NO write events
16
+ const socket = net.connect(SOCKET, () => {
17
+ console.log('connected (no ping), closing immediately');
18
+ socket.destroy(); // IMPORTANT: destroy() does not flush anything
19
+ });
20
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 11. Client sends heartbeats sporadically (sometimes missing)
4
+ // Simulates flaky client.
5
+
6
+ const net = require('net');
7
+
8
+ const SOCKET = process.env.WATCHDOG_SOCKET;
9
+ if (!SOCKET) throw new Error('WATCHDOG_SOCKET missing');
10
+
11
+ setInterval(ping, 1000);
12
+
13
+ function ping()
14
+ {
15
+ // Connect → immediately destroy the socket → NO write events
16
+ const socket = net.connect(SOCKET, () => {
17
+ if (Math.random() > 0.5) {
18
+ socket.end('PING');
19
+ }
20
+ else {
21
+ console.log('connected (no ping), closing immediately');
22
+ socket.destroy(); // IMPORTANT: destroy() does not flush anything
23
+ }
24
+ });
25
+ }