@vbarbarosh/node-helpers 3.74.1 → 3.76.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 (367) hide show
  1. package/README.md +29 -3
  2. package/dist/browser.js +1 -2504
  3. package/package.json +31 -11
  4. package/src/HeartbeatServer.js +24 -3
  5. package/src/HeartbeatServer.md +33 -0
  6. package/src/array_chunk.js +1 -1
  7. package/src/array_chunk.md +8 -0
  8. package/src/array_chunk_balanced.md +14 -0
  9. package/src/array_fps.md +12 -0
  10. package/src/array_gcd.js +4 -2
  11. package/src/array_gcd.md +2 -1
  12. package/src/array_group.js +3 -3
  13. package/src/array_group.md +14 -0
  14. package/src/array_group_map.js +3 -3
  15. package/src/array_group_map.md +13 -0
  16. package/src/array_index.js +2 -2
  17. package/src/array_index.md +9 -0
  18. package/src/array_lcm.js +4 -2
  19. package/src/array_lcm.md +2 -1
  20. package/src/array_max.js +11 -4
  21. package/src/array_max.md +1 -1
  22. package/src/array_min.js +11 -4
  23. package/src/array_min.md +1 -1
  24. package/src/array_permutations.js +4 -0
  25. package/src/array_permutations.md +13 -0
  26. package/src/array_sort.js +7 -2
  27. package/src/array_sort.md +13 -0
  28. package/src/array_sort_other.js +8 -5
  29. package/src/array_sort_other.md +11 -0
  30. package/src/array_sum.js +2 -2
  31. package/src/array_sum.md +8 -0
  32. package/src/array_unique.js +2 -2
  33. package/src/array_unique_last.js +2 -2
  34. package/src/assert_valid_expires_at.md +10 -0
  35. package/src/clamp.md +9 -0
  36. package/src/cli-apps/watchdog.js +5 -0
  37. package/src/cli.js +3 -1
  38. package/src/cli.md +15 -0
  39. package/src/const_stream.md +7 -0
  40. package/src/const_type.md +5 -0
  41. package/src/countdown.js +22 -5
  42. package/src/countdown.md +21 -0
  43. package/src/crypto_decrypt_aes256.md +13 -0
  44. package/src/crypto_encrypt_aes256.md +13 -0
  45. package/src/crypto_hash_md5.md +10 -0
  46. package/src/crypto_hash_sha256.md +10 -0
  47. package/src/crypto_hmac_sha256.md +11 -0
  48. package/src/date_add_hours.md +8 -0
  49. package/src/date_add_milliseconds.md +4 -0
  50. package/src/date_add_minutes.md +4 -0
  51. package/src/date_add_months.md +13 -0
  52. package/src/date_add_seconds.md +4 -0
  53. package/src/date_diff_seconds.md +4 -0
  54. package/src/date_is_leap_year.md +9 -0
  55. package/src/escape_content_disposition.js +6 -3
  56. package/src/escape_content_disposition.md +13 -0
  57. package/src/escape_regexp.md +7 -0
  58. package/src/eta.js +7 -1
  59. package/src/eta.md +18 -0
  60. package/src/factorize_ms.js +3 -0
  61. package/src/factorize_ms.md +10 -0
  62. package/src/fastdl.js +14 -3
  63. package/src/fcmp_dates.md +12 -0
  64. package/src/fcmp_default.js +7 -0
  65. package/src/fcmp_default.md +29 -0
  66. package/src/fcmp_default_desc.js +4 -7
  67. package/src/fcmp_default_desc.md +10 -0
  68. package/src/fcmp_from_spec.md +13 -0
  69. package/src/fcmp_numbers.md +11 -0
  70. package/src/fcmp_tuples.md +17 -0
  71. package/src/fcmp_utf8_bin.md +14 -0
  72. package/src/fcmp_utf8_ci.md +14 -0
  73. package/src/fcmp_utf8_cs.md +15 -0
  74. package/src/fcmp_utf8_natural_bin.js +38 -0
  75. package/src/fcmp_utf8_natural_bin.md +8 -0
  76. package/src/fcmp_utf8_natural_ci.md +13 -0
  77. package/src/fcmp_utf8_natural_cs.md +13 -0
  78. package/src/fcmpx.md +26 -9
  79. package/src/filter1_from_spec.js +3 -3
  80. package/src/format_bytes.md +15 -0
  81. package/src/format_date.md +7 -0
  82. package/src/format_date_fs.md +7 -0
  83. package/src/format_date_human.md +6 -0
  84. package/src/format_date_ymd.md +6 -0
  85. package/src/format_error_report.js +3 -3
  86. package/src/format_error_report.md +12 -0
  87. package/src/format_kilo.md +13 -0
  88. package/src/format_ms.js +3 -2
  89. package/src/format_ms.md +12 -0
  90. package/src/format_ms2.js +6 -3
  91. package/src/format_ms2.md +12 -0
  92. package/src/format_ms3.js +3 -2
  93. package/src/format_ms3.md +12 -0
  94. package/src/format_percents.md +10 -0
  95. package/src/format_progress_bytes.md +13 -0
  96. package/src/format_progress_kilo.md +10 -0
  97. package/src/format_seconds.md +11 -0
  98. package/src/format_thousands.md +9 -0
  99. package/src/fs_append.md +7 -0
  100. package/src/fs_append_json.md +8 -0
  101. package/src/fs_append_utf8.md +6 -0
  102. package/src/fs_assert_file_exists.md +3 -0
  103. package/src/fs_assert_file_readable.js +1 -1
  104. package/src/fs_assert_file_readable.md +3 -0
  105. package/src/fs_assert_file_socket.md +2 -0
  106. package/src/fs_copy.md +7 -0
  107. package/src/fs_copy_excl.md +2 -0
  108. package/src/fs_exists.md +6 -0
  109. package/src/fs_fclose.md +5 -0
  110. package/src/fs_fi.js +13 -1
  111. package/src/fs_fi.md +11 -0
  112. package/src/fs_file_md5.md +8 -0
  113. package/src/fs_find.md +7 -0
  114. package/src/fs_fopen.md +6 -0
  115. package/src/fs_fread.js +2 -2
  116. package/src/fs_fread.md +11 -0
  117. package/src/fs_fsize.md +2 -0
  118. package/src/fs_fstat.md +2 -0
  119. package/src/fs_fwrite.js +2 -2
  120. package/src/fs_fwrite.md +9 -0
  121. package/src/fs_list.md +5 -0
  122. package/src/fs_list_deep.md +5 -0
  123. package/src/fs_ls.md +2 -0
  124. package/src/fs_lsr.md +4 -0
  125. package/src/fs_lstat.md +3 -0
  126. package/src/fs_mkdir.md +3 -0
  127. package/src/fs_mkdirp.js +3 -30
  128. package/src/fs_mkdirp.md +7 -0
  129. package/src/fs_path_basename.md +2 -0
  130. package/src/fs_path_dirname.md +2 -0
  131. package/src/fs_path_extname.md +9 -0
  132. package/src/fs_path_join.md +2 -0
  133. package/src/fs_path_relative.md +2 -0
  134. package/src/fs_path_resolve.md +2 -0
  135. package/src/fs_path_tempdir.md +2 -0
  136. package/src/fs_path_untildify.md +8 -0
  137. package/src/fs_read.md +3 -0
  138. package/src/fs_read_buffer.md +5 -0
  139. package/src/fs_read_json.md +6 -0
  140. package/src/fs_read_lines.md +7 -0
  141. package/src/fs_read_stream.md +2 -0
  142. package/src/fs_read_utf8.md +5 -0
  143. package/src/fs_readdir.md +3 -0
  144. package/src/fs_readlink.md +6 -0
  145. package/src/fs_rename.md +6 -0
  146. package/src/fs_rm.md +4 -0
  147. package/src/fs_rmdir.md +3 -0
  148. package/src/fs_rmf.md +2 -0
  149. package/src/fs_rmrf.md +8 -0
  150. package/src/fs_sha256.js +16 -0
  151. package/src/fs_sha256.md +9 -0
  152. package/src/fs_size.js +2 -2
  153. package/src/fs_size.md +6 -0
  154. package/src/fs_stat.md +2 -0
  155. package/src/fs_tempdir.js +20 -5
  156. package/src/fs_touch.md +6 -0
  157. package/src/fs_walk.js +2 -2
  158. package/src/fs_walk.md +14 -0
  159. package/src/fs_write.md +3 -0
  160. package/src/fs_write_json.md +7 -0
  161. package/src/fs_write_stream.md +2 -0
  162. package/src/gettype.md +13 -0
  163. package/src/http_delete.md +11 -0
  164. package/src/http_get_blob.md +8 -0
  165. package/src/http_get_buffer.md +10 -0
  166. package/src/http_get_file.js +8 -8
  167. package/src/http_get_file.md +14 -0
  168. package/src/http_get_json.md +10 -0
  169. package/src/http_get_stream.md +14 -0
  170. package/src/http_get_stream_range.js +27 -9
  171. package/src/http_get_stream_range.md +22 -0
  172. package/src/http_get_utf8.md +9 -0
  173. package/src/http_head.md +9 -0
  174. package/src/http_patch_json.md +10 -0
  175. package/src/http_post_json.md +11 -0
  176. package/src/http_post_multipart.md +15 -0
  177. package/src/http_post_urlencoded.md +11 -0
  178. package/src/http_put_buffer.md +9 -0
  179. package/src/http_put_file.js +9 -6
  180. package/src/http_put_file.md +16 -0
  181. package/src/http_put_json.md +11 -0
  182. package/src/http_put_utf8.md +9 -0
  183. package/src/http_range_parse.js +6 -3
  184. package/src/http_range_parse.md +18 -0
  185. package/src/http_stream_range.js +35 -22
  186. package/src/http_stream_range.md +29 -0
  187. package/src/identity.md +2 -0
  188. package/src/ignore.md +2 -0
  189. package/src/json_stringify_safe.md +10 -0
  190. package/src/json_stringify_stable.md +9 -0
  191. package/src/logger.js +2 -3
  192. package/src/logger.md +15 -0
  193. package/src/make_progress.js +8 -5
  194. package/src/make_progress.md +20 -0
  195. package/src/math_gcd.js +2 -1
  196. package/src/mongo_stream_upsert.js +10 -3
  197. package/src/mongo_stream_upsert.md +17 -0
  198. package/src/mongo_stream_write.js +10 -3
  199. package/src/mongo_stream_write.md +16 -0
  200. package/src/msval.md +10 -0
  201. package/src/now_fs.md +7 -0
  202. package/src/now_human.md +6 -0
  203. package/src/object_defaults.md +11 -0
  204. package/src/object_schema.js +2 -1
  205. package/src/object_schema.md +12 -0
  206. package/src/object_walk_preorder.md +16 -0
  207. package/src/parallel.js +23 -10
  208. package/src/parse_bytes.js +4 -2
  209. package/src/parse_bytes.md +15 -0
  210. package/src/parse_hms.js +1 -1
  211. package/src/parse_http_content_range.js +9 -5
  212. package/src/parse_http_content_range.md +16 -0
  213. package/src/perf_end_human.md +10 -0
  214. package/src/perf_end_ms.md +8 -0
  215. package/src/perf_measure.md +10 -0
  216. package/src/perf_measure_human.md +8 -0
  217. package/src/perf_start.md +9 -0
  218. package/src/pgid_exists.md +6 -0
  219. package/src/pgid_kill_grace.js +1 -1
  220. package/src/pgid_kill_grace.md +15 -0
  221. package/src/pid_exists.md +5 -0
  222. package/src/pid_kill_grace.js +1 -1
  223. package/src/pid_kill_grace.md +14 -0
  224. package/src/plural.md +13 -0
  225. package/src/random_hex.md +9 -0
  226. package/src/random_int.md +8 -0
  227. package/src/range.js +4 -2
  228. package/src/range.md +2 -0
  229. package/src/redis_connect.js +1 -1
  230. package/src/redis_connect.md +10 -0
  231. package/src/redis_lshift.md +14 -0
  232. package/src/redis_poll.js +1 -0
  233. package/src/redis_poll.md +17 -0
  234. package/src/redis_poll_zshift_callback_rpush.js +1 -2
  235. package/src/redis_poll_zshift_callback_rpush.md +17 -0
  236. package/src/redis_poll_zshift_spawn_rpush.js +2 -1
  237. package/src/redis_poll_zshift_spawn_rpush.md +20 -0
  238. package/src/redis_zshift.md +14 -0
  239. package/src/round.js +7 -8
  240. package/src/round.md +15 -0
  241. package/src/sanitize_dash_name.md +13 -0
  242. package/src/sanitize_filename.md +9 -0
  243. package/src/sanitize_var_name.md +14 -0
  244. package/src/sftp_get_file_info.js +2 -2
  245. package/src/sftp_get_file_info.md +14 -0
  246. package/src/sftp_get_stream_range.js +32 -22
  247. package/src/sftp_get_stream_range.md +20 -0
  248. package/src/shell_curl_progress.js +2 -1
  249. package/src/shell_curl_progress.md +17 -0
  250. package/src/shell_lines.js +2 -2
  251. package/src/shell_lines.md +1 -1
  252. package/src/shell_spawn.js +7 -0
  253. package/src/shell_ytdlp_progress.js +2 -1
  254. package/src/shell_ytdlp_progress.md +17 -0
  255. package/src/str_camel_to_snake.md +11 -0
  256. package/src/str_parse_kv.md +11 -0
  257. package/src/stream_curl_progress.md +14 -0
  258. package/src/stream_data_ln.js +7 -1
  259. package/src/stream_discard.md +8 -0
  260. package/src/stream_group.md +3 -0
  261. package/src/stream_gunzip.md +7 -0
  262. package/src/stream_hash.md +10 -0
  263. package/src/stream_map.js +8 -1
  264. package/src/stream_map_parallel.js +9 -1
  265. package/src/stream_md5.md +9 -0
  266. package/src/stream_multiplex.js +11 -0
  267. package/src/stream_multiplex.md +16 -0
  268. package/src/stream_through.md +3 -0
  269. package/src/stream_transform.md +2 -0
  270. package/src/stream_xml_analyze.js +9 -1
  271. package/src/stream_xml_parse.js +9 -1
  272. package/src/stream_ytdlp_progress.js +1 -1
  273. package/src/stream_ytdlp_progress.md +18 -0
  274. package/src/strtr.js +15 -5
  275. package/src/strtr.md +11 -0
  276. package/src/urlclean.js +9 -2
  277. package/src/urlmod.js +5 -0
  278. package/src/urlparts.md +17 -0
  279. package/src/wait_while.js +1 -1
  280. package/src/wait_while.md +1 -1
  281. package/src/HeartbeatServer.test.js +0 -31
  282. package/src/array_chunk.test.js +0 -24
  283. package/src/array_chunk_balanced.test.js +0 -37
  284. package/src/array_fps.test.js +0 -72
  285. package/src/array_gcd.test.js +0 -10
  286. package/src/array_index.test.js +0 -14
  287. package/src/array_lcm.test.js +0 -12
  288. package/src/array_max.test.js +0 -20
  289. package/src/array_min.test.js +0 -20
  290. package/src/array_permutations.test.js +0 -20
  291. package/src/array_sort.test.js +0 -85
  292. package/src/array_sort_other.test.js +0 -9
  293. package/src/array_sum.test.js +0 -11
  294. package/src/cli-apps/watchdog.d/connect_close.js +0 -17
  295. package/src/cli-apps/watchdog.d/connect_close2.js +0 -20
  296. package/src/cli-apps/watchdog.d/connect_close3.js +0 -25
  297. package/src/cli-apps/watchdog.d/connect_no_ping.js +0 -13
  298. package/src/cli-apps/watchdog.d/early_exit.js +0 -7
  299. package/src/cli-apps/watchdog.d/flood.js +0 -13
  300. package/src/cli-apps/watchdog.d/flood2.js +0 -15
  301. package/src/cli-apps/watchdog.d/hang.js +0 -8
  302. package/src/cli-apps/watchdog.d/huge_delay.js +0 -18
  303. package/src/cli-apps/watchdog.d/ignore_sigterm.js +0 -29
  304. package/src/cli-apps/watchdog.d/late_heartbeat.js +0 -21
  305. package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +0 -16
  306. package/src/cli-apps/watchdog.d/my-happy-path.js +0 -16
  307. package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +0 -22
  308. package/src/cli-apps/watchdog.d/my-never-ping.js +0 -15
  309. package/src/cli-apps/watchdog.d/no_heartbeat.js +0 -5
  310. package/src/cli-apps/watchdog.d/ok.js +0 -17
  311. package/src/cli-apps/watchdog.d/quick-exit-after-2-pings.js +0 -15
  312. package/src/cli-apps/watchdog.d/quick-hang-after-1-ping.js +0 -15
  313. package/src/cli-apps/watchdog.d/spawn_children.js +0 -14
  314. package/src/cli-apps/watchdog.test.js +0 -48
  315. package/src/crypto_encrypt_aes256.test.js +0 -10
  316. package/src/crypto_hash_sha256.test.js +0 -10
  317. package/src/date_add_months.test.js +0 -40
  318. package/src/date_is_leap_year.test.js +0 -14
  319. package/src/escape_content_disposition.test.js +0 -22
  320. package/src/fcmp_from_spec.test.js +0 -91
  321. package/src/fcmpx.test.js +0 -173
  322. package/src/filter1_from_spec.test.js +0 -31
  323. package/src/format_bytes.test.js +0 -22
  324. package/src/format_error_report.d/src/1.js +0 -26
  325. package/src/format_error_report.d/src/2.js +0 -29
  326. package/src/format_kilo.test.js +0 -25
  327. package/src/format_ms.test.js +0 -33
  328. package/src/format_ms2.test.js +0 -33
  329. package/src/format_ms3.test.js +0 -33
  330. package/src/format_percents.test.js +0 -22
  331. package/src/format_progress_bytes.test.js +0 -19
  332. package/src/format_progress_kilo.test.js +0 -19
  333. package/src/format_seconds.test.js +0 -42
  334. package/src/format_thousands.test.js +0 -28
  335. package/src/fs_file_md5.test.js +0 -11
  336. package/src/fs_mkdirp.test.js +0 -24
  337. package/src/fs_path_extname.test.js +0 -22
  338. package/src/gettype.test.js +0 -27
  339. package/src/object_schema.test.js +0 -15
  340. package/src/object_walk_preorder.test.js +0 -17
  341. package/src/parallel.test.js +0 -11
  342. package/src/parse_bytes.test.js +0 -21
  343. package/src/parse_hms.test.js +0 -32
  344. package/src/parse_http_content_range.test.js +0 -14
  345. package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +0 -33
  346. package/src/pid_kill_grace.d/ignore-sigterm.js +0 -22
  347. package/src/pid_kill_grace.test.js +0 -63
  348. package/src/ping_socket.test.js +0 -58
  349. package/src/plural.test.js +0 -20
  350. package/src/range.test.js +0 -41
  351. package/src/redis_lshift.test.js +0 -57
  352. package/src/redis_zshift.test.js +0 -58
  353. package/src/round.test.js +0 -56
  354. package/src/sanitize_dash_name.test.js +0 -59
  355. package/src/sanitize_var_name.test.js +0 -39
  356. package/src/shell_spawn.test.js +0 -30
  357. package/src/str_camel_to_snake.test.js +0 -17
  358. package/src/stream_curl_progress.d/basic +0 -3
  359. package/src/stream_curl_progress.d/basic.json +0 -42
  360. package/src/stream_curl_progress.test.js +0 -17
  361. package/src/stream_data_ln.test.js +0 -24
  362. package/src/stream_map_parallel.test.js +0 -26
  363. package/src/stream_multiplex.test.js +0 -67
  364. package/src/stream_strpbrk.test.js +0 -29
  365. package/src/urlclean.test.js +0 -18
  366. package/src/urlmod.test.js +0 -40
  367. package/src/urlparts.test.js +0 -294
