@vbarbarosh/node-helpers 3.72.4 → 3.73.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.
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.72.4/dist/browser.js"></script>
23
+ <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.73.0/dist/browser.js"></script>
24
24
  ```
25
25
 
26
26
  ⚠️ All browser functions are exposed globally.
package/dist/browser.js CHANGED
@@ -72,14 +72,16 @@ function serializer(replacer, cycleReplacer) {
72
72
 
73
73
  /***/ }),
74
74
 
75
- /***/ "./src sync recursive \\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$":
76
- /*!*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
77
- !*** ./src/ sync \b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\.test)\.js$ ***!
78
- \*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
75
+ /***/ "./src sync recursive ^(?%21.*(cli-apps%7Cbrowser%7C\\.d)\\/).*\\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$":
76
+ /*!**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
77
+ !*** ./src/ sync ^(?%21.*(cli-apps%7Cbrowser%7C\.d)\/).*\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\.test)\.js$ ***!
78
+ \**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
79
79
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
80
80
 
81
81
  var map = {
82
82
  "./array_chunk.js": "./src/array_chunk.js",
83
+ "./array_chunk_jobs.js": "./src/array_chunk_jobs.js",
84
+ "./array_fps.js": "./src/array_fps.js",
83
85
  "./array_gcd.js": "./src/array_gcd.js",
84
86
  "./array_group.js": "./src/array_group.js",
85
87
  "./array_group_map.js": "./src/array_group_map.js",
@@ -94,8 +96,6 @@ var map = {
94
96
  "./array_sum.js": "./src/array_sum.js",
95
97
  "./array_unique.js": "./src/array_unique.js",
96
98
  "./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",
99
99
  "./date_add_hours.js": "./src/date_add_hours.js",
100
100
  "./date_add_milliseconds.js": "./src/date_add_milliseconds.js",
101
101
  "./date_add_minutes.js": "./src/date_add_minutes.js",
@@ -146,8 +146,6 @@ var map = {
146
146
  "./http_put_utf8.js": "./src/http_put_utf8.js",
147
147
  "./identity.js": "./src/identity.js",
148
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",
151
149
  "./plural.js": "./src/plural.js",
152
150
  "./random_int.js": "./src/random_int.js",
153
151
  "./urlmod.js": "./src/urlmod.js",
@@ -172,7 +170,7 @@ webpackContext.keys = function webpackContextKeys() {
172
170
  };
173
171
  webpackContext.resolve = webpackContextResolve;
174
172
  module.exports = webpackContext;
175
- webpackContext.id = "./src sync recursive \\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$";
173
+ webpackContext.id = "./src sync recursive ^(?%21.*(cli-apps%7Cbrowser%7C\\.d)\\/).*\\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$";
176
174
 
177
175
  /***/ }),
178
176
 
@@ -205,6 +203,83 @@ function array_chunk(array = [], limit = 1)
205
203
  module.exports = array_chunk;
206
204
 
207
205
 
206
+ /***/ }),
207
+
208
+ /***/ "./src/array_chunk_jobs.js":
209
+ /*!*********************************!*\
210
+ !*** ./src/array_chunk_jobs.js ***!
211
+ \*********************************/
212
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
213
+
214
+ const clamp = __webpack_require__(/*! ./clamp */ "./src/clamp.js");
215
+
216
+ /**
217
+ * Split an array of jobs between up to `max_threads`, while trying to put at least
218
+ * `min_jobs_per_thread` items into each chunk.
219
+ *
220
+ * - If an array is empty → returns [].
221
+ * - If there are too few items to satisfy `min_items_per_chunk`, even for 1 chunk,
222
+ * everything goes into a single chunk.
223
+ *
224
+ * Alternative names:
225
+ * - array_chunk_balanced
226
+ *
227
+ * @param array
228
+ * @param max_threads
229
+ * @param min_jobs_per_thread
230
+ * @returns {*[]}
231
+ */
232
+ function array_chunk_jobs(array = [], max_threads = 1, min_jobs_per_thread = 1)
233
+ {
234
+ const total_threads = clamp(1, max_threads, Math.floor(array.length / min_jobs_per_thread));
235
+ const jobs_per_thread = Math.floor(array.length / total_threads);
236
+ let extra = array.length % total_threads;
237
+
238
+ const out = [];
239
+ for (let i = 0; i < array.length; ) {
240
+ const size = jobs_per_thread + (extra ? 1 : 0);
241
+ out.push(array.slice(i, i + size));
242
+ i += size;
243
+ if (extra) {
244
+ extra--;
245
+ }
246
+ }
247
+ return out;
248
+ }
249
+
250
+ module.exports = array_chunk_jobs;
251
+
252
+
253
+ /***/ }),
254
+
255
+ /***/ "./src/array_fps.js":
256
+ /*!**************************!*\
257
+ !*** ./src/array_fps.js ***!
258
+ \**************************/
259
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
260
+
261
+ const round = __webpack_require__(/*! ./round */ "./src/round.js");
262
+
263
+ const EPS = 0.00001;
264
+
265
+ function array_fps(duration_sec, fps)
266
+ {
267
+ const total_frames = Math.floor(duration_sec * fps);
268
+
269
+ if (total_frames <= 1) {
270
+ return [0];
271
+ }
272
+
273
+ const out = [];
274
+ for (let i = 0; i < total_frames; ++i) {
275
+ out.push(round((i / (total_frames - 1)) * (duration_sec - EPS), EPS));
276
+ }
277
+ return out;
278
+ }
279
+
280
+ module.exports = array_fps;
281
+
282
+
208
283
  /***/ }),
209
284
 
210
285
  /***/ "./src/array_gcd.js":
@@ -650,112 +725,18 @@ module.exports = array_unique_last;
650
725
 
651
726
  /***/ }),
652
727
 
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");
728
+ /***/ "./src/clamp.js":
729
+ /*!**********************!*\
730
+ !*** ./src/clamp.js ***!
731
+ \**********************/
732
+ /***/ ((module) => {
732
733
 
733
- /**
734
- * Entry point for Node CLI apps
735
- */
736
- function cli(main, report = error => console.error(error))
734
+ function clamp(min, max, value)
737
735
  {
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
- }
736
+ return Math.max(min, Math.min(max, value));
756
737
  }
757
738
 
758
- module.exports = cli;
739
+ module.exports = clamp;
759
740
 
760
741
 
761
742
  /***/ }),
@@ -901,25 +882,6 @@ function date_is_leap_year(d)
901
882
  module.exports = date_is_leap_year;
902
883
 
903
884
 
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
-
923
885
  /***/ }),
924
886
 
925
887
  /***/ "./src/factorize_ms.js":
@@ -1725,7 +1687,7 @@ const factorize_ms = __webpack_require__(/*! ./factorize_ms */ "./src/factorize_
1725
1687
  /**
1726
1688
  * Format milliseconds
1727
1689
  */
1728
- function format_ms3(ms)
1690
+ function format_ms3(ms, include_zero_hours = false)
1729
1691
  {
1730
1692
  if (!ms) {
1731
1693
  ms = 0;
@@ -1733,7 +1695,7 @@ function format_ms3(ms)
1733
1695
 
1734
1696
  // 00:05.445
1735
1697
  const [h, m, s, xx] = factorize_ms(ms);
1736
- if (h) {
1698
+ if (h || include_zero_hours) {
1737
1699
  return `0${h}:0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '');
