@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.
- package/README.md +29 -3
- package/dist/browser.js +1 -2504
- package/dist/index.html +1 -1
- package/package.json +33 -12
- package/src/HeartbeatServer.js +24 -3
- package/src/HeartbeatServer.md +33 -0
- package/src/array_chunk.js +1 -1
- package/src/array_chunk.md +8 -0
- package/src/array_chunk_balanced.md +14 -0
- package/src/array_fps.md +12 -0
- package/src/array_gcd.js +4 -2
- package/src/array_gcd.md +2 -1
- package/src/array_group.js +3 -3
- package/src/array_group.md +14 -0
- package/src/array_group_map.js +3 -3
- package/src/array_group_map.md +13 -0
- package/src/array_index.js +2 -2
- package/src/array_index.md +9 -0
- package/src/array_lcm.js +4 -2
- package/src/array_lcm.md +2 -1
- package/src/array_max.js +11 -4
- package/src/array_max.md +1 -1
- package/src/array_min.js +11 -4
- package/src/array_min.md +1 -1
- package/src/array_permutations.js +4 -0
- package/src/array_permutations.md +13 -0
- package/src/array_sort.js +7 -2
- package/src/array_sort.md +13 -0
- package/src/array_sort_other.js +8 -5
- package/src/array_sort_other.md +11 -0
- package/src/array_sum.js +2 -2
- package/src/array_sum.md +8 -0
- package/src/array_unique.js +2 -2
- package/src/array_unique_last.js +2 -2
- package/src/assert_valid_expires_at.md +10 -0
- package/src/clamp.md +9 -0
- package/src/cli-apps/watchdog.js +5 -0
- package/src/cli.js +3 -1
- package/src/cli.md +15 -0
- package/src/const_stream.md +7 -0
- package/src/const_type.md +5 -0
- package/src/countdown.js +22 -5
- package/src/countdown.md +21 -0
- package/src/crypto_decrypt_aes256.md +13 -0
- package/src/crypto_encrypt_aes256.md +13 -0
- package/src/crypto_hash_md5.md +10 -0
- package/src/crypto_hash_sha256.md +10 -0
- package/src/crypto_hmac_sha256.md +11 -0
- package/src/date_add_hours.md +8 -0
- package/src/date_add_milliseconds.md +4 -0
- package/src/date_add_minutes.md +4 -0
- package/src/date_add_months.md +13 -0
- package/src/date_add_seconds.md +4 -0
- package/src/date_diff_seconds.md +4 -0
- package/src/date_is_leap_year.md +9 -0
- package/src/escape_content_disposition.js +6 -3
- package/src/escape_content_disposition.md +13 -0
- package/src/escape_regexp.md +7 -0
- package/src/eta.js +7 -1
- package/src/eta.md +18 -0
- package/src/factorize_ms.js +3 -0
- package/src/factorize_ms.md +10 -0
- package/src/fastdl.js +14 -3
- package/src/fcmp_dates.md +12 -0
- package/src/fcmp_default.js +7 -0
- package/src/fcmp_default.md +29 -0
- package/src/fcmp_default_desc.js +4 -7
- package/src/fcmp_default_desc.md +10 -0
- package/src/fcmp_from_spec.md +13 -0
- package/src/fcmp_numbers.md +11 -0
- package/src/fcmp_tuples.md +17 -0
- package/src/fcmp_utf8_bin.md +14 -0
- package/src/fcmp_utf8_ci.md +14 -0
- package/src/fcmp_utf8_cs.md +15 -0
- package/src/fcmp_utf8_natural_bin.js +38 -0
- package/src/fcmp_utf8_natural_bin.md +8 -0
- package/src/fcmp_utf8_natural_ci.md +13 -0
- package/src/fcmp_utf8_natural_cs.md +13 -0
- package/src/fcmpx.md +26 -9
- package/src/filter1_from_spec.js +3 -3
- package/src/format_bytes.md +15 -0
- package/src/format_date.md +7 -0
- package/src/format_date_fs.md +7 -0
- package/src/format_date_human.md +6 -0
- package/src/format_date_ymd.md +6 -0
- package/src/format_error_report.js +3 -3
- package/src/format_error_report.md +12 -0
- package/src/format_kilo.md +13 -0
- package/src/format_ms.js +3 -2
- package/src/format_ms.md +12 -0
- package/src/format_ms2.js +6 -3
- package/src/format_ms2.md +12 -0
- package/src/format_ms3.js +3 -2
- package/src/format_ms3.md +12 -0
- package/src/format_percents.md +10 -0
- package/src/format_progress_bytes.md +13 -0
- package/src/format_progress_kilo.md +10 -0
- package/src/format_seconds.md +11 -0
- package/src/format_thousands.md +9 -0
- package/src/fs_append.md +7 -0
- package/src/fs_append_json.md +8 -0
- package/src/fs_append_utf8.md +6 -0
- package/src/fs_assert_file_exists.md +3 -0
- package/src/fs_assert_file_readable.js +1 -1
- package/src/fs_assert_file_readable.md +3 -0
- package/src/fs_assert_file_socket.md +2 -0
- package/src/fs_copy.md +7 -0
- package/src/fs_copy_excl.md +2 -0
- package/src/fs_exists.md +6 -0
- package/src/fs_fclose.md +5 -0
- package/src/fs_fi.js +13 -1
- package/src/fs_fi.md +11 -0
- package/src/fs_file_md5.md +8 -0
- package/src/fs_find.md +7 -0
- package/src/fs_fopen.md +6 -0
- package/src/fs_fread.js +2 -2
- package/src/fs_fread.md +11 -0
- package/src/fs_fsize.md +2 -0
- package/src/fs_fstat.md +2 -0
- package/src/fs_fwrite.js +2 -2
- package/src/fs_fwrite.md +9 -0
- package/src/fs_list.md +5 -0
- package/src/fs_list_deep.md +5 -0
- package/src/fs_ls.md +2 -0
- package/src/fs_lsr.md +4 -0
- package/src/fs_lstat.md +3 -0
- package/src/fs_mkdir.md +3 -0
- package/src/fs_mkdirp.js +3 -30
- package/src/fs_mkdirp.md +7 -0
- package/src/fs_path_basename.md +2 -0
- package/src/fs_path_dirname.md +2 -0
- package/src/fs_path_extname.md +9 -0
- package/src/fs_path_join.md +2 -0
- package/src/fs_path_relative.md +2 -0
- package/src/fs_path_resolve.md +2 -0
- package/src/fs_path_tempdir.md +2 -0
- package/src/fs_path_untildify.md +8 -0
- package/src/fs_read.md +3 -0
- package/src/fs_read_buffer.md +5 -0
- package/src/fs_read_json.md +6 -0
- package/src/fs_read_lines.md +7 -0
- package/src/fs_read_stream.md +2 -0
- package/src/fs_read_utf8.md +5 -0
- package/src/fs_readdir.md +3 -0
- package/src/fs_readlink.md +6 -0
- package/src/fs_rename.md +6 -0
- package/src/fs_rm.md +4 -0
- package/src/fs_rmdir.md +3 -0
- package/src/fs_rmf.md +2 -0
- package/src/fs_rmrf.md +8 -0
- package/src/fs_sha256.js +16 -0
- package/src/fs_sha256.md +9 -0
- package/src/fs_size.js +2 -2
- package/src/fs_size.md +6 -0
- package/src/fs_stat.md +2 -0
- package/src/fs_tempdir.js +20 -5
- package/src/fs_touch.md +6 -0
- package/src/fs_walk.js +2 -2
- package/src/fs_walk.md +14 -0
- package/src/fs_write.md +3 -0
- package/src/fs_write_json.md +7 -0
- package/src/fs_write_stream.md +2 -0
- package/src/gettype.md +13 -0
- package/src/http_delete.md +11 -0
- package/src/http_get_blob.md +8 -0
- package/src/http_get_buffer.md +10 -0
- package/src/http_get_file.js +8 -8
- package/src/http_get_file.md +14 -0
- package/src/http_get_json.md +10 -0
- package/src/http_get_stream.md +14 -0
- package/src/http_get_stream_range.js +27 -9
- package/src/http_get_stream_range.md +22 -0
- package/src/http_get_utf8.md +9 -0
- package/src/http_head.md +9 -0
- package/src/http_patch_json.md +10 -0
- package/src/http_post_json.md +11 -0
- package/src/http_post_multipart.md +15 -0
- package/src/http_post_urlencoded.md +11 -0
- package/src/http_put_buffer.md +9 -0
- package/src/http_put_file.js +9 -6
- package/src/http_put_file.md +16 -0
- package/src/http_put_json.md +11 -0
- package/src/http_put_utf8.md +9 -0
- package/src/http_range_parse.js +6 -3
- package/src/http_range_parse.md +18 -0
- package/src/http_stream_range.js +35 -22
- package/src/http_stream_range.md +29 -0
- package/src/identity.md +2 -0
- package/src/ignore.md +2 -0
- package/src/json_stringify_safe.md +10 -0
- package/src/json_stringify_stable.md +9 -0
- package/src/logger.js +2 -3
- package/src/logger.md +15 -0
- package/src/make_progress.js +8 -5
- package/src/make_progress.md +20 -0
- package/src/math_gcd.js +2 -1
- package/src/mongo_stream_upsert.js +10 -3
- package/src/mongo_stream_upsert.md +17 -0
- package/src/mongo_stream_write.js +10 -3
- package/src/mongo_stream_write.md +16 -0
- package/src/msval.md +10 -0
- package/src/now_fs.md +7 -0
- package/src/now_human.md +6 -0
- package/src/object_defaults.md +11 -0
- package/src/object_schema.js +2 -1
- package/src/object_schema.md +12 -0
- package/src/object_walk_preorder.md +16 -0
- package/src/parallel.js +23 -10
- package/src/parse_bytes.js +4 -2
- package/src/parse_bytes.md +15 -0
- package/src/parse_hms.js +1 -1
- package/src/parse_http_content_range.js +9 -5
- package/src/parse_http_content_range.md +16 -0
- package/src/perf_end_human.md +10 -0
- package/src/perf_end_ms.md +8 -0
- package/src/perf_measure.md +10 -0
- package/src/perf_measure_human.md +8 -0
- package/src/perf_start.md +9 -0
- package/src/pgid_exists.md +6 -0
- package/src/pgid_kill_grace.js +1 -1
- package/src/pgid_kill_grace.md +15 -0
- package/src/pid_exists.md +5 -0
- package/src/pid_kill_grace.js +1 -1
- package/src/pid_kill_grace.md +14 -0
- package/src/plural.md +13 -0
- package/src/random_hex.md +9 -0
- package/src/random_int.md +8 -0
- package/src/range.js +4 -2
- package/src/range.md +2 -0
- package/src/redis_connect.js +1 -1
- package/src/redis_connect.md +10 -0
- package/src/redis_lshift.md +14 -0
- package/src/redis_poll.js +1 -0
- package/src/redis_poll.md +17 -0
- package/src/redis_poll_zshift_callback_rpush.js +1 -2
- package/src/redis_poll_zshift_callback_rpush.md +17 -0
- package/src/redis_poll_zshift_spawn_rpush.js +2 -1
- package/src/redis_poll_zshift_spawn_rpush.md +20 -0
- package/src/redis_zshift.md +14 -0
- package/src/round.js +7 -8
- package/src/round.md +15 -0
- package/src/sanitize_dash_name.md +13 -0
- package/src/sanitize_filename.md +9 -0
- package/src/sanitize_var_name.md +14 -0
- package/src/sftp_get_file_info.js +2 -2
- package/src/sftp_get_file_info.md +14 -0
- package/src/sftp_get_stream_range.js +32 -22
- package/src/sftp_get_stream_range.md +20 -0
- package/src/shell_curl_progress.js +2 -1
- package/src/shell_curl_progress.md +17 -0
- package/src/shell_lines.js +2 -2
- package/src/shell_lines.md +1 -1
- package/src/shell_spawn.js +7 -0
- package/src/shell_ytdlp_progress.js +2 -1
- package/src/shell_ytdlp_progress.md +17 -0
- package/src/str_camel_to_snake.md +11 -0
- package/src/str_parse_kv.md +11 -0
- package/src/stream_curl_progress.md +14 -0
- package/src/stream_data_ln.js +7 -1
- package/src/stream_discard.md +8 -0
- package/src/stream_group.md +3 -0
- package/src/stream_gunzip.md +7 -0
- package/src/stream_hash.md +10 -0
- package/src/stream_map.js +8 -1
- package/src/stream_map_parallel.js +9 -1
- package/src/stream_md5.md +9 -0
- package/src/stream_multiplex.js +11 -0
- package/src/stream_multiplex.md +16 -0
- package/src/stream_through.md +3 -0
- package/src/stream_transform.md +2 -0
- package/src/stream_xml_analyze.js +9 -1
- package/src/stream_xml_parse.js +9 -1
- package/src/stream_ytdlp_progress.js +1 -1
- package/src/stream_ytdlp_progress.md +18 -0
- package/src/strtr.js +15 -5
- package/src/strtr.md +11 -0
- package/src/urlclean.js +9 -2
- package/src/urlmod.js +22 -11
- package/src/urlmod.md +15 -3
- package/src/urlparts.md +17 -0
- package/src/wait_while.js +1 -1
- package/src/wait_while.md +1 -1
- package/src/HeartbeatServer.test.js +0 -31
- package/src/array_chunk.test.js +0 -24
- package/src/array_chunk_balanced.test.js +0 -37
- package/src/array_fps.test.js +0 -72
- package/src/array_gcd.test.js +0 -10
- package/src/array_index.test.js +0 -14
- package/src/array_lcm.test.js +0 -12
- package/src/array_max.test.js +0 -20
- package/src/array_min.test.js +0 -20
- package/src/array_permutations.test.js +0 -20
- package/src/array_sort.test.js +0 -85
- package/src/array_sort_other.test.js +0 -9
- package/src/array_sum.test.js +0 -11
- package/src/cli-apps/watchdog.d/connect_close.js +0 -17
- package/src/cli-apps/watchdog.d/connect_close2.js +0 -20
- package/src/cli-apps/watchdog.d/connect_close3.js +0 -25
- package/src/cli-apps/watchdog.d/connect_no_ping.js +0 -13
- package/src/cli-apps/watchdog.d/early_exit.js +0 -7
- package/src/cli-apps/watchdog.d/flood.js +0 -13
- package/src/cli-apps/watchdog.d/flood2.js +0 -15
- package/src/cli-apps/watchdog.d/hang.js +0 -8
- package/src/cli-apps/watchdog.d/huge_delay.js +0 -18
- package/src/cli-apps/watchdog.d/ignore_sigterm.js +0 -29
- package/src/cli-apps/watchdog.d/late_heartbeat.js +0 -21
- package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +0 -16
- package/src/cli-apps/watchdog.d/my-happy-path.js +0 -16
- package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +0 -22
- package/src/cli-apps/watchdog.d/my-never-ping.js +0 -15
- package/src/cli-apps/watchdog.d/no_heartbeat.js +0 -5
- package/src/cli-apps/watchdog.d/ok.js +0 -17
- package/src/cli-apps/watchdog.d/quick-exit-after-2-pings.js +0 -15
- package/src/cli-apps/watchdog.d/quick-hang-after-1-ping.js +0 -15
- package/src/cli-apps/watchdog.d/spawn_children.js +0 -14
- package/src/cli-apps/watchdog.test.js +0 -48
- package/src/crypto_encrypt_aes256.test.js +0 -10
- package/src/crypto_hash_sha256.test.js +0 -10
- package/src/date_add_months.test.js +0 -40
- package/src/date_is_leap_year.test.js +0 -14
- package/src/escape_content_disposition.test.js +0 -22
- package/src/fcmp_from_spec.test.js +0 -91
- package/src/fcmpx.test.js +0 -173
- package/src/filter1_from_spec.test.js +0 -31
- package/src/format_bytes.test.js +0 -22
- package/src/format_error_report.d/src/1.js +0 -26
- package/src/format_error_report.d/src/2.js +0 -29
- package/src/format_kilo.test.js +0 -25
- package/src/format_ms.test.js +0 -33
- package/src/format_ms2.test.js +0 -33
- package/src/format_ms3.test.js +0 -33
- package/src/format_percents.test.js +0 -22
- package/src/format_progress_bytes.test.js +0 -19
- package/src/format_progress_kilo.test.js +0 -19
- package/src/format_seconds.test.js +0 -42
- package/src/format_thousands.test.js +0 -28
- package/src/fs_file_md5.test.js +0 -11
- package/src/fs_mkdirp.test.js +0 -24
- package/src/fs_path_extname.test.js +0 -22
- package/src/gettype.test.js +0 -27
- package/src/object_schema.test.js +0 -15
- package/src/object_walk_preorder.test.js +0 -17
- package/src/parallel.test.js +0 -11
- package/src/parse_bytes.test.js +0 -21
- package/src/parse_hms.test.js +0 -32
- package/src/parse_http_content_range.test.js +0 -14
- package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +0 -33
- package/src/pid_kill_grace.d/ignore-sigterm.js +0 -22
- package/src/pid_kill_grace.test.js +0 -63
- package/src/ping_socket.test.js +0 -58
- package/src/plural.test.js +0 -20
- package/src/range.test.js +0 -41
- package/src/redis_lshift.test.js +0 -57
- package/src/redis_zshift.test.js +0 -58
- package/src/round.test.js +0 -56
- package/src/sanitize_dash_name.test.js +0 -59
- package/src/sanitize_var_name.test.js +0 -39
- package/src/shell_spawn.test.js +0 -30
- package/src/str_camel_to_snake.test.js +0 -17
- package/src/stream_curl_progress.d/basic +0 -3
- package/src/stream_curl_progress.d/basic.json +0 -42
- package/src/stream_curl_progress.test.js +0 -17
- package/src/stream_data_ln.test.js +0 -24
- package/src/stream_map_parallel.test.js +0 -26
- package/src/stream_multiplex.test.js +0 -67
- package/src/stream_strpbrk.test.js +0 -29
- package/src/urlclean.test.js +0 -18
- package/src/urlmod.test.js +0 -40
- 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
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
function parse_http_content_range(value)
|
|
2
2
|
{
|
|
3
3
|
// Content-Range: bytes 262144000-272629759/5037662208
|
|
4
|
-
|
|
5
|
-
|
|
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,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.
|
package/src/pgid_kill_grace.js
CHANGED
|
@@ -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.
|
package/src/pid_kill_grace.js
CHANGED
|
@@ -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
|
+
```
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/src/redis_connect.js
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
-
|
|
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
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
23
|
-
|
|
22
|
+
try {
|
|
23
|
+
user_friendly_status('Asking for an sftp service...');
|
|
24
|
+
const sftp = await waitcb(cb => conn.sftp(cb));
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
user_friendly_status('Requesting file info...');
|
|
27
|
+
const stat = await waitcb(cb => sftp.stat(pathname, cb));
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (first < 0 || last >= stat.size) {
|
|
30
|
+
throw new Error(`Invalid range: [first=${first}][last=${last}]`);
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|