@@ -0,0 +1,9 @@
1
+ Sends a `PUT` request via axios with a string body sent as-is and resolves
2
+ with `response.data`. Extra axios options — e.g. a `Content-Type` header —
3
+ pass through.
4
+
5
+ ```js
6
+ await http_put_utf8(upload_url, csv, {headers: {'Content-Type': 'text/csv'}});
7
+ ```
8
+
9
+ Sibling of `http_put_buffer` (binary body) and `http_put_json` (JSON body).
@@ -12,15 +12,18 @@ function http_range_parse(range, total)
12
12
  let first = +m[1];
13
13
  let last = +m[2];
14
14
 
15
- if (!m[1]) { // -LAST
16
- first = total - last;
15
+ if (!m[1]) { // -LAST: a suffix longer than the resource means the whole resource (RFC 7233)
16
+ first = Math.max(0, total - last);
17
17
  last = total - 1;
18
18
  }
19
19
  else if (!m[2]) { // FIRST-
20
20
  last = total - 1;
21
21
  }
22
+ else if (last >= total) { // FIRST-LAST reaching past the end reads to the end (RFC 7233)
23
+ last = total - 1;
24
+ }
22
25
 
23
- if (first < 0 || first > last || first >= total || last < 0 || last < first || last >= total) {
26
+ if (first < 0 || first > last || first >= total) {
24
27
  throw new Error(`Invalid range: first=${first}, last=${last}, total=${total}, expr="${range}"`);
25
28
  }
26
29
 
@@ -0,0 +1,18 @@
1
+ Parses an HTTP `Range` request header against a known resource size. Supports
2
+ `bytes=FIRST-LAST`, `bytes=FIRST-`, and `bytes=-LAST` (single range, `bytes`
3
+ unit only; spaces around `=` are allowed). Returns `{first, last}` with both
4
+ bounds inclusive.
5
+
6
+ Clamping follows RFC 7233:
7
+
8
+ - a suffix longer than the resource (`bytes=-5000` of 100) → the whole resource
9
+ - `last` past the end (`bytes=0-999999` of 100) → reads to the end
10
+
11
+ Throws for unparsable expressions and for unsatisfiable ranges: `first` past
12
+ the end (`bytes=100-` of 100), `first > last`, or an empty suffix (`bytes=-0`).
13
+
14
+ ```js
15
+ http_range_parse('bytes=500-', 1000) // {first: 500, last: 999}
16
+ http_range_parse('bytes=-100', 1000) // {first: 900, last: 999}
17
+ http_range_parse('bytes=0-999999', 100) // {first: 0, last: 99}
18
+ ```
@@ -27,6 +27,7 @@ async function http_stream_range(req, res, file)
27
27
  const mime = mime_types.lookup(file);
28
28
 
29
29
  if (req.method === 'HEAD') {
30
+ res.header('Accept-Ranges', 'bytes');
30
31
  res.header('Content-Type', mime);
31
32
  res.header('Content-Length', total);
32
33
  res.header('Content-Disposition', `inline; filename=${escape_content_disposition(fs_path_basename(file))};`);
@@ -40,6 +41,34 @@ async function http_stream_range(req, res, file)
40
41
  return;
41
42
  }
42
43
 
44
+ res.header('Accept-Ranges', 'bytes');
45
+
46
+ let first = 0;
47
+ let last = total - 1;
48
+ let status = 200;
49
+
50
+ // bytes=0-
51
+ // bytes=-100
52
+ if (req.headers.range) {
53
+ try {
54
+ const range = http_range_parse(req.headers.range, total);
55
+ first = range.first;
56
+ last = range.last;
57
+ status = 206;
58
+ }
59
+ catch (error) {
60
+ // A Range expression we cannot parse is ignored (RFC 7233 says to
61
+ // serve the whole resource), but a syntactically valid range which
62
+ // cannot be satisfied gets 416
63
+ if (error.message.startsWith('Invalid range')) {
64
+ res.status(416);
65
+ res.header('Content-Range', `bytes */${total}`);
66
+ res.end();
67
+ return;
68
+ }
69
+ }
70
+ }
71
+
43
72
  const fp = await fs_fopen(file);
44
73
  const buf = Buffer.alloc(2*1024*1024);
45
74
 
@@ -47,33 +76,17 @@ async function http_stream_range(req, res, file)
47
76
  let req_close = false;
48
77
  req.once('close', () => req_close = true);
49
78
 
50
- let first, last;
51
-
52
- // bytes=0-
53
- // bytes=-100
54
- if (req.headers.range) {
55
- const range = http_range_parse(req.headers.range, total);
56
- first = range.first;
57
- last = range.last;
58
- // req.log(`[http_stream_range_range] ${JSON.stringify({range: req.headers.range, first, last, total, orig: req.headers.range})}`);
59
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
60
- res.status(206);
61
- res.header('Content-Type', mime);
79
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
80
+ res.status(status);
81
+ res.header('Content-Type', mime);
82
+ res.header('Content-Length', last - first + 1);
83
+ if (status === 206) {
62
84
  res.header('Content-Range', `bytes ${first}-${last}/${total}`);
63
- res.header('Content-Length', last - first + 1);
64
- }
65
- else {
66
- first = 0;
67
- last = total - 1;
68
- res.header('Content-Type', mime);
69
- res.header('Content-Length', last - first + 1);
70
85
  }
71
86
 
72
- let total_read = 0;
73
87
  for (let offset = first; offset <= last && !req_close; ) {
74
88
  const chunk = await fs_fread(fp, buf, offset, Math.min(buf.length, last - offset + 1));
75
89
  offset += chunk.length;
76
- total_read += chunk.length;
77
90
  if (res.write(chunk)) {
78
91
  // req.log(`[http_stream_range_write] ${chunk.length}`);
79
92
  continue;
@@ -96,7 +109,7 @@ async function http_stream_range(req, res, file)
96
109
  });
97
110
  }
98
111
 
99
- // req.log(`[http_stream_range_end_ok] ${JSON.stringify({req_close, total_read})}`);
112
+ // req.log(`[http_stream_range_end_ok] ${JSON.stringify({req_close})}`);
100
113
  res.end();
101
114
  }
102
115
  finally {
@@ -0,0 +1,29 @@
1
+ Express/Node request handler that streams a file from disk, honoring a single
2
+ HTTP `Range` header (parsed with `http_range_parse`; see its doc for supported
3
+ forms and clamping). `Content-Type` comes from `mime-types` lookup on the file
4
+ name.
5
+
6
+ ## Signature
7
+
8
+ ```
9
+ http_stream_range(req, res, file) → Promise
10
+ ```
11
+
12
+ - `HEAD` → 200 with `Content-Type`, `Content-Length`, and an
13
+ `inline; filename=...` `Content-Disposition` (escaped via
14
+ `escape_content_disposition`); no body.
15
+ - `GET` with `Range` → 206 with `Content-Range: bytes first-last/total` and a
16
+ matching `Content-Length`.
17
+ - `GET` without `Range` → 200 with the whole file.
18
+ - A `Range` expression it cannot parse is ignored → 200 with the whole file
19
+ (RFC 7233); an unsatisfiable range → 416 with `Content-Range: bytes */total`.
20
+ - Any other method → 405.
21
+
22
+ `GET` and `HEAD` responses advertise `Accept-Ranges: bytes`.
23
+
24
+ Reads in 2 MiB chunks with an `fs_fread` loop, respecting backpressure
25
+ (`res.write` + `drain`) and stopping early when the client disconnects.
26
+
27
+ ```js
28
+ app.get('/file', (req, res) => http_stream_range(req, res, '/data/movie.mp4'));
29
+ ```
@@ -0,0 +1,2 @@
1
+ Returns its first argument unchanged (objects by reference). Useful as a
2
+ default projection or a no-op transformer.
package/src/ignore.md ADDED
@@ -0,0 +1,2 @@
1
+ Does nothing and returns `undefined`, whatever the arguments. Useful as a
2
+ no-op callback or for explicitly discarding a promise result.
@@ -0,0 +1,10 @@
1
+ Re-export of the [json-stringify-safe](https://www.npmjs.com/package/json-stringify-safe)
2
+ package: `JSON.stringify` that never throws on circular references — cycles
3
+ are replaced with `'[Circular ~]'`-style markers. Same signature as
4
+ `JSON.stringify(obj, replacer, spaces)` plus an optional 4th `cycleReplacer`.
5
+
6
+ ```js
7
+ const a = {name: 'x'};
8
+ a.self = a;
9
+ json_stringify_safe(a) // '{"name":"x","self":"[Circular ~]"}'
10
+ ```
@@ -0,0 +1,9 @@
1
+ Re-export of the [json-stable-stringify](https://www.npmjs.com/package/json-stable-stringify)
2
+ package: deterministic `JSON.stringify` — object keys are sorted at every
3
+ level, so equal objects always produce byte-identical output. Useful for
4
+ hashing, caching keys, and diff-friendly snapshots.
5
+
6
+ ```js
7
+ json_stringify_stable({b: 2, a: 1, c: {z: 1, y: 2}})
8
+ // '{"a":1,"b":2,"c":{"y":2,"z":1}}'
9
+ ```
package/src/logger.js CHANGED
@@ -5,15 +5,14 @@ const re = /^((?:\[\s*[^\]]*\])*)\s*(.*)$/m;
5
5
 
6
6
  /**
7
7
  * log = logger();
8
- * log.add('hey')
9
- * log.add('[tag] heyhey')
8
+ * log('hey');
9
+ * log('[tag] heyhey');
10
10
  */
11
11
  function logger(options = {})
12
12
  {
13
13
  function out(message) {
14
14
  const v = typeof message === 'string' ? JSON.stringify(message.trim()).slice(1, -1) : JSON.stringify(message);
15
15
  const [, a, m] = v.match(re);
16
- const time = new Date().toJSON();
17
16
  console.log(`${a} ${m}`.trim());
18
17
  };
19
18
  out.spawn = function () {
package/src/logger.md ADDED
@@ -0,0 +1,15 @@
1
+ Creates a minimal console logger. The returned function prints one line per
2
+ call via `console.log`: string messages are trimmed and JSON-escaped (without
3
+ the surrounding quotes), non-strings are printed as JSON. Leading `[tag]`
4
+ prefixes are split off and the whitespace between the tags and the message is
5
+ normalized to a single space.
6
+
7
+ ```js
8
+ const log = logger();
9
+ log('hey'); // hey
10
+ log('[tag] heyhey'); // [tag] heyhey
11
+ log({a: 1}); // {"a":1}
12
+ ```
13
+
14
+ `log.spawn()` is a stub that throws `NotImplemented` — the `redis_poll_*`
15
+ helpers expect a logger whose `spawn()` returns a child logger.
@@ -22,7 +22,7 @@ function make_progress(total)
22
22
  percents: null,
23
23
  add: function (delta = 0) {
24
24
  const now_minus_10sec = Date.now() - 10000;
25
- history.splice(0, history.findLastIndex(v => v.time < now_minus_10sec));
25
+ history.splice(0, history.findLastIndex(v => v.time < now_minus_10sec) + 1);
26
26
  history.push({time: Date.now(), delta});
27
27
  out.done += delta;
28
28
  out.refresh();
@@ -32,16 +32,19 @@ function make_progress(total)
32
32
  },
33
33
  refresh: function () {
34
34
  out.duration = (Date.now() - time0)/1000;
35
- out.percents = !out.total ? null : out.done/out.total;
36
- out.eta = !out.total ? null : (out.total - out.done)/out.rate;
35
+ // `rate` and `eta` are null while unknown (same as before the
36
+ // first add): a zero time window would give rate = Infinity,
37
+ // and a zero rate would give eta = Infinity.
37
38
  if (history.length) {
38
39
  const delta = history.reduce((a,v) => a + v.delta, 0);
39
40
  const time_sec = (Date.now() - history[0].time)/1000;
40
- out.rate = delta/time_sec;
41
+ out.rate = time_sec ? delta/time_sec : null;
41
42
  }
42
43
  else {
43
- out.rate = out.done/out.duration;
44
+ out.rate = out.duration ? out.done/out.duration : null;
44
45
  }
46
+ out.percents = !out.total ? null : out.done/out.total;
47
+ out.eta = (!out.total || !out.rate) ? null : (out.total - out.done)/out.rate;
45
48
  },
46
49
  };
47
50
  return out;
@@ -0,0 +1,20 @@
1
+ Creates a progress tracker for any countable work (bytes copied, jobs done,
2
+ rows created). Pass the snapshot to `format_progress_bytes` /
3
+ `format_progress_kilo` for display.
4
+
5
+ **Fields:** `done`, `total`, `duration` (seconds since creation), `rate`
6
+ (units/second over a sliding 10-second history window), `eta` (seconds),
7
+ `percents` (a fraction, `done/total`). `rate` and `eta` are `null` while
8
+ unknown — before the first `add`, or when they would be `Infinity` (zero
9
+ time window, zero rate); `percents` is `null` when `total` is falsy.
10
+
11
+ **Methods:**
12
+ - `add(delta)` — add `delta` to `done` and recompute
13
+ - `update(done)` — set the absolute `done` value
14
+ - `refresh()` — recompute `duration`/`rate`/`eta`/`percents` without adding
15
+
16
+ ```js
17
+ const p = make_progress(100);
18
+ p.add(10); // p.done → 10, p.percents → 0.1
19
+ p.rate; p.eta; // numbers once enough history exists, null while unknown
20
+ ```
package/src/math_gcd.js CHANGED
@@ -4,7 +4,8 @@ function math_gcd(a, b)
4
4
  if (b) {
5
5
  return math_gcd(b, a % b);
6
6
  }
7
- return a;
7
+ // GCD is canonically non-negative: without abs, gcd(4,-6) would be -2
8
+ return Math.abs(a);
8
9
  }
9
10
 
10
11
  module.exports = math_gcd;
@@ -15,8 +15,9 @@ function mongo_stream_upsert({collection, concurrency = 1})
15
15
  next();
16
16
  },
17
17
  destroy: async function (error, next) {
18
+ // Wait for in-flight bulkWrite operations before tearing down.
18
19
  await wait_while(() => this.running > 0);
19
- next();
20
+ next(error);
20
21
  },
21
22
  write: async function (items, enc, next) {
22
23
  if (errors.length) {
@@ -39,7 +40,10 @@ function mongo_stream_upsert({collection, concurrency = 1})
39
40
  };
40
41
  });
41
42
  this.running++;
42
- Promise.resolve(collection.bulkWrite(operations)).catch(e => errors.push(e)).finally(() => this.running--);
43
+ // Promise.try turns a synchronous throw from bulkWrite into a
44
+ // rejection, so `running` is always decremented; otherwise
45
+ // destroy/final would wait for it forever.
46
+ Promise.try(() => collection.bulkWrite(operations)).catch(e => errors.push(e)).finally(() => this.running--);
43
47
  next();
44
48
  }