1738
1700
  }
1739
1701
  return `0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '')
@@ -2219,99 +2181,6 @@ function math_lcm(a, b)
2219
2181
  module.exports = math_lcm;
2220
2182
 
2221
2183
 
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
-
2315
2184
  /***/ }),
2316
2185
 
2317
2186
  /***/ "./src/plural.js":
@@ -2350,6 +2219,117 @@ function random_int(min, max)
2350
2219
  module.exports = random_int;
2351
2220
 
2352
2221
 
2222
+ /***/ }),
2223
+
2224
+ /***/ "./src/round.js":
2225
+ /*!**********************!*\
2226
+ !*** ./src/round.js ***!
2227
+ \**********************/
2228
+ /***/ ((module) => {
2229
+
2230
+ function round(value, precision = 1)
2231
+ {
2232
+ if (precision === 0) {
2233
+ return value;
2234
+ }
2235
+
2236
+ const factor = 1 / precision;
2237
+ const sign = Math.sign(value);
2238
+
2239
+ // Count decimal places for the cleanup (toFixed)
2240
+ const exponent = get_decimal_places(precision);
2241
+
2242
+ // Small epsilon helps fix most floating point edge cases involving ties
2243
+ const epsilon = 1e-12*sign;
2244
+
2245
+ // Scale → Nudge → Round → Unscale
2246
+ const final = (Math.round(value*factor + epsilon) / factor);
2247
+
2248
+ // Cleanup trailing garbage such as: 34.038000000000004 → 34.038
2249
+ return Number(final.toFixed(exponent));
2250
+ }
2251
+
2252
+ function get_decimal_places(precision)
2253
+ {
2254
+ if (Math.floor(precision) === precision) {
2255
+ return 0;
2256
+ }
2257
+
2258
+ // Scientific notation support: e.g., 1e-20
2259
+ const e = precision.toExponential().split('e');
2260
+ if (e.length > 1) {
2261
+ return Math.abs(parseInt(e[1], 10));
2262
+ }
2263
+
2264
+ // Normal decimal notation (e.g., 0.001)
2265
+ return precision.toString().split('.')[1]?.length || 0;
2266
+ }
2267
+
2268
+ // // round(34.037531, 0.001) -> 34.038000000000004
2269
+ // function round(value, precision = 1)
2270
+ // {
2271
+ // return Math.round(value / precision) * precision;
2272
+ // }
2273
+
2274
+ // // round(1.005, 0.01) -> 1
2275
+ // function round(value, precision = 1)
2276
+ // {
2277
+ // const p = 1 / precision;
2278
+ // return Math.round(value * p) / p;
2279
+ // }
2280
+
2281
+ // function round(value, precision = 1)
2282
+ // {
2283
+ // if (precision === 0) {
2284
+ // return value;
2285
+ // }
2286
+ // const factor = 1 / precision;
2287
+ // const epsilon = 1e-12 * Math.sign(value);
2288
+ // return Number((Math.round((value * factor) + epsilon) / factor).toPrecision(15));
2289
+ // }
2290
+
2291
+ // function round(value, precision = 1)
2292
+ // {
2293
+ // if (precision === 0) {
2294
+ // return value;
2295
+ // }
2296
+ //
2297
+ // const factor = 1 / precision;
2298
+ //
2299
+ // // The exponent is critical for defining the precision of the cleanup
2300
+ // const exponent = Math.max(0, String(precision).length - 2);
2301
+ //
2302
+ // // 1. Calculate the scaled value.
2303
+ // let scaled = value * factor;
2304
+ //
2305
+ // // 2. Strategic Nudge: Add a tiny, signed epsilon (1e-12) to correct floating-point
2306
+ // // representation errors and force symmetric rounding (as Math.abs failed).
2307
+ // const epsilon = 1e-12 * Math.sign(value);
2308
+ //
2309
+ // // 3. Round the nudged value.
2310
+ // const roundedScaled = Math.round(scaled + epsilon);
2311
+ //
2312
+ // // 4. Unscale the value.
2313
+ // const result = roundedScaled / factor;
2314
+ //
2315
+ // // 5. CRITICAL FINAL STEP: Use .toFixed() with the required decimal places for cleanup.
2316
+ // // This stabilizes the result against the strict equality checks, including
2317
+ // // the safe integer boundary errors caused by the final division.
2318
+ // return Number(result.toFixed(exponent));
2319
+ // }
2320
+
2321
+ // 💎 The most precise solution, but - extra dependency.
2322
+ //
2323
+ // const Decimal = require('decimal.js');
2324
+ //
2325
+ // function round(value, precision = 1)
2326
+ // {
2327
+ // return new Decimal(value).div(precision).round().times(precision).toNumber();
2328
+ // }
2329
+
2330
+ module.exports = round;
2331
+
2332
+
2353
2333
  /***/ }),
2354
2334
 
2355
2335
  /***/ "./src/urlmod.js":
@@ -2499,14 +2479,14 @@ var __webpack_exports__ = {};
2499
2479
  \******************************/
2500
2480
  const ns = new URL(document.currentScript.src).searchParams.get('var') ?? 'h';
2501
2481
  if (typeof window[ns] !== 'undefined') {
2502
- console.log(`❌ @vbarbarosh/node-helpers@${"3.72.4"} was not injected — window.${ns} is already in use`);
2482
+ console.log(`❌ @vbarbarosh/node-helpers@${"3.73.0"} was not injected — window.${ns} is already in use`);
2503
2483
  }
2504
2484
  else {
2505
- console.log(`🎉 @vbarbarosh/node-helpers@${"3.72.4"} successfully exposed as window.${ns}`);
2485
+ console.log(`🎉 @vbarbarosh/node-helpers@${"3.73.0"} successfully exposed as window.${ns}`);
2506
2486
  window[ns] = {};
2507
2487
  // https://github.com/webpack/webpack/issues/625
2508
2488
  // https://webpack.js.org/guides/dependency-management/#require-context
2509
- const require_tmp = __webpack_require__("./src sync recursive \\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$");
2489
+ const require_tmp = __webpack_require__("./src sync recursive ^(?%21.*(cli-apps%7Cbrowser%7C\\.d)\\/).*\\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$");
2510
2490
  require_tmp.keys().forEach(function (key) {
2511
2491
  const [, basename] = key.match(/([^/]+)\.js$/);
2512
2492
  window[ns][basename] = require_tmp(key);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
3
3
  "license": "MIT",
4
4
  "name": "@vbarbarosh/node-helpers",
5
- "version": "3.72.4",
5
+ "version": "3.73.0",
6
6
  "description": "A set of helpers for JavaScript/Node.js",
7
7
  "unpkg": "dist/browser.js",
8
8
  "jsdelivr": "dist/browser.js",
@@ -0,0 +1,37 @@
1
+ const clamp = require('./clamp');
2
+
3
+ /**
4
+ * Split an array of jobs between up to `max_threads`, while trying to put at least
5
+ * `min_jobs_per_thread` items into each chunk.
6
+ *
7
+ * - If an array is empty → returns [].
8
+ * - If there are too few items to satisfy `min_items_per_chunk`, even for 1 chunk,
9
+ * everything goes into a single chunk.
10
+ *
11
+ * Alternative names:
12
+ * - array_chunk_balanced
13
+ *
14
+ * @param array
15
+ * @param max_threads
16
+ * @param min_jobs_per_thread
17
+ * @returns {*[]}
18
+ */
19
+ function array_chunk_jobs(array = [], max_threads = 1, min_jobs_per_thread = 1)
20
+ {
21
+ const total_threads = clamp(1, max_threads, Math.floor(array.length / min_jobs_per_thread));
22
+ const jobs_per_thread = Math.floor(array.length / total_threads);
23
+ let extra = array.length % total_threads;
24
+
25
+ const out = [];
26
+ for (let i = 0; i < array.length; ) {
27
+ const size = jobs_per_thread + (extra ? 1 : 0);
28
+ out.push(array.slice(i, i + size));
29
+ i += size;
30
+ if (extra) {
31
+ extra--;
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+
37
+ module.exports = array_chunk_jobs;
@@ -0,0 +1,37 @@
1
+ const array_chunk_jobs = require('./array_chunk_jobs');
2
+ const assert = require('assert');
3
+
4
+ describe('array_chunk_jobs', function () {
5
+ it('should handle basic input', function () {
6
+ assert.deepStrictEqual(array_chunk_jobs([1]), [[1]]);
7
+ assert.deepStrictEqual(array_chunk_jobs([1,2]), [[1,2]]);
8
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3]), [[1,2,3]]);
9
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5], 3, 2), [[1,2,3],[4,5]]);
10
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6], 3, 2), [[1,2],[3,4],[5,6]]);
11
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6,7], 3, 2), [[1,2,3],[4,5],[6,7]]);
12
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5], 3, 3), [[1,2,3,4,5]]);
13
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6], 3, 3), [[1,2,3],[4,5,6]]);
14
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6,7], 3, 3), [[1,2,3,4],[5,6,7]]);
15
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6,7,8], 3, 3), [[1,2,3,4],[5,6,7,8]]);
16
+ });
17
+ it('edge: If an array is empty → returns []', function () {
18
+ assert.deepStrictEqual(array_chunk_jobs([]), []);
19
+ });
20
+ it('edge: If there are too few items to satisfy `min_items_per_chunk`, even for 1 chunk, everything goes into a single chunk', function () {
21
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3], 5, 5), [[1,2,3]]);
22
+ });
23
+ it('should never produce more chunks than max_threads', function () {
24
+ // 12 jobs, max 5 threads, min 2 jobs per thread
25
+ // Correct result MUST have exactly 5 chunks
26
+ assert.deepStrictEqual(
27
+ array_chunk_jobs([1,2,3,4,5,6,7,8,9,10,11,12], 5, 2),
28
+ [
29
+ [1,2,3],
30
+ [4,5,6],
31
+ [7,8],
32
+ [9,10],
33
+ [11,12]
34
+ ]
35
+ );
36
+ });
37
+ });
@@ -0,0 +1,20 @@
1
+ const round = require('./round');
2
+
3
+ const EPS = 0.00001;
4
+
5
+ function array_fps(duration_sec, fps)
6
+ {
7
+ const total_frames = Math.floor(duration_sec * fps);
8
+
9
+ if (total_frames <= 1) {
10
+ return [0];
11
+ }
12
+
13
+ const out = [];
14
+ for (let i = 0; i < total_frames; ++i) {
15
+ out.push(round((i / (total_frames - 1)) * (duration_sec - EPS), EPS));
16
+ }
17
+ return out;
18
+ }
19
+
20
+ module.exports = array_fps;
@@ -0,0 +1,11 @@
1
+ const array_fps = require('./array_fps');
2
+ const assert = require('assert');
3
+
4
+ describe('array_fps', function () {
5
+ it('should handle basic input', function () {
6
+ assert.deepStrictEqual(array_fps(1, 1), [0]);
7
+ assert.deepStrictEqual(array_fps(1, 2), [0, 0.99999]);
8
+ assert.deepStrictEqual(array_fps(1, 3), [0, 0.49999, 0.99999]);
9
+ assert.deepStrictEqual(array_fps(1, 4), [0, 0.33333, 0.66666, 0.99999]);
10
+ });
11
+ });
@@ -7,7 +7,7 @@ else {
7
7
  window[ns] = {};
8
8
  // https://github.com/webpack/webpack/issues/625
9
9
  // https://webpack.js.org/guides/dependency-management/#require-context
10
- const require_tmp = require.context('..', true, /\b(array|date|http_delete|http_get_blob|http_get_buffer|http_get_json|http_get_utf8|http_head|http_patch_json|http_post_json|http_post_multipart|http_put_buffer|http_put_json|http_put_utf8|identity|ignore|fcmp|filter|format|plural|random_int|urlmod|waitcb)[^/]*(?<!\.test)\.js$/);
10
+ const require_tmp = require.context('..', true, /^(?!.*(cli-apps|browser|\.d)\/).*\b(array|date|http_delete|http_get_blob|http_get_buffer|http_get_json|http_get_utf8|http_head|http_patch_json|http_post_json|http_post_multipart|http_put_buffer|http_put_json|http_put_utf8|identity|ignore|fcmp|filter|format|plural|random_int|urlmod|waitcb)[^/]*(?<!\.test)\.js$/);
11
11
  require_tmp.keys().forEach(function (key) {
12
12
  const [, basename] = key.match(/([^/]+)\.js$/);
13
13
  window[ns][basename] = require_tmp(key);
package/src/clamp.js ADDED
@@ -0,0 +1,6 @@
1
+ function clamp(min, max, value)
2
+ {
3
+ return Math.max(min, Math.min(max, value));
4
+ }
5
+
6
+ module.exports = clamp;
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Promise = require('bluebird');
4
+ const cli = require('../../cli');
5
+ const ping_socket = require('../../ping_socket');
6
+
7
+ cli(main);
8
+
9
+ async function main()
10
+ {
11
+ for (let iter = 1, last = 2; iter <= last; ++iter) {
12
+ await ping_socket(process.env.WATCHDOG_SOCKET);
13
+ await Promise.delay(10);
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Promise = require('bluebird');
4
+ const cli = require('../../cli');
5
+ const ping_socket = require('../../ping_socket');
6
+
7
+ cli(main);
8
+
9
+ async function main()
10
+ {
11
+ await ping_socket(process.env.WATCHDOG_SOCKET);
12
+ while (true) {
13
+ await Promise.delay(1000);
14
+ }
15
+ }
@@ -15,6 +15,7 @@ const shell_spawn = require('../shell_spawn');
15
15
 
16
16
  const STARTED_AT = Date.now();
17
17
  const LOGS_ROOT_UID = cuid.createId();
18
+ const ARGS = process.argv.slice(2);
18
19
 
19
20
  let PING_COUNTER = 0;
20
21
 
@@ -35,18 +36,17 @@ cli(main, report);
35
36
 
36
37
  function report(error)
37
38
  {
38
- log(`[watchdog_end_error] ${render_stats()} | ${error.message}`);
39
+ log(`[watchdog_end_error] ${render_stats()} | ${error.message} | ${ARGS.join(' ')}`);
39
40
  }
40
41
 
41
42
  async function main()
42
43
  {
43
- const args = process.argv.slice(2);
44
- if (!args.length) {
44
+ if (!ARGS.length) {
45
45
  usage();
46
46
  process.exit(1);
47
47
  }
48
48
 
49
- switch (args[0]) {
49
+ switch (ARGS[0]) {
50
50
  case '-v':
51
51
  case '--version':
52
52
  version();
@@ -56,18 +56,20 @@ async function main()
56
56
 
57
57
  log(`[watchdog_begin] v${pkg.version}`);
58
58
 
59
- const WATCHDOG_INTERVAL = make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1000, default: 5000});
59
+ const WATCHDOG_INTERVAL = process.env.__TESTING__
60
+ ? make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1, default: 5000})
61
+ : make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1000, default: 5000});
60
62
  const heartbeat_server = new HeartbeatServer(WATCHDOG_INTERVAL);
61
63
  const WATCHDOG_SOCKET = heartbeat_server.socket_path;
62
64
  heartbeat_server.on('heartbeat', () => log(`[watchdog_ping] ❤️ ${render_stats(++PING_COUNTER)}`))
63
65
  heartbeat_server.on('warning', error => log(`[watchdog_warn] ⚠️ ${error}`))
64
66
 
65
67
  try {
66
- log(`[watchdog_args] ${args.join(' ')}`);
68
+ log(`[watchdog_args] ${ARGS.join(' ')}`);
67
69
  log(`[watchdog_interval] ${WATCHDOG_INTERVAL}`);
68
70
  log(`[watchdog_socket_path] ${heartbeat_server.socket_path}`);
69
71
  const env = {...process.env, WATCHDOG_INTERVAL, WATCHDOG_SOCKET};
70
- const proc = await shell_spawn(args, {stdio: 'inherit', env, detached: true}).init();
72
+ const proc = await shell_spawn(ARGS, {stdio: 'inherit', env, detached: true}).init();
71
73
  const pgid = proc.pid; // group id == leader pid
72
74
 
73
75
  const signal = new Promise(function (resolve, reject) {
@@ -98,7 +100,7 @@ async function main()
98
100
  await Promise.resolve(heartbeat_server.dispose()).timeout(1000);
99
101
  }
100
102
 
101
- log(`[watchdog_end_ok] ${render_stats()}`);
103
+ log(`[watchdog_end_ok] ${render_stats()} | ${ARGS.join(' ')}`);
102
104
  process.exit(0);
103
105
  }
104
106
 
@@ -106,10 +108,10 @@ function log(s)
106
108
  {
107
109
  const ss = JSON.stringify(s).slice(1, -1).replaceAll('\\"', '"');
108
110
  if (ss[0] === '[') {
109
- console.log(`[${LOGS_ROOT_UID}][+${format_ms3(Date.now() - STARTED_AT)}]${ss}`);
111
+ console.log(`[${LOGS_ROOT_UID}][${format_ms3(Date.now() - STARTED_AT, true)}]${ss}`);
110
112
  }
111
113
  else {
112
- console.log(`[${LOGS_ROOT_UID}][+${format_ms3(Date.now() - STARTED_AT)}] ${ss}`);
114
+ console.log(`[${LOGS_ROOT_UID}][${format_ms3(Date.now() - STARTED_AT, true)}] ${ss}`);
113
115
  }
114
116
  }
115
117
 
@@ -1,7 +1,48 @@
1
1
  const assert = require('assert');
2
+ const pid_exists = require('../pid_exists');
3
+ const shell_spawn = require('../shell_spawn');
4
+ const stream = require('stream');
5
+ const stream_each = require('../stream_each');
6
+ const stream_lines = require('../stream_lines');
2
7
 
3
8
  // watchdog true -> race condition; true exists too quickly; bug in shell_spawn
4
9
 
5
- describe.skip('watchdog', function () {
6
-
10
+ describe('watchdog', function () {
11
+ this.timeout(1000);
12
+ it('Happy Path', async function () {
13
+ const env = {...process.env, WATCHDOG_INTERVAL: 100, __TESTING__: 1};
14
+ const proc = shell_spawn([`${__dirname}/watchdog.js`, `${__dirname}/watchdog.d/quick-exit-after-2-pings.js`], {env});
15
+ try {
16
+ const stdout = [];
17
+ await Promise.all([
18
+ stream.promises.pipeline(proc.stdout, stream_lines(), stream_each(v => stdout.push(v))),
19
+ proc.promise(),
20
+ ]);
21
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_ping]')).length, 2);
22
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_end_ok] pings=2')).length, 1);
23
+ }
24
+ finally {
25
+ if (pid_exists(proc.pid)) {
26
+ process.kill(proc.pid, 'SIGTERM');
27
+ }
28
+ }
29
+ });
30
+ it('Hang after 1 ping', async function () {
31
+ const env = {...process.env, WATCHDOG_INTERVAL: 100, __TESTING__: 1};
32
+ const proc = shell_spawn([`${__dirname}/watchdog.js`, `${__dirname}/watchdog.d/quick-hang-after-1-ping.js`], {env});
33
+ const stdout = [];
34
+ try {
35
+ await Promise.all([
36
+ stream.promises.pipeline(proc.stdout, stream_lines(), stream_each(v => stdout.push(v))),
37
+ assert.rejects(proc.promise().timeout(500), {message: 'Process terminated with code 124 and signal null'}),
38
+ ]);
39
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_ping]')).length, 1);
40
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_end_error] pings=1 | No heartbeat for the last 100ms')).length, 1);
41
+ }
42
+ finally {
43
+ if (pid_exists(proc.pid)) {
44
+ process.kill(proc.pid, 'SIGTERM');
45
+ }
46
+ }
47
+ });
7
48
  });
package/src/format_ms3.js CHANGED
@@ -3,7 +3,7 @@ const factorize_ms = require('./factorize_ms');
3
3
  /**
4
4
  * Format milliseconds
5
5
  */
6
- function format_ms3(ms)
6
+ function format_ms3(ms, include_zero_hours = false)
7
7
  {
8
8
  if (!ms) {
9
9
  ms = 0;
@@ -11,7 +11,7 @@ function format_ms3(ms)
11
11
 
12
12
  // 00:05.445
13
13
  const [h, m, s, xx] = factorize_ms(ms);
14
- if (h) {
14
+ if (h || include_zero_hours) {
15
15
  return `0${h}:0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '');
