@vbarbarosh/node-helpers 3.75.0 → 3.76.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 (369) hide show
  1. package/README.md +29 -3
  2. package/dist/browser.js +1 -2504
  3. package/dist/index.html +1 -1
  4. package/package.json +33 -12
  5. package/src/HeartbeatServer.js +24 -3
  6. package/src/HeartbeatServer.md +33 -0
  7. package/src/array_chunk.js +1 -1
  8. package/src/array_chunk.md +8 -0
  9. package/src/array_chunk_balanced.md +14 -0
  10. package/src/array_fps.md +12 -0
  11. package/src/array_gcd.js +4 -2
  12. package/src/array_gcd.md +2 -1
  13. package/src/array_group.js +3 -3
  14. package/src/array_group.md +14 -0
  15. package/src/array_group_map.js +3 -3
  16. package/src/array_group_map.md +13 -0
  17. package/src/array_index.js +2 -2
  18. package/src/array_index.md +9 -0
  19. package/src/array_lcm.js +4 -2
  20. package/src/array_lcm.md +2 -1
  21. package/src/array_max.js +11 -4
  22. package/src/array_max.md +1 -1
  23. package/src/array_min.js +11 -4
  24. package/src/array_min.md +1 -1
  25. package/src/array_permutations.js +4 -0
  26. package/src/array_permutations.md +13 -0
  27. package/src/array_sort.js +7 -2
  28. package/src/array_sort.md +13 -0
  29. package/src/array_sort_other.js +8 -5
  30. package/src/array_sort_other.md +11 -0
  31. package/src/array_sum.js +2 -2
  32. package/src/array_sum.md +8 -0
  33. package/src/array_unique.js +2 -2
  34. package/src/array_unique_last.js +2 -2
  35. package/src/assert_valid_expires_at.md +10 -0
  36. package/src/clamp.md +9 -0
  37. package/src/cli-apps/watchdog.js +5 -0
  38. package/src/cli.js +3 -1
  39. package/src/cli.md +15 -0
  40. package/src/const_stream.md +7 -0
  41. package/src/const_type.md +5 -0
  42. package/src/countdown.js +22 -5
  43. package/src/countdown.md +21 -0
  44. package/src/crypto_decrypt_aes256.md +13 -0
  45. package/src/crypto_encrypt_aes256.md +13 -0
  46. package/src/crypto_hash_md5.md +10 -0
  47. package/src/crypto_hash_sha256.md +10 -0
  48. package/src/crypto_hmac_sha256.md +11 -0
  49. package/src/date_add_hours.md +8 -0
  50. package/src/date_add_milliseconds.md +4 -0
  51. package/src/date_add_minutes.md +4 -0
  52. package/src/date_add_months.md +13 -0
  53. package/src/date_add_seconds.md +4 -0
  54. package/src/date_diff_seconds.md +4 -0
  55. package/src/date_is_leap_year.md +9 -0
  56. package/src/escape_content_disposition.js +6 -3
  57. package/src/escape_content_disposition.md +13 -0
  58. package/src/escape_regexp.md +7 -0
  59. package/src/eta.js +7 -1
  60. package/src/eta.md +18 -0
  61. package/src/factorize_ms.js +3 -0
  62. package/src/factorize_ms.md +10 -0
  63. package/src/fastdl.js +14 -3
  64. package/src/fcmp_dates.md +12 -0
  65. package/src/fcmp_default.js +7 -0
  66. package/src/fcmp_default.md +29 -0
  67. package/src/fcmp_default_desc.js +4 -7
  68. package/src/fcmp_default_desc.md +10 -0
  69. package/src/fcmp_from_spec.md +13 -0
  70. package/src/fcmp_numbers.md +11 -0
  71. package/src/fcmp_tuples.md +17 -0
  72. package/src/fcmp_utf8_bin.md +14 -0
  73. package/src/fcmp_utf8_ci.md +14 -0
  74. package/src/fcmp_utf8_cs.md +15 -0
  75. package/src/fcmp_utf8_natural_bin.js +38 -0
  76. package/src/fcmp_utf8_natural_bin.md +8 -0
  77. package/src/fcmp_utf8_natural_ci.md +13 -0
  78. package/src/fcmp_utf8_natural_cs.md +13 -0
  79. package/src/fcmpx.md +26 -9
  80. package/src/filter1_from_spec.js +3 -3
  81. package/src/format_bytes.md +15 -0
  82. package/src/format_date.md +7 -0
  83. package/src/format_date_fs.md +7 -0
  84. package/src/format_date_human.md +6 -0
  85. package/src/format_date_ymd.md +6 -0
  86. package/src/format_error_report.js +3 -3
  87. package/src/format_error_report.md +12 -0
  88. package/src/format_kilo.md +13 -0
  89. package/src/format_ms.js +3 -2
  90. package/src/format_ms.md +12 -0
  91. package/src/format_ms2.js +6 -3
  92. package/src/format_ms2.md +12 -0
  93. package/src/format_ms3.js +3 -2
  94. package/src/format_ms3.md +12 -0
  95. package/src/format_percents.md +10 -0
  96. package/src/format_progress_bytes.md +13 -0
  97. package/src/format_progress_kilo.md +10 -0
  98. package/src/format_seconds.md +11 -0
  99. package/src/format_thousands.md +9 -0
  100. package/src/fs_append.md +7 -0
  101. package/src/fs_append_json.md +8 -0
  102. package/src/fs_append_utf8.md +6 -0
  103. package/src/fs_assert_file_exists.md +3 -0
  104. package/src/fs_assert_file_readable.js +1 -1
  105. package/src/fs_assert_file_readable.md +3 -0
  106. package/src/fs_assert_file_socket.md +2 -0
  107. package/src/fs_copy.md +7 -0
  108. package/src/fs_copy_excl.md +2 -0
  109. package/src/fs_exists.md +6 -0
  110. package/src/fs_fclose.md +5 -0
  111. package/src/fs_fi.js +13 -1
  112. package/src/fs_fi.md +11 -0
  113. package/src/fs_file_md5.md +8 -0
  114. package/src/fs_find.md +7 -0
  115. package/src/fs_fopen.md +6 -0
  116. package/src/fs_fread.js +2 -2
  117. package/src/fs_fread.md +11 -0
  118. package/src/fs_fsize.md +2 -0
  119. package/src/fs_fstat.md +2 -0
  120. package/src/fs_fwrite.js +2 -2
  121. package/src/fs_fwrite.md +9 -0
  122. package/src/fs_list.md +5 -0
  123. package/src/fs_list_deep.md +5 -0
  124. package/src/fs_ls.md +2 -0
  125. package/src/fs_lsr.md +4 -0
  126. package/src/fs_lstat.md +3 -0
  127. package/src/fs_mkdir.md +3 -0
  128. package/src/fs_mkdirp.js +3 -30
  129. package/src/fs_mkdirp.md +7 -0
  130. package/src/fs_path_basename.md +2 -0
  131. package/src/fs_path_dirname.md +2 -0
  132. package/src/fs_path_extname.md +9 -0
  133. package/src/fs_path_join.md +2 -0
  134. package/src/fs_path_relative.md +2 -0
  135. package/src/fs_path_resolve.md +2 -0
  136. package/src/fs_path_tempdir.md +2 -0
  137. package/src/fs_path_untildify.md +8 -0
  138. package/src/fs_read.md +3 -0
  139. package/src/fs_read_buffer.md +5 -0
  140. package/src/fs_read_json.md +6 -0
  141. package/src/fs_read_lines.md +7 -0
  142. package/src/fs_read_stream.md +2 -0
  143. package/src/fs_read_utf8.md +5 -0
  144. package/src/fs_readdir.md +3 -0
  145. package/src/fs_readlink.md +6 -0
  146. package/src/fs_rename.md +6 -0
  147. package/src/fs_rm.md +4 -0
  148. package/src/fs_rmdir.md +3 -0
  149. package/src/fs_rmf.md +2 -0
  150. package/src/fs_rmrf.md +8 -0
  151. package/src/fs_sha256.js +16 -0
  152. package/src/fs_sha256.md +9 -0
  153. package/src/fs_size.js +2 -2
  154. package/src/fs_size.md +6 -0
  155. package/src/fs_stat.md +2 -0
  156. package/src/fs_tempdir.js +20 -5
  157. package/src/fs_touch.md +6 -0
  158. package/src/fs_walk.js +2 -2
  159. package/src/fs_walk.md +14 -0
  160. package/src/fs_write.md +3 -0
  161. package/src/fs_write_json.md +7 -0
  162. package/src/fs_write_stream.md +2 -0
  163. package/src/gettype.md +13 -0
  164. package/src/http_delete.md +11 -0
  165. package/src/http_get_blob.md +8 -0
  166. package/src/http_get_buffer.md +10 -0
  167. package/src/http_get_file.js +8 -8
  168. package/src/http_get_file.md +14 -0
  169. package/src/http_get_json.md +10 -0
  170. package/src/http_get_stream.md +14 -0
  171. package/src/http_get_stream_range.js +27 -9
  172. package/src/http_get_stream_range.md +22 -0
  173. package/src/http_get_utf8.md +9 -0
  174. package/src/http_head.md +9 -0
  175. package/src/http_patch_json.md +10 -0
  176. package/src/http_post_json.md +11 -0
  177. package/src/http_post_multipart.md +15 -0
  178. package/src/http_post_urlencoded.md +11 -0
  179. package/src/http_put_buffer.md +9 -0
  180. package/src/http_put_file.js +9 -6
  181. package/src/http_put_file.md +16 -0
  182. package/src/http_put_json.md +11 -0
  183. package/src/http_put_utf8.md +9 -0
  184. package/src/http_range_parse.js +6 -3
  185. package/src/http_range_parse.md +18 -0
  186. package/src/http_stream_range.js +35 -22
  187. package/src/http_stream_range.md +29 -0
  188. package/src/identity.md +2 -0
  189. package/src/ignore.md +2 -0
  190. package/src/json_stringify_safe.md +10 -0
  191. package/src/json_stringify_stable.md +9 -0
  192. package/src/logger.js +2 -3
  193. package/src/logger.md +15 -0
  194. package/src/make_progress.js +8 -5
  195. package/src/make_progress.md +20 -0
  196. package/src/math_gcd.js +2 -1
  197. package/src/mongo_stream_upsert.js +10 -3
  198. package/src/mongo_stream_upsert.md +17 -0
  199. package/src/mongo_stream_write.js +10 -3
  200. package/src/mongo_stream_write.md +16 -0
  201. package/src/msval.md +10 -0
  202. package/src/now_fs.md +7 -0
  203. package/src/now_human.md +6 -0
  204. package/src/object_defaults.md +11 -0
  205. package/src/object_schema.js +2 -1
  206. package/src/object_schema.md +12 -0
  207. package/src/object_walk_preorder.md +16 -0
  208. package/src/parallel.js +23 -10
  209. package/src/parse_bytes.js +4 -2
  210. package/src/parse_bytes.md +15 -0
  211. package/src/parse_hms.js +1 -1
  212. package/src/parse_http_content_range.js +9 -5
  213. package/src/parse_http_content_range.md +16 -0
  214. package/src/perf_end_human.md +10 -0
  215. package/src/perf_end_ms.md +8 -0
  216. package/src/perf_measure.md +10 -0
  217. package/src/perf_measure_human.md +8 -0
  218. package/src/perf_start.md +9 -0
  219. package/src/pgid_exists.md +6 -0
  220. package/src/pgid_kill_grace.js +1 -1
  221. package/src/pgid_kill_grace.md +15 -0
  222. package/src/pid_exists.md +5 -0
  223. package/src/pid_kill_grace.js +1 -1
  224. package/src/pid_kill_grace.md +14 -0
  225. package/src/plural.md +13 -0
  226. package/src/random_hex.md +9 -0
  227. package/src/random_int.md +8 -0
  228. package/src/range.js +4 -2
  229. package/src/range.md +2 -0
  230. package/src/redis_connect.js +1 -1
  231. package/src/redis_connect.md +10 -0
  232. package/src/redis_lshift.md +14 -0
  233. package/src/redis_poll.js +1 -0
  234. package/src/redis_poll.md +17 -0
  235. package/src/redis_poll_zshift_callback_rpush.js +1 -2
  236. package/src/redis_poll_zshift_callback_rpush.md +17 -0
  237. package/src/redis_poll_zshift_spawn_rpush.js +2 -1
  238. package/src/redis_poll_zshift_spawn_rpush.md +20 -0
  239. package/src/redis_zshift.md +14 -0
  240. package/src/round.js +7 -8
  241. package/src/round.md +15 -0
  242. package/src/sanitize_dash_name.md +13 -0
  243. package/src/sanitize_filename.md +9 -0
  244. package/src/sanitize_var_name.md +14 -0
  245. package/src/sftp_get_file_info.js +2 -2
  246. package/src/sftp_get_file_info.md +14 -0
  247. package/src/sftp_get_stream_range.js +32 -22
  248. package/src/sftp_get_stream_range.md +20 -0
  249. package/src/shell_curl_progress.js +2 -1
  250. package/src/shell_curl_progress.md +17 -0
  251. package/src/shell_lines.js +2 -2
  252. package/src/shell_lines.md +1 -1
  253. package/src/shell_spawn.js +7 -0
  254. package/src/shell_ytdlp_progress.js +2 -1
  255. package/src/shell_ytdlp_progress.md +17 -0
  256. package/src/str_camel_to_snake.md +11 -0
  257. package/src/str_parse_kv.md +11 -0
  258. package/src/stream_curl_progress.md +14 -0
  259. package/src/stream_data_ln.js +7 -1
  260. package/src/stream_discard.md +8 -0
  261. package/src/stream_group.md +3 -0
  262. package/src/stream_gunzip.md +7 -0
  263. package/src/stream_hash.md +10 -0
  264. package/src/stream_map.js +8 -1
  265. package/src/stream_map_parallel.js +9 -1
  266. package/src/stream_md5.md +9 -0
  267. package/src/stream_multiplex.js +11 -0
  268. package/src/stream_multiplex.md +16 -0
  269. package/src/stream_through.md +3 -0
  270. package/src/stream_transform.md +2 -0
  271. package/src/stream_xml_analyze.js +9 -1
  272. package/src/stream_xml_parse.js +9 -1
  273. package/src/stream_ytdlp_progress.js +1 -1
  274. package/src/stream_ytdlp_progress.md +18 -0
  275. package/src/strtr.js +15 -5
  276. package/src/strtr.md +11 -0
  277. package/src/urlclean.js +9 -2
  278. package/src/urlmod.js +22 -11
  279. package/src/urlmod.md +15 -3
  280. package/src/urlparts.md +17 -0
  281. package/src/wait_while.js +1 -1
  282. package/src/wait_while.md +1 -1
  283. package/src/HeartbeatServer.test.js +0 -31
  284. package/src/array_chunk.test.js +0 -24
  285. package/src/array_chunk_balanced.test.js +0 -37
  286. package/src/array_fps.test.js +0 -72
  287. package/src/array_gcd.test.js +0 -10
  288. package/src/array_index.test.js +0 -14
  289. package/src/array_lcm.test.js +0 -12
  290. package/src/array_max.test.js +0 -20
  291. package/src/array_min.test.js +0 -20
  292. package/src/array_permutations.test.js +0 -20
  293. package/src/array_sort.test.js +0 -85
  294. package/src/array_sort_other.test.js +0 -9
  295. package/src/array_sum.test.js +0 -11
  296. package/src/cli-apps/watchdog.d/connect_close.js +0 -17
  297. package/src/cli-apps/watchdog.d/connect_close2.js +0 -20
  298. package/src/cli-apps/watchdog.d/connect_close3.js +0 -25
  299. package/src/cli-apps/watchdog.d/connect_no_ping.js +0 -13
  300. package/src/cli-apps/watchdog.d/early_exit.js +0 -7
  301. package/src/cli-apps/watchdog.d/flood.js +0 -13
  302. package/src/cli-apps/watchdog.d/flood2.js +0 -15
  303. package/src/cli-apps/watchdog.d/hang.js +0 -8
  304. package/src/cli-apps/watchdog.d/huge_delay.js +0 -18
  305. package/src/cli-apps/watchdog.d/ignore_sigterm.js +0 -29
  306. package/src/cli-apps/watchdog.d/late_heartbeat.js +0 -21
  307. package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +0 -16
  308. package/src/cli-apps/watchdog.d/my-happy-path.js +0 -16
  309. package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +0 -22
  310. package/src/cli-apps/watchdog.d/my-never-ping.js +0 -15
  311. package/src/cli-apps/watchdog.d/no_heartbeat.js +0 -5
  312. package/src/cli-apps/watchdog.d/ok.js +0 -17
  313. package/src/cli-apps/watchdog.d/quick-exit-after-2-pings.js +0 -15
  314. package/src/cli-apps/watchdog.d/quick-hang-after-1-ping.js +0 -15
  315. package/src/cli-apps/watchdog.d/spawn_children.js +0 -14
  316. package/src/cli-apps/watchdog.test.js +0 -48
  317. package/src/crypto_encrypt_aes256.test.js +0 -10
  318. package/src/crypto_hash_sha256.test.js +0 -10
  319. package/src/date_add_months.test.js +0 -40
  320. package/src/date_is_leap_year.test.js +0 -14
  321. package/src/escape_content_disposition.test.js +0 -22
  322. package/src/fcmp_from_spec.test.js +0 -91
  323. package/src/fcmpx.test.js +0 -173
  324. package/src/filter1_from_spec.test.js +0 -31
  325. package/src/format_bytes.test.js +0 -22
  326. package/src/format_error_report.d/src/1.js +0 -26
  327. package/src/format_error_report.d/src/2.js +0 -29
  328. package/src/format_kilo.test.js +0 -25
  329. package/src/format_ms.test.js +0 -33
  330. package/src/format_ms2.test.js +0 -33
  331. package/src/format_ms3.test.js +0 -33
  332. package/src/format_percents.test.js +0 -22
  333. package/src/format_progress_bytes.test.js +0 -19
  334. package/src/format_progress_kilo.test.js +0 -19
  335. package/src/format_seconds.test.js +0 -42
  336. package/src/format_thousands.test.js +0 -28
  337. package/src/fs_file_md5.test.js +0 -11
  338. package/src/fs_mkdirp.test.js +0 -24
  339. package/src/fs_path_extname.test.js +0 -22
  340. package/src/gettype.test.js +0 -27
  341. package/src/object_schema.test.js +0 -15
  342. package/src/object_walk_preorder.test.js +0 -17
  343. package/src/parallel.test.js +0 -11
  344. package/src/parse_bytes.test.js +0 -21
  345. package/src/parse_hms.test.js +0 -32
  346. package/src/parse_http_content_range.test.js +0 -14
  347. package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +0 -33
  348. package/src/pid_kill_grace.d/ignore-sigterm.js +0 -22
  349. package/src/pid_kill_grace.test.js +0 -63
  350. package/src/ping_socket.test.js +0 -58
  351. package/src/plural.test.js +0 -20
  352. package/src/range.test.js +0 -41
  353. package/src/redis_lshift.test.js +0 -57
  354. package/src/redis_zshift.test.js +0 -58
  355. package/src/round.test.js +0 -56
  356. package/src/sanitize_dash_name.test.js +0 -59
  357. package/src/sanitize_var_name.test.js +0 -39
  358. package/src/shell_spawn.test.js +0 -30
  359. package/src/str_camel_to_snake.test.js +0 -17
  360. package/src/stream_curl_progress.d/basic +0 -3
  361. package/src/stream_curl_progress.d/basic.json +0 -42
  362. package/src/stream_curl_progress.test.js +0 -17
  363. package/src/stream_data_ln.test.js +0 -24
  364. package/src/stream_map_parallel.test.js +0 -26
  365. package/src/stream_multiplex.test.js +0 -67
  366. package/src/stream_strpbrk.test.js +0 -29
  367. package/src/urlclean.test.js +0 -18
  368. package/src/urlmod.test.js +0 -40
  369. package/src/urlparts.test.js +0 -294