45
49
  catch (error) {
@@ -47,11 +51,14 @@ function mongo_stream_upsert({collection, concurrency = 1})
47
51
  }
48
52
  },
49
53
  final: async function (next) {
54
+ // Wait for in-flight bulkWrite operations before checking `errors`,
55
+ // otherwise an error from the last batch(es) would be silently
56
+ // dropped and the stream would finish successfully.
57
+ await wait_while(() => this.running > 0);
50
58
  if (errors.length) {
51
59
  next(errors[0]);
52
60
  return;
53
61
  }
54
- await wait_while(() => this.running > 0);
55
62
  next();
56
63
  },
57
64
  });
@@ -0,0 +1,17 @@
1
+ Object-mode writable that inserts or replaces documents in a mongo collection.
2
+ Every chunk written to it must be an array of documents; each document becomes
3
+ a `replaceOne` upsert keyed by its `_id`, and the batch goes to
4
+ `collection.bulkWrite`.
5
+
6
+ - Up to `concurrency` (default `1`) `bulkWrite` calls run in flight; further
7
+ writes wait until a slot frees up.
8
+ - A `bulkWrite` error is reported on the next write or when the stream
9
+ finishes. `final` and `destroy` wait for all in-flight batches first, so an
10
+ error from the last batch is never silently dropped.
11
+ - A non-array chunk errors the stream (`An array of objects is expected.`).
12
+
13
+ ## Signature
14
+
15
+ ```
16
+ mongo_stream_upsert({collection, concurrency = 1}) → stream.Writable
17
+ ```
@@ -15,8 +15,9 @@ function mongo_stream_write({collection, concurrency = 1})
15
15
  next();