16
16
  }
17
17
  return `0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '')
package/src/fs_lsr.js CHANGED
@@ -3,6 +3,9 @@ const fs_fi = require('./fs_fi');
3
3
  const fs_path_join = require('./fs_path_join');
4
4
  const fs_readdir = require('./fs_readdir');
5
5
 
6
+ /**
7
+ * @deprecated Deprecated in favor or fs_walk
8
+ */
6
9
  async function fs_lsr(pathname = '.')
7
10
  {
8
11
  const fi = await fs_fi(pathname);
package/src/fs_walk.js ADDED
@@ -0,0 +1,40 @@
1
+ const fs_lstat = require('./fs_lstat');
2
+ const fs_path_join = require('./fs_path_join');
3
+ const fs_readdir = require('./fs_readdir');
4
+ const ignore = require('./ignore');
5
+
6
+ async function fs_walk({path = '.', user_friendly_status = ignore, error_handler = error_handler, signal})
7
+ {
8
+ const out = [];
9
+ const queue = [path];
10
+ while (queue.length) {
11
+ if (signal) {
12
+ signal.throwIfAborted();
13
+ }
14
+ try {
15
+ const current_path = queue.pop();
16
+ user_friendly_status(`Reading ${current_path}`);
17
+ const lstat = await fs_lstat(current_path);
18
+ lstat.path = current_path;
19
+ out.push(lstat);
20
+ if (lstat.isDirectory()) {
21
+ if (current_path === '/dev' || current_path === '/proc') {
22
+ continue;
23
+ }
24
+ const basenames = await fs_readdir(current_path);
25
+ basenames.forEach(v => queue.push(fs_path_join(current_path, v)));
26
+ }
27
+ }
28
+ catch (error) {
29
+ error_handler(error);
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function error_handler(error)
36
+ {
37
+ console.log(`⚠️ ${error.message}`);
38
+ }
39
+
40
+ module.exports = fs_walk;
package/src/round.js ADDED
@@ -0,0 +1,101 @@
1
+ function round(value, precision = 1)
2
+ {
3
+ if (precision === 0) {
4
+ return value;
5
+ }
6
+
7
+ const factor = 1 / precision;
8
+ const sign = Math.sign(value);
9
+
10
+ // Count decimal places for the cleanup (toFixed)
11
+ const exponent = get_decimal_places(precision);
12
+
13
+ // Small epsilon helps fix most floating point edge cases involving ties
14
+ const epsilon = 1e-12*sign;
15
+
16
+ // Scale → Nudge → Round → Unscale
17
+ const final = (Math.round(value*factor + epsilon) / factor);
18
+
19
+ // Cleanup trailing garbage such as: 34.038000000000004 → 34.038
20
+ return Number(final.toFixed(exponent));
21
+ }
22
+
23
+ function get_decimal_places(precision)
24
+ {
25
+ if (Math.floor(precision) === precision) {
26
+ return 0;
27
+ }
28
+
29
+ // Scientific notation support: e.g., 1e-20
30
+ const e = precision.toExponential().split('e');
31
+ if (e.length > 1) {
32
+ return Math.abs(parseInt(e[1], 10));
33
+ }
34
+
35
+ // Normal decimal notation (e.g., 0.001)
36
+ return precision.toString().split('.')[1]?.length || 0;
37
+ }
38
+
39
+ // // round(34.037531, 0.001) -> 34.038000000000004
40
+ // function round(value, precision = 1)
41
+ // {
42
+ // return Math.round(value / precision) * precision;
43
+ // }
44
+
45
+ // // round(1.005, 0.01) -> 1
46
+ // function round(value, precision = 1)
47
+ // {
48
+ // const p = 1 / precision;
49
+ // return Math.round(value * p) / p;
50
+ // }
51
+
52
+ // function round(value, precision = 1)
53
+ // {
54
+ // if (precision === 0) {
55
+ // return value;
56
+ // }
57
+ // const factor = 1 / precision;
58
+ // const epsilon = 1e-12 * Math.sign(value);
59
+ // return Number((Math.round((value * factor) + epsilon) / factor).toPrecision(15));
60
+ // }
61
+
62
+ // function round(value, precision = 1)
63
+ // {
64
+ // if (precision === 0) {
65
+ // return value;
66
+ // }
67
+ //
68
+ // const factor = 1 / precision;
69
+ //
70
+ // // The exponent is critical for defining the precision of the cleanup
71
+ // const exponent = Math.max(0, String(precision).length - 2);
72
+ //
73
+ // // 1. Calculate the scaled value.
74
+ // let scaled = value * factor;
75
+ //
76
+ // // 2. Strategic Nudge: Add a tiny, signed epsilon (1e-12) to correct floating-point
77
+ // // representation errors and force symmetric rounding (as Math.abs failed).
78
+ // const epsilon = 1e-12 * Math.sign(value);
79
+ //
80
+ // // 3. Round the nudged value.
81
+ // const roundedScaled = Math.round(scaled + epsilon);
82
+ //
83
+ // // 4. Unscale the value.
84
+ // const result = roundedScaled / factor;
85
+ //
86
+ // // 5. CRITICAL FINAL STEP: Use .toFixed() with the required decimal places for cleanup.
87
+ // // This stabilizes the result against the strict equality checks, including
88
+ // // the safe integer boundary errors caused by the final division.
89
+ // return Number(result.toFixed(exponent));
90
+ // }
91
+
92
+ // 💎 The most precise solution, but - extra dependency.
93
+ //
94
+ // const Decimal = require('decimal.js');
95
+ //
96
+ // function round(value, precision = 1)
97
+ // {
98
+ // return new Decimal(value).div(precision).round().times(precision).toNumber();
99
+ // }
100
+
101
+ module.exports = round;
@@ -0,0 +1,56 @@
1
+ const assert = require('assert');
2
+ const round = require('./round');
3
+
4
+ const items = [
5
+ [34, 25, 25],
6
+ [37.5, 25, 50],
7
+ [34.037531, 0.001, 34.038],
8
+ [1.005, 0.01, 1.01],
9
+ [0.99999, 0.00001, 0.99999],
10
+
11
+ // Synthetic cases generated by DeepSeek
12
+ [1.005, 0.001, 1.005], // More precise rounding
13
+ [1.005, 0.1, 1.0], // Less precise rounding
14
+ [1.015, 0.01, 1.02], // Another edge case with 5
15
+ [1.025, 0.01, 1.03], // Yet another edge case
16
+ [0.5, 1, 1], // Round to nearest integer
17
+ [0.4, 1, 0], // Round down to integer
18
+ [123.456, 0.01, 123.46], // Standard decimal rounding
19
+ [123.456, 0.1, 123.5], // Different precision
20
+ [123.456, 1, 123], // Round to whole number
21
+ [123.456, 10, 120], // Round to tens
22
+ [123.456, 100, 100], // Round to hundreds
23
+ [0, 0.01, 0], // Zero value
24
+ [-1.005, 0.01, -1.01], // Negative numbers
25
+ [-1.015, 0.01, -1.02], // Negative edge case
26
+ [-37.5, 25, -50], // Negative with integer precision
27
+ [1.2345e-5, 1e-6, 0.000012], // Very small numbers
28
+ [999.999, 0.001, 999.999], // Rounding that causes carry-over
29
+ [0.0000000000001, 0.01, 0], // Extremely small number
30
+ [Number.MAX_SAFE_INTEGER, 1000, 9007199254741000], // Large numbers
31
+
32
+ [0.005, 0.01, 0.01],
33
+ [-1.5, 1, -2], // Symmetric negative rounding (Math.abs is critical here)
34
+ [-0.5, 1, -1], // Symmetric negative rounding for half
35
+ [123456789123.45, 1000, 123456789000], // Testing very large numbers
36
+ [1.2345e-20, 1e-20, 1e-20], // Testing very small numbers
37
+ [1.0000000000000001, 0.0000000000000001, 1.0000000000000000], // Testing rounding near Number.EPSILON range
38
+
39
+ [9007199254740992, 1, 9007199254740992],
40
+ [9007199254740991.5, 1, 9007199254740992],
41
+ [Number.MIN_SAFE_INTEGER, 1, Number.MIN_SAFE_INTEGER],
42
+ [Number.MAX_SAFE_INTEGER, 1, Number.MAX_SAFE_INTEGER],
43
+
44
+ // Out of scope of standard JS
45
+ // [0.004999999999999999, 0.01, 0.0],
46
+ // [Number.MIN_SAFE_INTEGER, 0.1, Number.MIN_SAFE_INTEGER],
47
+ // [Number.MAX_SAFE_INTEGER, 0.1, Number.MAX_SAFE_INTEGER],
48
+ ];
49
+
50
+ describe('round', function () {
51
+ items.forEach(function ([value, precision, expected],) {
52
+ it(`${value} [${precision}] → ${expected}`, function () {
53
+ assert.deepStrictEqual(round(value, precision), expected);
54
+ });
55
+ });
56
+ });
package/src/shell.md ADDED
@@ -0,0 +1,16 @@
1
+ Basic helper for running external commands via `execFile`.
2
+
3
+ - Fails if the command exits with non-zero code.
4
+ - Fails if anything is printed to `stderr`.
5
+ - Resolves with `stdout` (string), exactly as produced.
6
+
7
+ ## Signature
8
+
9
+ ```
10
+ shell(args, options) → Promise<string>
11
+ ```
12
+
13
+ ## Notes
14
+
15
+ - `args[0]` is the executable; `args[1..]` are arguments.
16
+ - Suitable for commands that produce clean stdout and no warnings.
@@ -0,0 +1,12 @@
1
+ Basic helper for running external commands via `execFile`, and parse result by JSON.parse.
2
+
3
+ - Fails if the command exits with non-zero code.
4
+ - Fails if anything is printed to `stderr`.
5
+ - Fails if stdout is not valid JSON.
6
+ - Resolves with parsed JSON value.
7
+
8
+ ## Signature
9
+
10
+ ```
11
+ shell_json(args, options) → Promise<any>
12
+ ```
@@ -0,0 +1,12 @@
1
+ Basic helper for running external commands via `execFile`, and splits stdout into lines.
2
+
3
+ - Fails if the command exits with non-zero code.
4
+ - Fails if anything is printed to `stderr`.
5
+ - Strips trailing newline(s).
6
+ - Returns array of lines.
7
+
8
+ ## Signature
9
+
10
+ ```
11
+ shell_lines(args, options) → Promise<string[]>
12
+ ```
@@ -1,6 +1,7 @@
1
1
  const ExitCodeError = require('./errors/ExitCodeError');
2
2
  const Promise = require('bluebird');
3
3
  const child_process = require('child_process');
4
+ const ignore = require('./ignore');
4
5
 
5
6
  // 📕 Notes by ChatGPT:
6
7
  // If the child is killed by a signal (e.g. SIGKILL from outside),
@@ -20,45 +21,44 @@ const child_process = require('child_process');
20
21
  // > }
21
22
  // > }
22
23
 
23
- // ⚠️ Both .init and .promise are targets for race conditions!
24
24
  function shell_spawn(args, options)
25
25
  {
26
- let init, promise;
26
+ const init = {};
27
+ const run = {};
28
+
29
+ init.promise = new Promise(function (resolve, reject) {
30
+ init.resolve = resolve;
31
+ init.reject = reject;
32
+ });
33
+ run.promise = new Promise(function (resolve, reject) {
34
+ run.resolve = resolve;
35
+ run.reject = reject;
36
+ });
37
+
27
38
  const out = child_process.spawn(args[0], args.slice(1), options);
28
- out.init = function () {
29
- return init = init || new Promise(function (resolve, reject) {
30
- out.once('error', init_error);
31
- out.once('spawn', init_spawn);
32
- function init_error(error) {
33
- out.off('spawn', init_spawn);
34
- reject(error);
35
- }
36
- function init_spawn() {
37
- out.off('error', init_error);
38
- resolve(out);
39
- }
40
- });
41
- };
42
- out.promise = function () {
43
- return promise = promise || new Promise(function (resolve, reject) {
44
- out.once('error', promise_error);
45
- out.once('exit', promise_exit);
46
- function promise_error(error) {
47
- out.off('exit', promise_exit);
48
- reject(error);
49
- }
50
- function promise_exit(code, signal) {
51
- out.off('error', promise_error);
52
- if (code === 0) {
53
- resolve();
54
- }
55
- else {
56
- reject(new ExitCodeError(code || 128, `Process terminated with code ${code} and signal ${signal}`));
57
- }
58
- }
59
- });
60
- };
39
+ out.once('error', onerror);
40
+ out.once('spawn', onspawn);
41
+ out.once('exit', onexit);
42
+ out.init = () => init.promise;
43
+ out.promise = () => run.promise;
61
44
  return out;
45
+
46
+ function onerror(error) {
47
+ init.reject(error);
48
+ run.reject(error);
49
+ }
50
+ function onexit(code, signal) {
51
+ if (code === 0) {
52
+ run.resolve();
53
+ }
54
+ else {
55
+ run.reject(new ExitCodeError(code || 128, `Process terminated with code ${code} and signal ${signal}`));
56
+ }
57
+ }
58
+ function onspawn() {
59
+ init.resolve(out);
60
+ init.reject = ignore;
61
+ }
62
62
  }
63
63
 
64
64
  module.exports = shell_spawn;
@@ -0,0 +1,25 @@
1
+ Spawn-based helper for long-running or streaming commands.
2
+
3
+ Provides two promises:
4
+
5
+ - `proc.init()` — resolves when the child emits `spawn`.
6
+ - `proc.promise()` — resolves when the child exits with code 0.
7
+
8
+ ## Failure semantics
9
+
10
+ - Rejects if `error` event occurs.
11
+ - Rejects if exit code is non-zero.
12
+ - If child dies by signal, treated as non-zero exit (uses exitCode = 128).
13
+
14
+ ## Signature
15
+
16
+ ```
17
+ const proc = shell_spawn(args, options)
18
+ proc.init() → Promise<ChildProcess>
19
+ proc.promise() → Promise<void>
20
+ ```
21
+
22
+ ## Notes
23
+
24
+ - Output is streamed (not buffered).
25
+ - Use when you need live stdout/stderr or long-running jobs.
@@ -3,7 +3,7 @@ const assert = require('assert');
3
3
  const pid_exists = require('./pid_exists');
4
4
  const shell_spawn = require('./shell_spawn');
5
5
 
6
- describe.skip('shell_spawn', function () {
6
+ describe('shell_spawn', function () {
7
7
  describe('Happy Path', function () {
8
8
  it('basic', async function () {
9
9
  await shell_spawn(['true']).promise().timeout(100);
@@ -20,4 +20,11 @@ describe.skip('shell_spawn', function () {
20
20
  assert(!pid_exists(proc.pid));
21
21
  await proc.promise().timeout(100);
22
22
  });
23
+ it('should throw when init fails', async function () {
24
+ await assert.rejects(shell_spawn(['ggg111']).init(), /^Error: spawn ggg111 ENOENT$/);
25
+ });
26
+ it('should throw when process exists with non zero code', async function () {
27
+ const proc = await shell_spawn(['false']).init();
28
+ await assert.rejects(proc.promise().timeout(100), /^Error: Process terminated with code 1 and signal null$/);
29
+ });
23
30
  });
@@ -0,0 +1,11 @@
1
+ Runs a command via `execFile` and returns both stdout and stderr.
2
+
3
+ - Rejects only on exec error.
4
+ - Resolves with `{stdout, stderr}`.
5
+ - Does NOT treat stderr output as failure.
6
+
7
+ ## Signature
8
+
9
+ ```
10
+ shell_stdall(args, options) → Promise<{stdout: string, stderr: string}>
11
+ ```
@@ -0,0 +1,10 @@
1
+ Convenience wrapper over `shell_spawn` with `stdio: 'inherit'`.
2
+
3
+ - Child stdout/stderr are streamed directly to parent terminal.
4
+ - Supports `proc.init()` and `proc.promise()` like `shell_spawn`.
5
+
6
+ ## Signature
7
+
8
+ ```
9
+ shell_thru(args, options) → ChildProcess
10
+ ```