@@ -0,0 +1,15 @@
1
+ Parses a human-readable size string into a number of bytes. Units are
2
+ 1024-based: `B`, `KB`, `MB`, `GB`, `TB`, `PB`; the `KiB`/`MiB` spellings are
3
+ accepted and mean the same thing. Matching is case-insensitive and anchored:
4
+ whitespace around and between number and unit is fine, but any other
5
+ trailing/leading garbage gives `NaN`. A bare number means bytes. The result
6
+ is `Math.floor`ed. Non-strings (including numbers) return `NaN`.
7
+
8
+ ```js
9
+ parse_bytes('10.5MB') // 11010048
10
+ parse_bytes('1KIB') // 1024
11
+ parse_bytes('5 mb') // 5242880
12
+ parse_bytes('123') // 123 (bare number means bytes)
13
+ parse_bytes('12KB34') // NaN (trailing garbage, not 12KB)
14
+ parse_bytes(123) // NaN (not a string)
15
+ ```
package/src/parse_hms.js CHANGED
@@ -11,7 +11,7 @@ function parse_hms(expr)
11
11
 
12
12
  const parts = expr.split(':');
13
13
 
14
- let h = 0, m = 0, s = 0;
14
+ let h = 0, m = 0, s;
15
15
 
16
16
  if (parts.length === 1) {
17
17
  // "SS(.fff)"
@@ -1,14 +1,18 @@
1
1
  function parse_http_content_range(value)
2
2
  {
3
3
  // Content-Range: bytes 262144000-272629759/5037662208
4
- const m = value.match(/^(\w+)\s+(\d+)-(\d+)[/](\d+)$/);
5
- if (!m) {
4
+ // Content-Range: bytes */5037662208 (unsatisfied range → first/last are null)
5
+ // Content-Range: bytes 0-499/* (unknown total → total is null)
6
+ const m = value.match(/^(\w+)\s+(?:(\d+)-(\d+)|[*])[/](?:(\d+)|[*])$/);
7
+ // "*/*" is not in the RFC 7233 grammar: an unsatisfied range must state
8
+ // the complete length
9
+ if (!m || (m[2] === undefined && m[4] === undefined)) {
6
10
  throw new Error(`Invalid Content-Range: [${value}]`);
7
11
  }
8
12
  const type = m[1];
9
- const first = +m[2];
10
- const last = +m[3];
11
- const total = +m[4];
13
+ const first = m[2] === undefined ? null : +m[2];
14
+ const last = m[3] === undefined ? null : +m[3];
15
+ const total = m[4] === undefined ? null : +m[4];
12
16
  return {type, first, last, total};
13
17
  }
14
18
 
@@ -0,0 +1,16 @@
1
+ Parses an HTTP `Content-Range` response header into
2
+ `{type, first, last, total}` (numbers, bounds inclusive).
3
+
4
+ - `bytes 0-499/1000` — a regular range.
5
+ - `bytes */5000` — unsatisfied range: `first` and `last` are `null`.
6
+ - `bytes 0-499/*` — unknown total: `total` is `null`.
7
+ - `bytes */*` throws — it is not in the RFC 7233 grammar: an unsatisfied
8
+ range must state the complete length.
9
+
10
+ Throws for anything else it cannot parse.
11
+
12
+ ```js
13
+ parse_http_content_range('bytes 0-499/1000') // {type: 'bytes', first: 0, last: 499, total: 1000}
14
+ parse_http_content_range('bytes */5000') // {type: 'bytes', first: null, last: null, total: 5000}
15
+ parse_http_content_range('bytes 0-499/*') // {type: 'bytes', first: 0, last: 499, total: null}
16
+ ```
@@ -0,0 +1,10 @@
1
+ Returns the time elapsed since `perf_start()` as a human-readable string of
2
+ seconds: `perf_end_human(time0, digits = 4)`. Uses `process.hrtime(time0)`
3
+ under the hood, formatted via `format_hrtime`.
4
+
5
+ ```js
6
+ const time0 = perf_start();
7
+ // ... work ...
8
+ perf_end_human(time0) // '0.0008s'
9
+ perf_end_human(time0, 2) // '0.00s'
10
+ ```
@@ -0,0 +1,8 @@
1
+ Returns the time elapsed since `perf_start()` in milliseconds, rounded to an
2
+ integer. Uses `process.hrtime(time0)` under the hood.
3
+
4
+ ```js
5
+ const time0 = perf_start();
6
+ // ... work ...
7
+ perf_end_ms(time0) // 1
8
+ ```
@@ -0,0 +1,10 @@
1
+ Runs `fn` (sync or async — the result is awaited) and measures how long it
2
+ took with `process.hrtime`. `perf_measure(fn, digits = 4)` returns a promise
3
+ of `{value, time_ms, time_human}` where `value` is the resolved result of
4
+ `fn`, `time_ms` is rounded integer milliseconds, and `time_human` is a
5
+ seconds string with `digits` decimal places.
6
+
7
+ ```js
8
+ await perf_measure(() => 42)
9
+ // {value: 42, time_ms: 1, time_human: '0.0008s'}
10
+ ```
@@ -0,0 +1,8 @@
1
+ Runs `fn` (sync or async — the result is awaited) and returns a promise of
2
+ just the elapsed time as a human-readable seconds string:
3
+ `perf_measure_human(fn, digits = 4)`. The return value of `fn` is discarded;
4
+ use `perf_measure` when you need it too.
5
+
6
+ ```js
7
+ await perf_measure_human(() => heavy_work()) // '0.0006s'
8
+ ```
@@ -0,0 +1,9 @@
1
+ Starts a high-resolution timer: returns `process.hrtime()` — a
2
+ `[seconds, nanoseconds]` tuple to be passed later to `perf_end_ms` or
3
+ `perf_end_human`.
4
+
5
+ ```js
6
+ const time0 = perf_start();
7
+ // ... work ...
8
+ perf_end_ms(time0) // elapsed integer milliseconds
9
+ ```
@@ -0,0 +1,6 @@
1
+ Checks whether a process group is alive by sending signal `0` to the group via
2
+ `process.kill(-pgid, 0)`. Returns `true` when the signal can be delivered to
3
+ at least one member, `false` on any error (no such group, or no permission).
4
+ A group id equals the pid of the group leader.
5
+
6
+ See `pid_exists` for the single-process variant.
@@ -12,7 +12,7 @@ async function pgid_kill_grace(pgid, {grace_timeout_ms = 5000, log = ignore} = {
12
12
  process.kill(-pgid, 'SIGTERM');
13
13
  }
14
14
  catch (error) {
15
- throw new Error(`Failed to send SIGTERM to process group ${pgid}: ${error.message}`);
15
+ throw new Error(`Failed to send SIGTERM to process group ${pgid}: ${error.message}`, {cause: error});
16
16
  }
17
17
 
18
18
  const end = Date.now() + grace_timeout_ms;
@@ -0,0 +1,15 @@
1
+ Terminates a whole process group gracefully: sends `SIGTERM` to the group
2
+ (`kill(-pgid, ...)`), polls (every ≤100ms) until the group dies or the grace
3
+ period expires, then sends `SIGKILL` and waits up to 200ms more for the kernel
4
+ to deliver it. A group id equals the pid of the group leader.
5
+
6
+ - Options: `grace_timeout_ms` (default `5000`) and `log` (progress callback,
7
+ default no-op).
8
+ - Throws `Failed to send SIGTERM to process group <pgid>: ...` when the
9
+ initial signal cannot be sent.
10
+ - A group that dies on its own during the grace period skips the `SIGKILL`;
11
+ `ESRCH` on the `SIGKILL` itself is treated as already dead.
12
+ - Throws `Process group <pgid> survived SIGKILL` if it is still alive 200ms
13
+ after the kill.
14
+
15
+ See `pid_kill_grace` for the single-process variant.
@@ -0,0 +1,5 @@
1
+ Checks whether a process is alive by sending signal `0` via
2
+ `process.kill(pid, 0)`. Returns `true` when the signal can be delivered,
3
+ `false` on any error (no such process, or no permission to signal it).
4
+
5
+ See `pgid_exists` for the process-group variant.
@@ -11,7 +11,7 @@ async function pid_kill_grace(pid, {grace_timeout_ms = 5000, log = ignore} = {})
11
11
  process.kill(pid, 'SIGTERM');
12
12
  }
13
13
  catch (error) {
14
- throw new Error(`Failed to send SIGTERM to process ${pid}: ${error.message}`);
14
+ throw new Error(`Failed to send SIGTERM to process ${pid}: ${error.message}`, {cause: error});
15
15
  }
16
16
 
17
17
  const end = Date.now() + grace_timeout_ms;
@@ -0,0 +1,14 @@
1
+ Terminates a single process gracefully: sends `SIGTERM`, polls (every ≤100ms)
2
+ until the process dies or the grace period expires, then sends `SIGKILL` and
3
+ waits up to 200ms more for the kernel to deliver it.
4
+
5
+ - Options: `grace_timeout_ms` (default `5000`) and `log` (progress callback,
6
+ default no-op).
7
+ - Throws `Failed to send SIGTERM to process <pid>: ...` when the initial
8
+ signal cannot be sent (e.g. the process does not exist).
9
+ - A process that dies on its own during the grace period skips the `SIGKILL`
10
+ entirely; `ESRCH` on the `SIGKILL` itself is treated as already dead.
11
+ - Throws `Process <pid> survived SIGKILL` if it is still alive 200ms after
12
+ the kill.
13
+
14
+ See `pgid_kill_grace` for the process-group variant.
package/src/plural.md ADDED
@@ -0,0 +1,13 @@
1
+ Slavic-style pluralizer: `plural(n, singular, plural, zero)`. The `singular`
2
+ form is used when `n` ends in 1 but not in 11 (1, 21, 101, ...); everything
3
+ else gets the `plural` form. Every `#` in the chosen form is replaced with
4
+ `n`. The optional `zero` string is returned as-is when `n === 0`.
5
+
6
+ ```js
7
+ plural(1, '# apple', '# apples') // '1 apple'
8
+ plural(2, '# apple', '# apples') // '2 apples'
9
+ plural(11, '# apple', '# apples') // '11 apples'
10
+ plural(21, '# apple', '# apples') // '21 apple'
11
+ plural(0, '# apple', '# apples') // '0 apples'
12
+ plural(0, '# apple', '# apples', 'No apples') // 'No apples'
13
+ ```
@@ -0,0 +1,9 @@
1
+ Returns `bytes` cryptographically random bytes (`crypto.randomBytes`) as a
2
+ lowercase hex string — 2 characters per byte. Defaults to 32 bytes (64 hex
3
+ characters).
4
+
5
+ ```js
6
+ random_hex(4) // '0566a554'
7
+ random_hex() // 64-character hex string
8
+ random_hex(0) // ''
9
+ ```
@@ -0,0 +1,8 @@
1
+ Returns a random integer between `min` and `max`, both inclusive. Based on
2
+ `Math.random` — not cryptographically secure (use `random_hex` for tokens).
3
+
4
+ ```js
5
+ random_int(1, 6) // 1..6, like a die roll
6
+ random_int(-3, 3) // -3..3
7
+ random_int(7, 7) // 7
8
+ ```
package/src/range.js CHANGED
@@ -17,7 +17,8 @@ function range(begin, end = na, step = 1, limit = 1E6)
17
17
 
18
18
  function range_forward(begin, end, step, limit)
19
19
  {
20
- if (step < 0) {
20
+ // step = 0 cannot make progress: an empty array, not `limit` copies of begin
21
+ if (step <= 0) {
21
22
  return [];
22
23
  }
23
24
 
@@ -30,7 +31,8 @@ function range_forward(begin, end, step, limit)
30
31
 
31
32
  function range_backward(begin, end, step, limit)
32
33
  {
33
- if (step > 0) {
34
+ // step = 0 cannot make progress: an empty array, not `limit` copies of begin
35
+ if (step >= 0) {
34
36
  return [];
35
37
  }
36
38
 
package/src/range.md CHANGED
@@ -10,6 +10,8 @@ Generates a numeric sequence.
10
10
  - Positive `step` counts **up** while `value < stop`
11
11
  - Negative `step` counts **down** while `value > stop`
12
12
  - If `step` direction does not allow progress, the result is an empty array
13
+ - `step = 0` never makes progress and gives an empty array
14
+ - The output is capped at `limit` items (4th parameter, default `1e6`)
13
15
 
14
16
  **Returns:**
15
17
  An array of numbers.
@@ -6,7 +6,7 @@ const redis = require('redis');
6
6
  async function redis_connect(url)
7
7
  {
8
8
  const tmp = new URL(url);
9
- const password = tmp.password || tmp.searchParams.get('password');
9
+ const password = tmp.password ? decodeURIComponent(tmp.password) : tmp.searchParams.get('password');
10
10
 
11
11
  if (url.match(/^tls:/)) {
12
12
  const out = redis.createClient({
@@ -0,0 +1,10 @@
1
+ Connects to redis from a URL and returns a connected `redis` (node-redis v4+
2
+ style) client. `tcp://` is rewritten to `redis://`; `tls://` is rewritten to
3
+ `rediss://` with peer certificate verification disabled
4
+ (`rejectUnauthorized: false`). The password comes from the URL userinfo
5
+ (URL-decoded) or, failing that, from a `?password=` query parameter.
6
+
7
+ ```js
8
+ await redis_connect('tcp://127.0.0.1:6379');
9
+ await redis_connect('tls://11.11.11.11:6378?password=xxx');
10
+ ```
@@ -0,0 +1,14 @@
1
+ Atomically pops up to `limit` items (default `1`) from the head of a redis
2
+ list: a `MULTI` transaction of `LRANGE queue 0 limit-1` plus
3
+ `LTRIM queue limit -1`. Returns the array of popped items — empty when the
4
+ list is empty.
5
+
6
+ Works with both legacy callback clients (redis@1–3, detected by the presence
7
+ of a lowercase `lrange` method) and promise clients (redis@4–6); the tests run
8
+ against all six major versions.
9
+
10
+ ## Signature
11
+
12
+ ```
13
+ redis_lshift(redis, queue, limit = 1) → Promise<string[]>
14
+ ```
package/src/redis_poll.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-unused-vars -- destructured option lists document the full options contract */
1
2
  const Promise = require('bluebird');
2
3
  const random_int = require('./random_int');
3
4
  const redis_zshift = require('./redis_zshift');
@@ -0,0 +1,17 @@
1
+ Generic worker poll loop with pluggable queue I/O: repeatedly calls
2
+ `options.poll_message()` for the next message object, runs `options.callback`
3
+ on it, and reports the result via `options.push_response({uid, version, type,
4
+ value})` with `type: 'resolve'` on success or `'reject'` (stack/message
5
+ string) on failure. An empty poll sleeps a random 500–1500ms; the loop runs
6
+ at most 86400 iterations.
7
+
8
+ - Messages without `uid` are skipped; a missing or past `expires_at` rejects
9
+ the message (payloads carry signed urls with limited lifetime).
10
+ - The callback receives `{message, log, user_friendly_status}`;
11
+ `user_friendly_status(value)` still rpushes
12
+ `{uid, version, type: 'user_friendly_status', value}` JSON directly to
13
+ `options.redis`/`options.redis_output_queue`, starting with `'Started...'`.
14
+ - `options.log` must provide `spawn()`; the many `log_waiter_*`/`log_worker_*`
15
+ options are the tag names used in log lines.
16
+
17
+ See `redis_poll_zshift_callback_rpush` for the concrete zshift/rpush variant.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-unused-vars -- destructured option lists document the full options contract */
1
2
  const Promise = require('bluebird');
2
3
  const random_int = require('./random_int');
3
4
  const redis_zshift = require('./redis_zshift');
@@ -54,8 +55,6 @@ async function redis_poll_zshift_callback_rpush(options)
54
55
  await Promise.delay(ms);
55
56
  continue;
56
57
  }
57
- console.log(message_string);
58
-
59
58
  let message = null;
60
59
  try {
61
60
  message = JSON.parse(message_string);
@@ -0,0 +1,17 @@
1
+ Worker poll loop over redis queues: repeatedly `redis_zshift`s one message
2
+ from `redis_input_queue`, JSON-parses it, runs `options.callback({message,
3
+ log, user_friendly_status})`, and rpushes a JSON response `{uid, version,
4
+ type, value}` to `redis_output_queue` — `type: 'resolve'` with the callback's
5
+ return value, or `'reject'` with the error stack/message. An empty poll
6
+ sleeps a random 500–1500ms; the loop runs at most 86400 iterations.
7
+
8
+ - Unparseable messages and messages without `uid` are skipped; a missing or
9
+ past `expires_at` rejects the message (payloads carry signed urls with
10
+ limited lifetime).
11
+ - `user_friendly_status(value)` rpushes `{uid, version, type:
12
+ 'user_friendly_status', value}` to the output queue; a generic
13
+ `'Started...'` status is always pushed first so the consumer starts
14
+ refreshing.
15
+ - `options.log` must provide `spawn()`; the `log_waiter_*`/`log_worker_*`
16
+ options are the tag names used in log lines. The redis client must provide
17
+ `rpush_p`.
@@ -138,7 +138,8 @@ async function worker(log, request, options)
138
138
  try {
139
139
  await new Promise(function (resolve, reject) {
140
140
  proc.once('error', reject);
141
- proc.once('exit', code => code ? reject(new Error(`Process terminated with code ${code}`)) : resolve());
141
+ // A signal-killed process exits with code = null: only 0 is a success
142
+ proc.once('exit', (code, signal) => code === 0 ? resolve() : reject(new Error(`Process terminated with code ${code} and signal ${signal}`)));
142
143
  end_stdout = stream_data_ln(proc.stdout, line => log(`[${log_worker_stdout}] ${line}`));
143
144
  end_stderr = stream_data_ln(proc.stderr, line => log(`[${log_worker_stderr}] ${line}`));
144
145
  end_user_friendly_status = stream_data_ln(proc.stdio[3], function (line) {
@@ -0,0 +1,20 @@
1
+ Worker poll loop over redis queues that delegates the actual work to an
2
+ external executable. Repeatedly `redis_zshift`s one message from
3
+ `redis_input_queue`, JSON-parses it, dumps it as `request.json` into a fresh
4
+ temp directory, and spawns `spawn_command` with that directory as `cwd`. On
5
+ exit code 0 it rpushes `{uid, version, type: 'resolve', value: null}` to
6
+ `redis_output_queue`; any failure rpushes `type: 'error'` with the stack.
7
+ An empty poll sleeps a random 500–1500ms; the loop runs at most 86400
8
+ iterations.
9
+
10
+ - The child gets an extra pipe on fd 3: each line written there is rpushed as
11
+ a `{uid, version, type: 'user_friendly_status', value}` message; a generic
12
+ `'Started...'` status is always pushed first. stdout/stderr are logged
13
+ line by line under the `log_worker_stdout`/`log_worker_stderr` tags.
14
+ - Only exit code 0 is a success — a signal-killed child (code `null`) fails.
15
+ - Unparseable messages and messages without `uid` are skipped; a missing or
16
+ past `expires_at` fails the message (payloads carry signed urls with
17
+ limited lifetime).
18
+ - `options.log` must provide `spawn()`; the redis client must provide
19
+ `rpush_p`. See `redis_poll_zshift_callback_rpush` for the in-process
20
+ callback variant (which reports failures as `type: 'reject'`).
@@ -0,0 +1,14 @@
1
+ Atomically pops up to `limit` (default `1`) lowest-scored members from a redis
2
+ sorted set: a `MULTI` transaction of `ZRANGE queue 0 limit-1` plus
3
+ `ZREMRANGEBYRANK queue 0 limit-1`. Returns the array of popped members —
4
+ empty when the set is empty.
5
+
6
+ Works with both legacy callback clients (redis@1–3, detected by the presence
7
+ of a lowercase `zrange` method) and promise clients (redis@4–6); the tests run
8
+ against all six major versions.
9
+
10
+ ## Signature
11
+
12
+ ```
13
+ redis_zshift(redis, queue, limit = 1) → Promise<string[]>
14
+ ```
package/src/round.js CHANGED
@@ -26,14 +26,13 @@ function get_decimal_places(precision)
26
26
  return 0;
27
27
  }
28
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;
29
+ // toExponential gives MANTISSAe±EXP; decimal places = mantissa fraction
30
+ // digits minus the exponent. Counting the exponent alone is not enough:
31
+ // 0.25 → "2.5e-1" → 1 - (-1) = 2 (not 1, which would re-round 1.25 → 1.3)
32
+ // 0.001 → "1e-3" → 0 - (-3) = 3
33
+ const [mantissa, exp] = precision.toExponential().split('e');
34
+ const digits = (mantissa.split('.')[1] || '').length;
35
+ return Math.max(0, digits - parseInt(exp, 10));
37
36
  }
38
37
 
39
38
  // // round(34.037531, 0.001) -> 34.038000000000004
package/src/round.md ADDED
@@ -0,0 +1,15 @@
1
+ Rounds `value` to the nearest multiple of `precision` (default `1`).
2
+ Precision can be fractional (`0.01`), coarse (`25`, `1000`), or a non-power
3
+ of 10 (`0.25`). A tiny signed epsilon fixes floating-point tie cases
4
+ (`1.005` at `0.01` correctly gives `1.01`, `-1.005` gives `-1.01`), and a
5
+ final `toFixed` cleanup strips artifacts like `34.038000000000004`.
6
+ `precision = 0` returns the value unchanged.
7
+
8
+ ```js
9
+ round(2.7) // 3
10
+ round(34.037531, 0.001) // 34.038
11
+ round(1.005, 0.01) // 1.01
12
+ round(123.456, 10) // 120
13
+ round(37.5, 25) // 50
14
+ round(1.13, 0.25) // 1.25
15
+ ```
@@ -0,0 +1,13 @@
1
+ Converts a string to a lowercase dash-name (slug): accents are stripped via
2
+ NFD normalization, every other non-ASCII-alphanumeric character becomes a
3
+ dash, runs of dashes collapse, and leading/trailing dashes are trimmed.
4
+ Characters with no ASCII base letter (e.g. Cyrillic) are dropped, so the
5
+ result can be an empty string.
6
+
7
+ ```js
8
+ sanitize_dash_name('1000 Stories') // '1000-stories'
9
+ sanitize_dash_name('Ópera Prima') // 'opera-prima'
10
+ sanitize_dash_name('Black & Decker') // 'black-decker'
11
+ sanitize_dash_name('M&M\'s') // 'm-m-s'
12
+ sanitize_dash_name('привет') // ''
13
+ ```
@@ -0,0 +1,9 @@
1
+ Thin wrapper around the [sanitize-filename](https://www.npmjs.com/package/sanitize-filename)
2
+ package: makes a string safe to use as a single filename by removing path
3
+ separators, reserved/control characters, and Windows-reserved names. It does
4
+ not slugify — spaces, dots, and unicode are kept.
5
+
6
+ ```js
7
+ sanitize_filename('../some/file*name?.txt') // '..somefilename.txt'
8
+ sanitize_filename('report: 2024/Q1.pdf') // 'report 2024Q1.pdf'
9
+ ```
@@ -0,0 +1,14 @@
1
+ Converts an arbitrary string (a spreadsheet header, an API field name) into
2
+ a safe snake_case identifier: camelCase is split first
3
+ (`str_camel_to_snake`), accents and symbols are handled by
4
+ `sanitize_dash_name`, and the dashes become underscores. The result always
5
+ matches `[a-z0-9_]+`: an empty result becomes `'_'`, and a leading digit is
6
+ prefixed with `'_'`.
7
+
8
+ ```js
9
+ sanitize_var_name('Image URL') // 'image_url'
10
+ sanitize_var_name('ItemInternalId') // 'item_internal_id'
11
+ sanitize_var_name('@odata.etag') // 'odata_etag'
12
+ sanitize_var_name('1000-stories') // '_1000_stories'
13
+ sanitize_var_name('') // '_'
14
+ ```
@@ -3,10 +3,10 @@ const ignore = require('./ignore');
3
3
  const ssh2 = require('ssh2');
4
4
  const waitcb = require('./waitcb');
5
5
 
6
- async function sftp_get_file_info(url, {user_friendly_status = ignore})
6
+ async function sftp_get_file_info(url, {user_friendly_status = ignore} = {})
7
7
  {
8
8
  const u = new URL(url);
9
- const host = u.host;
9
+ const host = u.hostname; // u.host would include the :port, breaking dns lookup
10
10
  const port = +u.port || 22;
11
11
  const username = decodeURIComponent(u.username);
12
12
  const password = decodeURIComponent(u.password);
@@ -0,0 +1,14 @@
1
+ Connects to an sftp server described by a URL
2
+ (`sftp://user:pass@host:port/path`) and returns `{stat, lstat, readdir}` for
3
+ the path. Credentials are percent-decoded from the URL; the port defaults
4
+ to 22. The connection is always closed afterwards, on success and on failure.
5
+
6
+ Rejects on connection or authentication errors, and when the path does not
7
+ exist. The optional `user_friendly_status` callback receives progress
8
+ messages (`'Establishing connection...'`, ...).
9
+
10
+ ```js
11
+ const info = await sftp_get_file_info('sftp://user:p%40ss@example.com/upload');
12
+ info.stat.isDirectory() // true
13
+ info.readdir[0] // {filename, longname, attrs}
14
+ ```
@@ -5,7 +5,7 @@ const waitcb = require('./waitcb');
5
5
  async function sftp_get_stream_range(url, first, last, {user_friendly_status = ignore} = {})
6
6
  {
7
7
  const u = new URL(url);
8
- const host = u.host;
8
+ const host = u.hostname; // u.host would include the :port, breaking dns lookup
9
9
  const port = +u.port || 22;
10
10
  const username = decodeURIComponent(u.username);
11
11
  const password = decodeURIComponent(u.password);
@@ -19,31 +19,41 @@ async function sftp_get_stream_range(url, first, last, {user_friendly_status = i
19
19
  conn.connect({host, port, username, password});
20
20
  });
21
21
 
22
- user_friendly_status('Asking for an sftp service...');
23
- const sftp = await waitcb(cb => conn.sftp(cb));
22
+ try {
23
+ user_friendly_status('Asking for an sftp service...');
24
+ const sftp = await waitcb(cb => conn.sftp(cb));
24
25
 
25
- user_friendly_status('Requesting file info...');
26
- const stat = await waitcb(cb => sftp.stat(pathname, cb));
26
+ user_friendly_status('Requesting file info...');
27
+ const stat = await waitcb(cb => sftp.stat(pathname, cb));
27
28
 
28
- if (first < 0 || last >= stat.size) {
29
- throw new Error(`Invalid range: [first=${first}][last=${last}]`);
30
- }
29
+ if (first < 0 || last >= stat.size) {
30
+ throw new Error(`Invalid range: [first=${first}][last=${last}]`);
31
+ }
31
32
 
32
- const out = sftp.createReadStream(pathname, {start: first, end: last})
33
- out.content_range = {
34
- type: 'bytes',
35
- first,
36
- last,
37
- total: stat.size,
38
- };
39
- out.total = out.content_range.total;
40
- out.once('error', function () {
41
- conn.destroy();
42
- });
43
- out.once('end', function () {
33
+ const out = sftp.createReadStream(pathname, {start: first, end: last})
34
+ out.content_range = {
35
+ type: 'bytes',
36
+ first,
37
+ last,
38
+ total: stat.size,
39
+ };
40
+ out.total = out.content_range.total;
41
+ // 'close' fires on end, on error, and also when the consumer
42
+ // destroys the stream early (e.g. an aborted http range request) —
43
+ // 'end'/'error' alone would leak the connection in that case.
44
+ out.once('close', function () {
45
+ // destroying the connection errors the in-flight sftp requests
46
+ // (prefetched READs) with "No response from server", and those
47
+ // land on this already-closed stream — ignore them
48
+ out.on('error', ignore);
49
+ conn.destroy();
50
+ });
51
+ return out;
52
+ }
53
+ catch (error) {
44
54
  conn.destroy();
45
- });
46
- return out;
55
+ throw error;
56
+ }
47
57
  }
48
58
 
49
59
  module.exports = sftp_get_stream_range;
@@ -0,0 +1,20 @@
1
+ Opens a byte range of a remote file over sftp and returns it as a readable
2
+ stream. The URL carries the connection details
3
+ (`sftp://user:pass@host:port/path`); credentials are percent-decoded, the
4
+ port defaults to 22. `first` and `last` are inclusive byte offsets.
5
+
6
+ The file is `stat`ed first: `first < 0` or `last >= size` rejects with an
7
+ `Invalid range` error (no clamping, unlike `http_range_parse`). Like
8
+ `http_get_stream_range`, the stream gets `content_range`
9
+ (`{type, first, last, total}`) and `total` (the full file size) attached.
10
+
11
+ The ssh connection is destroyed when the stream closes — on end, on error,
12
+ and also when the consumer destroys the stream early (e.g. an aborted http
13
+ range request); nothing leaks. Setup failures (bad credentials, missing
14
+ file, invalid range) also close the connection before rejecting. The
15
+ optional `user_friendly_status` callback receives progress messages.
16
+
17
+ ```js
18
+ const rs = await sftp_get_stream_range(url, 0, 1023, {user_friendly_status: console.log});
19
+ rs.total // full file size
20
+ ```
@@ -21,7 +21,8 @@ async function shell_curl_progress(args, {options, user_friendly_status})
21
21
  const promises = [];
22
22
  promises.push(new Promise(function (resolve, reject) {
23
23
  proc.once('error', reject);
24
- proc.once('exit', code => code ? reject(new Error(`Process terminated with code ${code}`)) : resolve());
24
+ // A signal-killed process exits with code = null: only 0 is a success
25
+ proc.once('exit', (code, signal) => code === 0 ? resolve() : reject(new Error(`Process terminated with code ${code} and signal ${signal}`)));
25
26
  }));
26
27
  promises.push(stream.promises.pipeline(proc.stderr, stream_curl_progress(), stream_each(progress_fn)));
27
28