16
16
  },
17
17
  destroy: async function (error, next) {
18
+ // Wait for in-flight bulkWrite operations before tearing down.
18
19
  await wait_while(() => this.running > 0);
19
- next();
20
+ next(error);
20
21
  },
21
22
  write: async function (operations, enc, next) {
22
23
  if (errors.length) {
@@ -30,7 +31,10 @@ function mongo_stream_write({collection, concurrency = 1})
30
31
  }
31
32
  await wait_while(() => this.running >= concurrency);
32
33
  this.running++;
33
- Promise.resolve(collection.bulkWrite(operations)).catch(e => errors.push(e)).finally(() => this.running--);
34
+ // Promise.try turns a synchronous throw from bulkWrite into a
35
+ // rejection, so `running` is always decremented; otherwise
36
+ // destroy/final would wait for it forever.
37
+ Promise.try(() => collection.bulkWrite(operations)).catch(e => errors.push(e)).finally(() => this.running--);
34
38
  next();
35
39
  }
36
40
  catch (error) {
@@ -38,11 +42,14 @@ function mongo_stream_write({collection, concurrency = 1})
38
42
  }
39
43
  },
40
44
  final: async function (next) {
45
+ // Wait for in-flight bulkWrite operations before checking `errors`,
46
+ // otherwise an error from the last batch(es) would be silently
47
+ // dropped and the stream would finish successfully.
48
+ await wait_while(() => this.running > 0);
41
49
  if (errors.length) {
42
50
  next(errors[0]);
43
51
  return;
44
52
  }
45
- await wait_while(() => this.running > 0);
46
53
  next();
47
54
  },
48
55
  });
@@ -0,0 +1,16 @@
1
+ Object-mode writable that feeds batches of `bulkWrite` operations to a mongo
2
+ collection. Every chunk written to it must be an array of operations; it is
3
+ passed to `collection.bulkWrite(operations)` as is.
4
+
5
+ - Up to `concurrency` (default `1`) `bulkWrite` calls run in flight; further
6
+ writes wait until a slot frees up.
7
+ - A `bulkWrite` error is reported on the next write or when the stream
8
+ finishes. `final` and `destroy` wait for all in-flight batches first, so an
9
+ error from the last batch is never silently dropped.
10
+ - A non-array chunk errors the stream (`An array of objects is expected.`).
11
+
12
+ ## Signature
13
+
14
+ ```
15
+ mongo_stream_write({collection, concurrency = 1}) → stream.Writable
16
+ ```
package/src/msval.md ADDED
@@ -0,0 +1,10 @@
1
+ Converts hours, minutes, seconds, and milliseconds to a single millisecond
2
+ value: `msval(h, m, s, ms)` → `h*3600000 + m*60000 + s*1000 + ms`. All four
3
+ arguments are required. Components are not normalized — `msval(0, 90, 0, 0)`
4
+ is fine — and negative components subtract.
5
+
6
+ ```js
7
+ msval(1, 30, 15, 500) // 5415500
8
+ msval(0, 90, 0, 0) // 5400000
9
+ msval(1, 0, 0, -1) // 3599999
10
+ ```
package/src/now_fs.md ADDED
@@ -0,0 +1,7 @@
1
+ Returns the current local time as a filesystem-safe timestamp:
2
+ `YYYYMMDD_HHMMSS` (via `format_date_fs`). Resolution is 1 second — handy for
3
+ naming files, dumps, and backups.
4
+
5
+ ```js
6
+ now_fs() // '20260716_194122'
7
+ ```
@@ -0,0 +1,6 @@
1
+ Returns the current local time in a human-readable form:
2
+ `YYYY/MM/DD HH:MM:SS` (via `format_date_human`).
3
+
4
+ ```js
5
+ now_human() // '2026/07/16 19:41:22'
6
+ ```
@@ -0,0 +1,11 @@
1
+ Fills missing keys of `obj` from `defaults`, mutating and returning `obj`.
2
+ Only keys whose current value is `undefined` (missing or explicitly set) are
3
+ filled; falsy values like `0`, `''`, `false`, and `null` are kept. The
4
+ `defaults` object is never modified.
5
+
6
+ ```js
7
+ object_defaults({a: 1, b: undefined}, {a: 10, b: 20, c: 30})
8
+ // {a: 1, b: 20, c: 30}
9
+ object_defaults({a: 0, b: null}, {a: 5, b: 5})
10
+ // {a: 0, b: null}
11
+ ```
@@ -8,12 +8,13 @@ function object_schema(obj)
8
8
  switch (type) {
9
9
  case const_type.array:
10
10
  return obj.map(object_schema).map(v => json_stringify_stable(v)).sort().filter((v,i,a) => a[i-1] !== v).map(v => JSON.parse(v));
11
- case const_type.object:
11
+ case const_type.object: {
12
12
  const out = {};
13
13
  Object.keys(obj).forEach(function (key) {
14
14
  out[key] = object_schema(obj[key]);
15
15
  });
16
16
  return out;
17
+ }
17
18
  default:
18
19
  return type;
19
20
  }
@@ -0,0 +1,12 @@
1
+ Returns the shape of a value with every leaf replaced by its `gettype` name.
2
+ Objects keep their keys; an array becomes the deduplicated, sorted list of
3
+ its distinct member schemas (compared via `json_stringify_stable`), so
4
+ `[1, 2, 'x']` collapses to `['number', 'string']`. Useful for eyeballing the
5
+ structure of unknown JSON.
6
+
7
+ ```js
8
+ object_schema({name: 'Bob', tags: ['a', 1], meta: {ok: true}})
9
+ // {name: 'string', tags: ['number', 'string'], meta: {ok: 'boolean'}}
10
+ object_schema([{a: 1}, {a: 2}, {a: 'x'}])
11
+ // [{a: 'number'}, {a: 'string'}]
12
+ ```
@@ -0,0 +1,16 @@
1
+ Recursively visits every nested value of an object/array in preorder
2
+ (parents before children), calling `fn(value, path)`. The root itself is not
3
+ visited (`path` would be empty). `path` is an array of keys from the root;
4
+ array elements contribute `'*'` instead of their index. `null` values are
5
+ visited but not descended into. Returns nothing.
6
+
7
+ ```js
8
+ object_walk_preorder({aaa: {foo: 1}, bbb: [10, 20]}, function (value, path) {
9
+ console.log(path.join('/'), value);
10
+ });
11
+ // aaa {foo: 1}
12
+ // aaa/foo 1
13
+ // bbb [10, 20]
14
+ // bbb/* 10
15
+ // bbb/* 20
16
+ ```
package/src/parallel.js CHANGED
@@ -28,11 +28,22 @@ async function parallel({concurrency, spawn, progress})
28
28
  if (failed) {
29
29
  break;
30
30
  }
31
- const w = spawn();
31
+ let w;
32
+ try {
33
+ w = spawn();
34
+ }
35
+ catch (error) {
36
+ // A synchronous throw from spawn must go through fail():
37
+ // when schedule() runs inside a resolved() handler it
38
+ // would otherwise become an unhandled rejection and this
39
+ // promise would never settle (and the timer would leak).
40
+ fail(error);
41
+ return;
42
+ }
32
43
  if (!w) {
33
44
  break;
34
45
  }
35
- const item = Promise.resolve(w).then(resolved, rejected);
46
+ const item = Promise.resolve(w).then(resolved, fail);
36
47
  running.push(item);
37
48
  function resolved() {
38
49
  if (failed) {
@@ -45,20 +56,22 @@ async function parallel({concurrency, spawn, progress})
45
56
  running.splice(i, 1);
46
57
  schedule();
47
58
  }
48
- function rejected(error) {
49
- if (failed) {
50
- return;
51
- }
52
- running.splice(0, running.length);
53
- reject(error);
54
- }
55
59
  }
56
- if (running.length === 0) {
60
+ if (running.length === 0 && !failed) {
57
61
  tick();
58
62
  clearInterval(timer);
59
63
  resolve();
60
64
  }
61
65
  }
66
+ function fail(error) {
67
+ if (failed) {
68
+ return;
69
+ }
70
+ failed = true;
71
+ running.splice(0, running.length);
72
+ clearInterval(timer);
73
+ reject(error);
74
+ }
62
75
  function tick() {
63
76
  if (progress) {
64
77
  progress(running);
@@ -4,11 +4,13 @@ function parse_bytes(bytes)
4
4
  if (typeof bytes !== 'string') {
5
5
  return Number.NaN;
6
6
  }
7
- const m = bytes.toUpperCase().match(/(\d+([.]?\d+)?)(([KMGTP]I?)?B)/);
7
+ // Anchored: trailing garbage ('12KB34') is NaN rather than 12KB. A bare
8
+ // number means bytes; whitespace around and between number and unit is ok
9
+ const m = bytes.toUpperCase().match(/^\s*(\d+(?:[.]\d+)?)\s*((?:[KMGTP]I?)?B)?\s*$/);
8
10
  if (!m) {
9
11
  return Number.NaN;
10
12
  }
11
- return Math.floor(m[1]*Math.pow(1024, sizes.indexOf(m[3].replace('I', ''))));
13
+ return Math.floor(m[1]*Math.pow(1024, sizes.indexOf((m[2] || 'B').replace('I', ''))));
12
14
  }
13
15
 
14
16
  module.exports = parse_bytes;
@@ -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
+ ```