@vbarbarosh/node-helpers 3.66.1 → 3.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/HeartbeatServer.js +99 -0
- package/src/array_group.js +11 -7
- package/src/array_index.js +3 -1
- package/src/array_max.js +8 -14
- package/src/array_max.test.js +8 -0
- package/src/array_min.js +8 -14
- package/src/array_min.test.js +8 -0
- package/src/array_sort.js +21 -12
- package/src/array_sort.test.js +78 -3
- package/src/array_sort_other.js +2 -2
- package/src/array_unique.js +9 -3
- package/src/array_unique_last.js +7 -14
- package/src/countdown.js +1 -1
- package/src/fcmp_default.js +12 -0
- package/src/fcmp_default_desc.js +12 -0
- package/src/fcmp_numbers.js +1 -1
- package/src/fcmp_tuples.js +15 -0
- package/src/fcmp_utf8_bin.js +13 -0
- package/src/fcmp_utf8_ci.js +7 -0
- package/src/fcmp_utf8_cs.js +7 -0
- package/src/fcmp_utf8_natural_ci.js +7 -0
- package/src/fcmp_utf8_natural_cs.js +7 -0
- package/src/fcmpx.js +120 -0
- package/src/fcmpx.test.js +158 -0
- package/src/format_date_fs.js +13 -0
- package/src/format_date_human.js +13 -0
- package/src/format_progress_bytes.js +6 -6
- package/src/format_progress_kilo.js +6 -6
- package/src/fs_list.js +16 -0
- package/src/fs_list_deep.js +20 -0
- package/src/fs_ls.js +3 -0
- package/src/fs_path_extname.js +14 -0
- package/src/fs_path_extname.test.js +22 -0
- package/src/now_fs.js +8 -0
- package/src/now_human.js +8 -0
- package/src/parallel.js +3 -3
- package/src/pid_exists.js +12 -0
- package/src/pid_kill_grace.js +28 -0
- package/src/ping_socket.js +32 -0
- package/src/plural.js +8 -2
- package/src/plural.test.js +5 -3
- package/src/sanitize_var_name.js +2 -2
- package/src/{camel_to_snake.js → str_camel_to_snake.js} +2 -2
- package/src/{camel_to_snake.test.js → str_camel_to_snake.test.js} +3 -3
- package/src/str_parse_kv.js +10 -0
- package/src/fcmp_strings.js +0 -6
- /package/src/{stream-parse-csv.js → stream_parse_csv.js} +0 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@vbarbarosh/node-helpers",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.67.0",
|
|
6
6
|
"description": "A set of helpers for JavaScript/Node.js",
|
|
7
7
|
"unpkg": "dist/browser.js",
|
|
8
8
|
"jsdelivr": "dist/browser.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"ssh2": "^1.16.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@vbarbarosh/tree": "^0.5.0",
|
|
45
46
|
"copy-webpack-plugin": "^12.0.2",
|
|
46
47
|
"express": "^4.21.2",
|
|
47
48
|
"mocha": "^11.0.1",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"redis-v3": "npm:redis@3",
|
|
51
52
|
"redis-v4": "npm:redis@4",
|
|
52
53
|
"throttle": "^1.0.3",
|
|
54
|
+
"tree-kill": "^1.2.2",
|
|
53
55
|
"webpack": "^5.98.0",
|
|
54
56
|
"webpack-cli": "^6.0.1"
|
|
55
57
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const Promise = require('bluebird');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fs_path_join = require('./fs_path_join');
|
|
5
|
+
const fs_rmf = require('./fs_rmf');
|
|
6
|
+
const net = require('net');
|
|
7
|
+
const now_fs = require('./now_fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
class HeartbeatServer extends EventEmitter
|
|
11
|
+
{
|
|
12
|
+
#socket_path;
|
|
13
|
+
#last_ping = Date.now();
|
|
14
|
+
#interval_ms;
|
|
15
|
+
#promise;
|
|
16
|
+
#reject;
|
|
17
|
+
#timer;
|
|
18
|
+
#server;
|
|
19
|
+
|
|
20
|
+
constructor(interval_ms = 5000) {
|
|
21
|
+
super();
|
|
22
|
+
this.#interval_ms = interval_ms;
|
|
23
|
+
this.#init();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#init() {
|
|
27
|
+
const _this = this;
|
|
28
|
+
|
|
29
|
+
this.#timer = setInterval(() => this.#tick(), this.#interval_ms);
|
|
30
|
+
|
|
31
|
+
// Put socket on tmpfs to avoid slow disks.
|
|
32
|
+
this.#socket_path = fs.existsSync('/dev/shm')
|
|
33
|
+
? fs_path_join('/dev/shm', `watchdog-${process.pid}-${now_fs()}.sock`)
|
|
34
|
+
: fs_path_join(os.tmpdir(), `watchdog-${process.pid}-${now_fs()}.sock`);
|
|
35
|
+
|
|
36
|
+
this.#promise = new Promise((resolve, reject) => {
|
|
37
|
+
this.#reject = reject;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.#server = net.createServer(function (socket) {
|
|
41
|
+
socket.setEncoding('utf8');
|
|
42
|
+
socket.on('error', function (error) {
|
|
43
|
+
_this.#reject(new Error(`Client Socket Failed: ${error.message}`));
|
|
44
|
+
});
|
|
45
|
+
socket.on('end', function () {
|
|
46
|
+
_this.#last_ping = Date.now();
|
|
47
|
+
_this.emit('heartbeat');
|
|
48
|
+
});
|
|
49
|
+
socket.on('data', function () {
|
|
50
|
+
// Not really interested in any data
|
|
51
|
+
});
|
|
52
|
+
socket.end('OK');
|
|
53
|
+
});
|
|
54
|
+
this.#server.on('error', function (error) {
|
|
55
|
+
_this.#reject(new Error(`Server Failed: ${error.message}`));
|
|
56
|
+
});
|
|
57
|
+
this.#server.on('close', function () {
|
|
58
|
+
_this.#reject(new Error(`Server Closed`));
|
|
59
|
+
});
|
|
60
|
+
this.#server.listen(this.#socket_path, function () {
|
|
61
|
+
// started = true;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#tick() {
|
|
66
|
+
if (this.#last_ping + this.#interval_ms < Date.now()) {
|
|
67
|
+
clearInterval(this.#timer);
|
|
68
|
+
this.#timer = null;
|
|
69
|
+
this.#reject(new Error(`No heartbeat for the last ${this.#interval_ms}ms`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get socket_path() {
|
|
74
|
+
return this.#socket_path;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
promise() {
|
|
78
|
+
return this.#promise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async dispose() {
|
|
82
|
+
if (this.#timer) {
|
|
83
|
+
clearInterval(this.#timer);
|
|
84
|
+
this.#timer = null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const _this = this;
|
|
88
|
+
await new Promise(function (resolve, reject) {
|
|
89
|
+
_this.#server.removeAllListeners();
|
|
90
|
+
_this.#server.close(error => error ? reject(error) : resolve());
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await fs_rmf(this.#socket_path);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = HeartbeatServer;
|
package/src/array_group.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
const array_group_map = require('./array_group_map');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Group items by common key and return an array of groups.
|
|
2
|
+
* Group items by a common key and return an array of groups.
|
|
5
3
|
*
|
|
6
|
-
* @
|
|
7
|
-
* @param fn
|
|
8
|
-
* @returns {unknown[]}
|
|
4
|
+
* @alternative Map.groupBy(items, fn)
|
|
9
5
|
*/
|
|
10
6
|
function array_group(array, fn)
|
|
11
7
|
{
|
|
12
|
-
|
|
8
|
+
const map = new Map();
|
|
9
|
+
array.forEach(function (item) {
|
|
10
|
+
const key = fn(item);
|
|
11
|
+
if (!map.has(key)) {
|
|
12
|
+
map.set(key, {key, items: []});
|
|
13
|
+
}
|
|
14
|
+
map.get(key).items.push(item);
|
|
15
|
+
});
|
|
16
|
+
return map.values().toArray();
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
module.exports = array_group;
|
package/src/array_index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const identity = require('./identity');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Usage:
|
|
3
5
|
* array_index(items, v => v.name)
|
|
@@ -5,7 +7,7 @@
|
|
|
5
7
|
* Seems, there is a native way to do it:
|
|
6
8
|
* Object.fromEntries(items.map(v => [v.name, v]));
|
|
7
9
|
*/
|
|
8
|
-
function array_index(array, fn)
|
|
10
|
+
function array_index(array, fn = identity)
|
|
9
11
|
{
|
|
10
12
|
const out = {};
|
|
11
13
|
array.forEach(v => out[fn(v)] = v);
|
package/src/array_max.js
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
+
const identity = require('./identity');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param array
|
|
5
|
-
* @param fn
|
|
4
|
+
* Returns the first element in the array with the maximal weight
|
|
6
5
|
*/
|
|
7
6
|
function array_max(array, fn = identity)
|
|
8
7
|
{
|
|
9
8
|
let out = null;
|
|
10
|
-
let
|
|
11
|
-
array.forEach(function (item) {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
9
|
+
let max = null;
|
|
10
|
+
array.forEach(function (item, i) {
|
|
11
|
+
const weight = fn(item);
|
|
12
|
+
if (i === 0 || max < weight) {
|
|
13
|
+
max = weight;
|
|
14
14
|
out = item;
|
|
15
|
-
out_value = item_value;
|
|
16
15
|
}
|
|
17
16
|
});
|
|
18
17
|
return out;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
function identity(value)
|
|
22
|
-
{
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
20
|
module.exports = array_max;
|
package/src/array_max.test.js
CHANGED
|
@@ -9,4 +9,12 @@ describe('array_max', function () {
|
|
|
9
9
|
assert.deepStrictEqual(array_max([1, 1, 2, 2, 3, 3, 1]), 3);
|
|
10
10
|
assert.deepStrictEqual(array_max([{x:1},{x:2}], v => v.x), {x: 2});
|
|
11
11
|
});
|
|
12
|
+
it('should handle strings', function () {
|
|
13
|
+
const items = [
|
|
14
|
+
{id: 1, time: '2025-08-08T05:03:13+00:00'},
|
|
15
|
+
{id: 2, time: '2025-08-07T05:03:13+00:00'},
|
|
16
|
+
{id: 3, time: '2025-08-06T05:03:13+00:00'},
|
|
17
|
+
];
|
|
18
|
+
assert.deepStrictEqual(array_max(items, v => v.time), items[0]);
|
|
19
|
+
});
|
|
12
20
|
});
|
package/src/array_min.js
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
+
const identity = require('./identity');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param array
|
|
5
|
-
* @param fn
|
|
4
|
+
* Returns the first element in the array with the minimal weight
|
|
6
5
|
*/
|
|
7
6
|
function array_min(array, fn = identity)
|
|
8
7
|
{
|
|
9
8
|
let out = null;
|
|
10
|
-
let
|
|
11
|
-
array.forEach(function (item) {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
9
|
+
let min = null;
|
|
10
|
+
array.forEach(function (item, i) {
|
|
11
|
+
const weight = fn(item);
|
|
12
|
+
if (i === 0 || min > weight) {
|
|
13
|
+
min = weight;
|
|
14
14
|
out = item;
|
|
15
|
-
out_value = item_value;
|
|
16
15
|
}
|
|
17
16
|
});
|
|
18
17
|
return out;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
function identity(value)
|
|
22
|
-
{
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
20
|
module.exports = array_min;
|
package/src/array_min.test.js
CHANGED
|
@@ -9,4 +9,12 @@ describe('array_min', function () {
|
|
|
9
9
|
assert.deepStrictEqual(array_min([1, 1, 2, 2, 3, 3, 1]), 1);
|
|
10
10
|
assert.deepStrictEqual(array_min([{x:2},{x:1}], v => v.x), {x: 1});
|
|
11
11
|
});
|
|
12
|
+
it('should handle strings', function () {
|
|
13
|
+
const items = [
|
|
14
|
+
{id: 1, time: '2025-08-08T05:03:13+00:00'},
|
|
15
|
+
{id: 2, time: '2025-08-07T05:03:13+00:00'},
|
|
16
|
+
{id: 3, time: '2025-08-06T05:03:13+00:00'},
|
|
17
|
+
];
|
|
18
|
+
assert.deepStrictEqual(array_min(items, v => v.time), items[2]);
|
|
19
|
+
});
|
|
12
20
|
});
|
package/src/array_sort.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fcmp_default = require('./fcmp_default');
|
|
2
|
+
const fcmp_tuples = require('./fcmp_tuples');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Sorts an array in place by the result of applying `fn` to each item,
|
|
6
|
+
* using `fcmp` to compare the results.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param fcmp
|
|
9
|
-
* @returns {*}
|
|
8
|
+
* array_sort(items, v => [v.name])
|
|
9
|
+
* array_sort(items, v => [v.age, v.name])
|
|
10
10
|
*/
|
|
11
|
-
function array_sort(array,
|
|
11
|
+
function array_sort(array, mapper, fcmp = fcmp_default)
|
|
12
12
|
{
|
|
13
|
+
const keys = new Map();
|
|
13
14
|
return array.sort(function (a, b) {
|
|
14
|
-
|
|
15
|
+
if (!keys.has(a)) {
|
|
16
|
+
keys.set(a, mapper(a));
|
|
17
|
+
}
|
|
18
|
+
if (!keys.has(b)) {
|
|
19
|
+
keys.set(b, mapper(b));
|
|
20
|
+
}
|
|
21
|
+
return fcmp_tuples(keys.get(a), keys.get(b), fcmp);
|
|
15
22
|
});
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
function identity
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
// function array_sort(array, fn = identity, fcmp = fcmp_default)
|
|
26
|
+
// {
|
|
27
|
+
// return array.sort(function (a, b) {
|
|
28
|
+
// return fcmp(fn(a), fn(b));
|
|
29
|
+
// });
|
|
30
|
+
// }
|
|
22
31
|
|
|
23
32
|
module.exports = array_sort;
|
package/src/array_sort.test.js
CHANGED
|
@@ -1,10 +1,85 @@
|
|
|
1
1
|
const array_sort = require('./array_sort');
|
|
2
2
|
const assert = require('assert');
|
|
3
|
+
const fs_path_extname = require('./fs_path_extname');
|
|
3
4
|
|
|
4
5
|
describe('array_sort', function () {
|
|
5
6
|
it('should handle basic input', function () {
|
|
6
|
-
assert.deepStrictEqual(array_sort([5,4,3,2,1]), [1,2,3,4,5]);
|
|
7
|
-
assert.deepStrictEqual(array_sort([5,4,3,2,1], v => v), [1,2,3,4,5]);
|
|
8
|
-
assert.deepStrictEqual(array_sort([{id:5},{id:4},{id:3},{id:2},{id:1}], v => v.id), [{id:1},{id:2},{id:3},{id:4},{id:5}]);
|
|
7
|
+
assert.deepStrictEqual(array_sort([5,4,3,2,1], v => [v]), [1,2,3,4,5]);
|
|
8
|
+
assert.deepStrictEqual(array_sort([5,4,3,2,1], v => [v]), [1,2,3,4,5]);
|
|
9
|
+
assert.deepStrictEqual(array_sort([{id:5},{id:4},{id:3},{id:2},{id:1}], v => [v.id]), [{id:1},{id:2},{id:3},{id:4},{id:5}]);
|
|
10
|
+
});
|
|
11
|
+
it('should handle sort by priorities', function () {
|
|
12
|
+
assert.deepStrictEqual(array_sort([1,2,3,4,5], v => [[5].indexOf(v) >>> 0]), [5,1,2,3,4]);
|
|
13
|
+
assert.deepStrictEqual(array_sort([{id:1},{id:2},{id:3},{id:4},{id:5}], v => [[5].indexOf(v.id) >>> 0]), [{id:5},{id:1},{id:2},{id:3},{id:4}]);
|
|
14
|
+
});
|
|
15
|
+
it('should handle sort by priorities #2', function () {
|
|
16
|
+
const files = [
|
|
17
|
+
{name: 'README.md', size: 1024, type: 'file'},
|
|
18
|
+
{name: 'package.json', size: 860, type: 'file'},
|
|
19
|
+
{name: 'src', size: 0, type: 'dir'},
|
|
20
|
+
{name: 'src/index.js', size: 2048, type: 'file'},
|
|
21
|
+
{name: 'src/utils', size: 0, type: 'dir'},
|
|
22
|
+
{name: 'src/utils/helpers.js', size: 1536, type: 'file'},
|
|
23
|
+
{name: 'node_modules', size: 0, type: 'dir'},
|
|
24
|
+
{name: 'LICENSE', size: 1078, type: 'file'},
|
|
25
|
+
];
|
|
26
|
+
const expected = [
|
|
27
|
+
{name: 'node_modules', size: 0, type: 'dir'},
|
|
28
|
+
{name: 'src', size: 0, type: 'dir'},
|
|
29
|
+
{name: 'src/utils', size: 0, type: 'dir'},
|
|
30
|
+
{name: 'LICENSE', size: 1078, type: 'file'},
|
|
31
|
+
{name: 'README.md', size: 1024, type: 'file'},
|
|
32
|
+
{name: 'package.json', size: 860, type: 'file'},
|
|
33
|
+
{name: 'src/index.js', size: 2048, type: 'file'},
|
|
34
|
+
{name: 'src/utils/helpers.js', size: 1536, type: 'file'},
|
|
35
|
+
];
|
|
36
|
+
const actual = array_sort(files, v => [pri_start(v.type, ['dir']), v.name]);
|
|
37
|
+
assert.deepStrictEqual(actual, expected);
|
|
38
|
+
});
|
|
39
|
+
it('sort by name, directory first; .js, then .txt, then all the reset', function () {
|
|
40
|
+
const files = [
|
|
41
|
+
{name: 'docs', type: 'dir', size: 0},
|
|
42
|
+
{name: 'zeta.txt', type: 'file', size: 1200},
|
|
43
|
+
{name: 'alpha.js', type: 'file', size: 1500},
|
|
44
|
+
{name: 'src', type: 'dir', size: 0},
|
|
45
|
+
{name: 'readme.txt', type: 'file', size: 800},
|
|
46
|
+
{name: 'beta.js', type: 'file', size: 1600},
|
|
47
|
+
{name: 'build', type: 'dir', size: 0},
|
|
48
|
+
{name: 'build.foo.js', type: 'dir', size: 0},
|
|
49
|
+
{name: 'build.foo.md', type: 'dir', size: 0},
|
|
50
|
+
{name: 'build.foo.txt', type: 'dir', size: 0},
|
|
51
|
+
{name: 'notes.md', type: 'file', size: 400},
|
|
52
|
+
{name: 'zeta.js', type: 'file', size: 1000},
|
|
53
|
+
{name: 'config', type: 'dir', size: 0},
|
|
54
|
+
];
|
|
55
|
+
const expected = [
|
|
56
|
+
{name: 'build.foo.js', type: 'dir', size: 0},
|
|
57
|
+
{name: 'build.foo.txt', type: 'dir', size: 0},
|
|
58
|
+
{name: 'build', type: 'dir', size: 0},
|
|
59
|
+
{name: 'build.foo.md', type: 'dir', size: 0},
|
|
60
|
+
{name: 'config', type: 'dir', size: 0},
|
|
61
|
+
{name: 'docs', type: 'dir', size: 0},
|
|
62
|
+
{name: 'src', type: 'dir', size: 0},
|
|
63
|
+
{name: 'alpha.js', type: 'file', size: 1500},
|
|
64
|
+
{name: 'beta.js', type: 'file', size: 1600},
|
|
65
|
+
{name: 'zeta.js', type: 'file', size: 1000},
|
|
66
|
+
{name: 'readme.txt', type: 'file', size: 800},
|
|
67
|
+
{name: 'zeta.txt', type: 'file', size: 1200},
|
|
68
|
+
{name: 'notes.md', type: 'file', size: 400},
|
|
69
|
+
];
|
|
70
|
+
array_sort(files, v => [pri_start(v.type, ['dir']), pri_start(fs_path_extname(v.name), ['.js', '.txt']), v.name])
|
|
71
|
+
assert.deepStrictEqual(files, expected);
|
|
9
72
|
});
|
|
10
73
|
});
|
|
74
|
+
|
|
75
|
+
// priorities.indexOf(v) >>> 0
|
|
76
|
+
function pri_start(value, array)
|
|
77
|
+
{
|
|
78
|
+
const i = array.indexOf(value);
|
|
79
|
+
return i === -1 ? array.length : i;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pri_end(value, array)
|
|
83
|
+
{
|
|
84
|
+
return array.indexOf(value);
|
|
85
|
+
}
|
package/src/array_sort_other.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fcmp_default = require('./fcmp_default');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Sort items in an `array` at the same order as in `other`. Values which
|
|
@@ -10,7 +10,7 @@ const fcmp_strings = require('./fcmp_strings');
|
|
|
10
10
|
* @param fcmp
|
|
11
11
|
* @returns {*}
|
|
12
12
|
*/
|
|
13
|
-
function array_sort_other(array, fn, other, fcmp =
|
|
13
|
+
function array_sort_other(array, fn, other, fcmp = fcmp_default)
|
|
14
14
|
{
|
|
15
15
|
const other_map = {};
|
|
16
16
|
other.forEach((v,i) => other_map[v] = i + 1);
|
package/src/array_unique.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
const identity = require('./identity');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return unique values; if a value occurs multiple times, keep the first one.
|
|
5
|
+
*/
|
|
6
|
+
function array_unique(values, fn = identity)
|
|
2
7
|
{
|
|
3
8
|
const set = new Set();
|
|
4
9
|
return values.filter(function (item) {
|
|
5
|
-
|
|
10
|
+
const key = fn(item);
|
|
11
|
+
if (set.has(key)) {
|
|
6
12
|
return false;
|
|
7
13
|
}
|
|
8
|
-
set.add(
|
|
14
|
+
set.add(key);
|
|
9
15
|
return true;
|
|
10
16
|
});
|
|
11
17
|
}
|
package/src/array_unique_last.js
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
|
+
const identity = require('./identity');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Return
|
|
3
|
-
*
|
|
4
|
-
* @param array
|
|
5
|
-
* @param fn
|
|
6
|
-
* @returns {*[]}
|
|
4
|
+
* Return unique values; if a value occurs multiple times, keep the last one.
|
|
7
5
|
*/
|
|
8
6
|
function array_unique_last(array, fn = identity)
|
|
9
7
|
{
|
|
10
8
|
const out = [];
|
|
11
|
-
const
|
|
9
|
+
const set = new Set();
|
|
12
10
|
for (let i = array.length; --i >= 0; ) {
|
|
13
11
|
const item = array[i];
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
|
|
12
|
+
const key = fn(item);
|
|
13
|
+
if (!set.has(key)) {
|
|
14
|
+
set.add(key);
|
|
17
15
|
out.push(item);
|
|
18
16
|
}
|
|
19
17
|
}
|
|
20
18
|
return out.reverse();
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
function identity(value)
|
|
24
|
-
{
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
module.exports = array_unique_last;
|
package/src/countdown.js
CHANGED
|
@@ -13,7 +13,7 @@ function countdown(ctx)
|
|
|
13
13
|
let timer = null;
|
|
14
14
|
return new Promise(function (resolve, reject) {
|
|
15
15
|
ctx.value = ctx.fn ? Promise.method(ctx.fn).call() : Promise.resolve(ctx.value);
|
|
16
|
-
ctx.timeout = ctx.timeout || 0;
|
|
16
|
+
ctx.timeout = ctx.timeout || 0; // ⚠️ Rename to timeout_ms
|
|
17
17
|
ctx.time_now = new Date();
|
|
18
18
|
ctx.time_begin = new Date();
|
|
19
19
|
ctx.time_end = Date.now() + ctx.timeout;
|
package/src/fcmp_numbers.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const fcmp_default = require('./fcmp_default');
|
|
2
|
+
|
|
3
|
+
function fcmp_tuples(a, b, fcmp = fcmp_default)
|
|
4
|
+
{
|
|
5
|
+
const end = Math.min(a.length, b.length);
|
|
6
|
+
for (let i = 0; i < end; ++i) {
|
|
7
|
+
const tmp = fcmp(a[i], b[i]);
|
|
8
|
+
if (tmp) {
|
|
9
|
+
return tmp;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = fcmp_tuples;
|
package/src/fcmpx.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fcmp_default = require('./fcmp_default');
|
|
2
|
+
const fcmp_default_desc = require('./fcmp_default_desc');
|
|
3
|
+
const identity = require('./identity');
|
|
4
|
+
|
|
5
|
+
const MISSING = Symbol('fcmpx.missing');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Comparator builder using compact expressions.
|
|
9
|
+
*
|
|
10
|
+
* fcmp expression — Creates an `fcmp` function from an expression, suitable for use with `[].sort()`.
|
|
11
|
+
*
|
|
12
|
+
* fcmpx('user.email')
|
|
13
|
+
* fcmpx('-user.age,user.email')
|
|
14
|
+
* fcmpx(v => v.user.email)
|
|
15
|
+
* fcmpx(['user.age', 'user.email'])
|
|
16
|
+
* fcmpx(['-user.age', 'user.email'])
|
|
17
|
+
*/
|
|
18
|
+
function fcmpx(expr)
|
|
19
|
+
{
|
|
20
|
+
if (typeof expr === 'string') {
|
|
21
|
+
if (expr.includes(',')) {
|
|
22
|
+
return fcmpx(expr.split(','));
|
|
23
|
+
}
|
|
24
|
+
return fcmpx_compile(expr);
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(expr)) {
|
|
27
|
+
return fcmpx_compile(expr);
|
|
28
|
+
}
|
|
29
|
+
const fcmp_items = expr.map(fcmpx_parse);
|
|
30
|
+
return function (a, b) {
|
|
31
|
+
const tuple1 = fcmp_items.map(v => v.read(a));
|
|
32
|
+
const tuple2 = fcmp_items.map(v => v.read(b));
|
|
33
|
+
for (let i = 0; i < fcmp_items.length; ++i) {
|
|
34
|
+
const tmp = fcmp_items[i].fcmp(tuple1[i], tuple2[i]);
|
|
35
|
+
if (tmp) {
|
|
36
|
+
return tmp;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fcmpx_compile(expr)
|
|
44
|
+
{
|
|
45
|
+
const {read, fcmp} = fcmpx_parse(expr);
|
|
46
|
+
return function (a, b) {
|
|
47
|
+
return fcmp(read(a), read(b));
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fcmpx_parse(expr)
|
|
52
|
+
{
|
|
53
|
+
if (typeof expr === 'function') {
|
|
54
|
+
return {read: fcmpx_compile_read(expr), fcmp: fcmp_wrapper(fcmp_default)};
|
|
55
|
+
}
|
|
56
|
+
if (typeof expr === 'string') {
|
|
57
|
+
const desc = expr.startsWith('-');
|
|
58
|
+
const read = desc ? fcmpx_compile_read(expr.slice(1)) : fcmpx_compile_read(expr);
|
|
59
|
+
const fcmp = fcmp_wrapper(desc ? fcmp_default_desc : fcmp_default);
|
|
60
|
+
return {read, fcmp};
|
|
61
|
+
}
|
|
62
|
+
const fcmp_user = expr.fcmp;
|
|
63
|
+
const read = fcmpx_compile_read(expr.read);
|
|
64
|
+
const desc = expr.desc ?? false;
|
|
65
|
+
const fcmp = fcmp_wrapper(desc ? (fcmp_user ? (b, a) => fcmp_user(a, b) : fcmp_default_desc) : (fcmp_user ?? fcmp_default));
|
|
66
|
+
if (Array.isArray(expr.top)) {
|
|
67
|
+
const priority = new Map(expr.top.map((v, i) => [v, i]));
|
|
68
|
+
return {read: v => priority.get(read(v)) ?? priority.size, fcmp};
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(expr.bottom)) {
|
|
71
|
+
const priority = new Map(expr.bottom.map((v, i) => [v, i]));
|
|
72
|
+
return {read: v => priority.get(read(v)) ?? -1, fcmp};
|
|
73
|
+
}
|
|
74
|
+
return {read, fcmp};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fcmpx_compile_read(read)
|
|
78
|
+
{
|
|
79
|
+
if (read === undefined || read === '' || read === '.') {
|
|
80
|
+
return identity;
|
|
81
|
+
}
|
|
82
|
+
if (typeof read === 'function') {
|
|
83
|
+
return function (value) {
|
|
84
|
+
return read(value) ?? MISSING;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (typeof read === 'string') {
|
|
88
|
+
const props = read.split('.');
|
|
89
|
+
const [a, b, c, d, e] = props;
|
|
90
|
+
switch (props.length) {
|
|
91
|
+
case 1: return v => v?.[a] ?? MISSING;
|
|
92
|
+
case 2: return v => v?.[a]?.[b] ?? MISSING;
|
|
93
|
+
case 3: return v => v?.[a]?.[b]?.[c] ?? MISSING;
|
|
94
|
+
case 4: return v => v?.[a]?.[b]?.[c]?.[d] ?? MISSING;
|
|
95
|
+
case 5: return v => v?.[a]?.[b]?.[c]?.[d]?.[e] ?? MISSING;
|
|
96
|
+
default: return v => props.reduce((a,p) => a?.[p], v) ?? MISSING;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return v => v?.[read] ?? MISSING;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Sorting rule: Missing values are considered greater than any defined value
|
|
103
|
+
// and are therefore placed after all defined values.
|
|
104
|
+
function fcmp_wrapper(fcmp)
|
|
105
|
+
{
|
|
106
|
+
return function (a, b) {
|
|
107
|
+
if (a === MISSING) {
|
|
108
|
+
if (b === MISSING) {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
if (b === MISSING) {
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
return fcmp(a, b);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = fcmpx;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fcmpx = require('./fcmpx');
|
|
3
|
+
|
|
4
|
+
const users = [
|
|
5
|
+
{id: 'fred|48|100', user: {name: 'fred', age: 48, balance: 100}},
|
|
6
|
+
{id: 'barney|36|-200', user: {name: 'barney', age: 36, balance: -200}},
|
|
7
|
+
{id: 'wilma|-|-', user: {name: 'wilma', tag: {short: 'foo', long: 'foofoo'}}},
|
|
8
|
+
{id: 'betty|32|-', user: {name: 'betty', tag: {short: 'bar', long: 'barbar'}, age: 32}},
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const tests = [
|
|
12
|
+
{
|
|
13
|
+
description: 'missing: should move undefined values to the bottom',
|
|
14
|
+
items: [
|
|
15
|
+
{id: 'user1', user: {age: 48}},
|
|
16
|
+
{id: 'user2', user: {age: 36}},
|
|
17
|
+
{id: 'user3'},
|
|
18
|
+
{id: 'user4', user: {age: 32}},
|
|
19
|
+
],
|
|
20
|
+
extract: v => v.id,
|
|
21
|
+
expressions: [
|
|
22
|
+
'user.age',
|
|
23
|
+
v => v.user?.age,
|
|
24
|
+
{read: 'user.age'},
|
|
25
|
+
{read: v => v.user?.age},
|
|
26
|
+
['user.age'],
|
|
27
|
+
[v => v.user?.age],
|
|
28
|
+
[{read: 'user.age'}],
|
|
29
|
+
[{read: v => v.user?.age}],
|
|
30
|
+
],
|
|
31
|
+
expected: ['user4', 'user2', 'user1', 'user3'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: 'missing: should move undefined values to the bottom #2',
|
|
35
|
+
items: [
|
|
36
|
+
{id: 'user1', user: {age: 48}},
|
|
37
|
+
{id: 'user2', user: {age: 36}},
|
|
38
|
+
{id: 'user3', user: {tag: {short: 'foo'}}},
|
|
39
|
+
{id: 'user4', user: {tag: {short: 'bar'}, age: 32}},
|
|
40
|
+
],
|
|
41
|
+
extract: v => v.id,
|
|
42
|
+
expressions: [
|
|
43
|
+
'user.age,user.tag.short',
|
|
44
|
+
['user.age', 'user.tag.short'],
|
|
45
|
+
[v => v.user.age, v => v.user.tag?.short],
|
|
46
|
+
[{read: 'user.age'}, {read: 'user.tag.short'}],
|
|
47
|
+
[{read: v => v.user.age}, {read: v => v.user.tag?.short}],
|
|
48
|
+
],
|
|
49
|
+
expected: ['user4', 'user2', 'user1', 'user3'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
description: 'missing: should move undefined values to the bottom and keep their initial order',
|
|
53
|
+
items: [
|
|
54
|
+
{id: 'user1', user: {balance: 100}},
|
|
55
|
+
{id: 'user2', user: {balance: -200}},
|
|
56
|
+
{id: 'user3'},
|
|
57
|
+
{id: 'user4'},
|
|
58
|
+
],
|
|
59
|
+
extract: v => v.id,
|
|
60
|
+
expressions: [
|
|
61
|
+
'user.balance',
|
|
62
|
+
v => v.user?.balance,
|
|
63
|
+
[v => v.user?.balance],
|
|
64
|
+
],
|
|
65
|
+
expected: ['user2', 'user1', 'user3', 'user4'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
description: 'move undefined value to the bottom, keep their initial order',
|
|
69
|
+
items: users,
|
|
70
|
+
extract: v => v.id,
|
|
71
|
+
expressions: [
|
|
72
|
+
'user.balance,user.age',
|
|
73
|
+
[v => v.user.balance, v => v.user.age],
|
|
74
|
+
],
|
|
75
|
+
expected: ['barney|36|-200', 'fred|48|100', 'betty|32|-', 'wilma|-|-'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
description: 'move undefined value to the bottom, keep their initial order',
|
|
79
|
+
items: users,
|
|
80
|
+
extract: v => v.id,
|
|
81
|
+
expressions: [
|
|
82
|
+
'user.balance,user.age',
|
|
83
|
+
[v => v.user.balance, v => v.user.age],
|
|
84
|
+
],
|
|
85
|
+
expected: ['barney|36|-200', 'fred|48|100', 'betty|32|-', 'wilma|-|-'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
description: 'user.tag.short',
|
|
89
|
+
items: [
|
|
90
|
+
{user: {name: 'fred', age: 48}},
|
|
91
|
+
{user: {name: 'barney', age: 36}},
|
|
92
|
+
{user: {name: 'wilma', tag: {short: 'foo'}}},
|
|
93
|
+
{user: {name: 'betty', tag: {short: 'bar'}, age: 32}},
|
|
94
|
+
],
|
|
95
|
+
extract: v => `${v.user.name}|${v.user.tag?.short ?? '-'}`,
|
|
96
|
+
expressions: [
|
|
97
|
+
'user.tag.short',
|
|
98
|
+
v => v.user.tag?.short,
|
|
99
|
+
[v => v.user.tag?.short],
|
|
100
|
+
],
|
|
101
|
+
expected: ['betty|bar', 'wilma|foo', 'fred|-', 'barney|-'],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
description: 'user.tag.short,user.age',
|
|
105
|
+
items: [
|
|
106
|
+
{user: {name: 'fred', age: 48}},
|
|
107
|
+
{user: {name: 'barney', age: 36}},
|
|
108
|
+
{user: {name: 'wilma', tag: {short: 'foo'}}},
|
|
109
|
+
{user: {name: 'betty', tag: {short: 'bar'}, age: 32}},
|
|
110
|
+
],
|
|
111
|
+
extract: v => `${v.user.name}|${v.user.tag?.short ?? ''}|${v.user.age ?? ''}`,
|
|
112
|
+
expressions: [
|
|
113
|
+
'user.tag.short,user.age',
|
|
114
|
+
['user.tag.short', 'user.age'],
|
|
115
|
+
[v => v.user.tag?.short, v => v.user.age],
|
|
116
|
+
],
|
|
117
|
+
expected: ['betty|bar|32', 'wilma|foo|', 'barney||36', 'fred||48'],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
description: 'top: move specified values to the top',
|
|
121
|
+
items: [
|
|
122
|
+
{id: 'task1', priority: 'normal'},
|
|
123
|
+
{id: 'task2', priority: 'low'},
|
|
124
|
+
{id: 'task3', priority: 'high'},
|
|
125
|
+
{id: 'task4', priority: 'normal'},
|
|
126
|
+
],
|
|
127
|
+
extract: v => `${v.id}:${v.priority}`,
|
|
128
|
+
expressions: [
|
|
129
|
+
{read: 'priority', top: ['high']},
|
|
130
|
+
],
|
|
131
|
+
expected: ['task3:high', 'task1:normal', 'task2:low', 'task4:normal'],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
description: 'top: move specified values to the top #2',
|
|
135
|
+
items: [
|
|
136
|
+
{id: 'task1', priority: 'normal'},
|
|
137
|
+
{id: 'task2', priority: 'low'},
|
|
138
|
+
{id: 'task3', priority: 'high'},
|
|
139
|
+
{id: 'task4', priority: 'normal'},
|
|
140
|
+
],
|
|
141
|
+
extract: v => `${v.id}:${v.priority}`,
|
|
142
|
+
expressions: [
|
|
143
|
+
{read: 'priority', top: ['high', 'normal', 'low']},
|
|
144
|
+
],
|
|
145
|
+
expected: ['task3:high', 'task1:normal', 'task4:normal', 'task2:low'],
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
describe('fcmpx', function () {
|
|
150
|
+
tests.forEach(function (test) {
|
|
151
|
+
test.expressions.forEach(function (expr, i) {
|
|
152
|
+
it(`${test.description}: #${i+1} | ${test.description}`, function () {
|
|
153
|
+
const actual = test.items.slice().sort(fcmpx(expr)).map(test.extract);
|
|
154
|
+
assert.deepStrictEqual(actual, test.expected);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function format_date_fs(d)
|
|
2
|
+
{
|
|
3
|
+
const ymd = d.getFullYear() + dd(d.getMonth() + 1) + dd(d.getDate());
|
|
4
|
+
const hms = dd(d.getHours()) + dd(d.getMinutes()) + dd(d.getSeconds());
|
|
5
|
+
return ymd + '_' + hms;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function dd(v)
|
|
9
|
+
{
|
|
10
|
+
return v > 9 ? `${v}` : `0${v}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = format_date_fs;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function format_date_human(d)
|
|
2
|
+
{
|
|
3
|
+
const ymd = d.getFullYear() + '/' + dd(d.getMonth() + 1) + '/' + dd(d.getDate());
|
|
4
|
+
const hms = dd(d.getHours()) + ':' + dd(d.getMinutes()) + ':' + dd(d.getSeconds());
|
|
5
|
+
return ymd + ' ' + hms;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function dd(v)
|
|
9
|
+
{
|
|
10
|
+
return v > 9 ? `${v}` : `0${v}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = format_date_human;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
const format_bytes = require('./format_bytes');
|
|
2
2
|
const format_percents = require('./format_percents');
|
|
3
3
|
const format_seconds = require('./format_seconds');
|
|
4
|
-
const
|
|
4
|
+
const is_num_gt = require('@vbarbarosh/type-helpers/src/is_num_gt');
|
|
5
5
|
|
|
6
6
|
function format_progress_bytes({percents, total, done, rate, eta, duration})
|
|
7
7
|
{
|
|
8
|
-
const bps =
|
|
8
|
+
const bps = is_num_gt(rate, 0) ? `${format_bytes(rate)}/s` : '~';
|
|
9
9
|
if (done > total) {
|
|
10
10
|
return `${format_bytes(done)} at ${bps} duration=${format_seconds(duration)}`;
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
13
|
-
const eta_str =
|
|
12
|
+
if (is_num_gt(total, 0)) {
|
|
13
|
+
const eta_str = is_num_gt(eta, 0) ? format_seconds(eta) : '~';
|
|
14
14
|
return `${format_percents(percents)} | ${format_bytes(done)} of ${format_bytes(total)} at ${bps} ETA ${eta_str} duration=${format_seconds(duration)}`;
|
|
15
15
|
}
|
|
16
|
-
if (
|
|
16
|
+
if (is_num_gt(done, 0)) {
|
|
17
17
|
return `${format_bytes(done)} of ~ at ${bps} duration=${format_seconds(duration)}`;
|
|
18
18
|
}
|
|
19
|
-
if (
|
|
19
|
+
if (is_num_gt(duration, 0)) {
|
|
20
20
|
return `~ duration=${format_seconds(duration)}`;
|
|
21
21
|
}
|
|
22
22
|
return '~';
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
const format_kilo = require('./format_kilo');
|
|
2
2
|
const format_percents = require('./format_percents');
|
|
3
3
|
const format_seconds = require('./format_seconds');
|
|
4
|
-
const
|
|
4
|
+
const is_num_gt = require('@vbarbarosh/type-helpers/src/is_num_gt');
|
|
5
5
|
|
|
6
6
|
function format_progress_kilo({percents, total, done, rate, eta, duration})
|
|
7
7
|
{
|
|
8
|
-
const speed =
|
|
8
|
+
const speed = is_num_gt(rate, 0) ? `${format_kilo(rate)}/s` : '~';
|
|
9
9
|
if (done > total) {
|
|
10
10
|
return `${format_kilo(done)} at ${speed} duration=${format_seconds(duration)}`;
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
13
|
-
const eta_str =
|
|
12
|
+
if (is_num_gt(total, 0)) {
|
|
13
|
+
const eta_str = is_num_gt(eta, 0) ? format_seconds(eta) : '~';
|
|
14
14
|
return `${format_percents(percents)} | ${format_kilo(done)} of ${format_kilo(total)} at ${speed} ETA ${eta_str} duration=${format_seconds(duration)}`;
|
|
15
15
|
}
|
|
16
|
-
if (
|
|
16
|
+
if (is_num_gt(done, 0)) {
|
|
17
17
|
return `${format_kilo(done)} of ~ at ${speed} duration=${format_seconds(duration)}`;
|
|
18
18
|
}
|
|
19
|
-
if (
|
|
19
|
+
if (is_num_gt(duration, 0)) {
|
|
20
20
|
return `~ duration=${format_seconds(duration)}`;
|
|
21
21
|
}
|
|
22
22
|
return '~';
|
package/src/fs_list.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const Promise = require('bluebird');
|
|
2
|
+
const fs_fi = require('./fs_fi');
|
|
3
|
+
const fs_path_join = require('./fs_path_join');
|
|
4
|
+
const fs_readdir = require('./fs_readdir');
|
|
5
|
+
|
|
6
|
+
async function fs_list(pathname = '.')
|
|
7
|
+
{
|
|
8
|
+
const fi = await fs_fi(pathname);
|
|
9
|
+
if (fi.isDirectory()) {
|
|
10
|
+
const names = await fs_readdir(pathname);
|
|
11
|
+
return Promise.all(names.map(v => fs_fi(fs_path_join(pathname, v))));
|
|
12
|
+
}
|
|
13
|
+
return [fi];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = fs_list;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const fs_fi = require('./fs_fi');
|
|
2
|
+
const fs_path_join = require('./fs_path_join');
|
|
3
|
+
const fs_readdir = require('./fs_readdir');
|
|
4
|
+
|
|
5
|
+
async function fs_list_deep(path = '.')
|
|
6
|
+
{
|
|
7
|
+
const out = [];
|
|
8
|
+
const queue = [path];
|
|
9
|
+
while (queue.length) {
|
|
10
|
+
const fi = await fs_fi(queue.pop());
|
|
11
|
+
out.push(fi);
|
|
12
|
+
if (fi.isDirectory()) {
|
|
13
|
+
const names = await fs_readdir(fi.pathname);
|
|
14
|
+
queue.push(...names.map(v => fs_path_join(fi.pathname, v)));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = fs_list_deep;
|
package/src/fs_ls.js
CHANGED
|
@@ -3,6 +3,9 @@ const fs_fi = require('./fs_fi');
|
|
|
3
3
|
const fs_path_join = require('./fs_path_join');
|
|
4
4
|
const fs_readdir = require('./fs_readdir');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Deprecated in favor or fs_list
|
|
8
|
+
*/
|
|
6
9
|
async function fs_ls(pathname = '.')
|
|
7
10
|
{
|
|
8
11
|
const fi = await fs_fi(pathname);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function fs_path_extname(p)
|
|
4
|
+
{
|
|
5
|
+
return path.extname(p);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// function extname(s)
|
|
9
|
+
// {
|
|
10
|
+
// const i = s.lastIndexOf('.');
|
|
11
|
+
// return (i > 0) ? s.slice(i) : null;
|
|
12
|
+
// }
|
|
13
|
+
|
|
14
|
+
module.exports = fs_path_extname;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs_path_extname = require('./fs_path_extname');
|
|
3
|
+
|
|
4
|
+
const tests = [
|
|
5
|
+
['.', ''],
|
|
6
|
+
['..', ''],
|
|
7
|
+
['.foo', ''],
|
|
8
|
+
['..foo', '.foo'],
|
|
9
|
+
['.foo.bar', '.bar'],
|
|
10
|
+
['fs_path_extname.js', '.js'],
|
|
11
|
+
['/path/to/file.js', '.js'],
|
|
12
|
+
['/path/to/file.js/foo', ''],
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
describe('fs_path_extname', function () {
|
|
16
|
+
tests.forEach(function ([input, expected]) {
|
|
17
|
+
it(`${input} -> ${expected}`, function () {
|
|
18
|
+
const actual = fs_path_extname(input);
|
|
19
|
+
assert.strictEqual(actual, expected);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/now_fs.js
ADDED
package/src/now_human.js
ADDED
package/src/parallel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const NotImplemented = require('./errors/NotImplemented');
|
|
2
2
|
const Promise = require('bluebird');
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const is_fn_async = require('@vbarbarosh/type-helpers/src/is_fn_async');
|
|
4
|
+
const is_fn_gen_async = require('@vbarbarosh/type-helpers/src/is_fn_gen_async');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* - Run until `spawn` return `null`.
|
|
@@ -12,7 +12,7 @@ const is_async_generator = require('@vbarbarosh/type-helpers/src/is_async_genera
|
|
|
12
12
|
*/
|
|
13
13
|
async function parallel({concurrency, spawn, progress})
|
|
14
14
|
{
|
|
15
|
-
if (
|
|
15
|
+
if (is_fn_async(spawn) || is_fn_gen_async(spawn)) {
|
|
16
16
|
throw new Error('[spawn] should not be async function. Instead, it should be a simple function returning either [null] or a [promise].');
|
|
17
17
|
}
|
|
18
18
|
const running = [];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const Promise = require('bluebird');
|
|
2
|
+
const format_thousands = require('./format_thousands');
|
|
3
|
+
const ignore = require('./ignore');
|
|
4
|
+
const pid_exists = require('./pid_exists');
|
|
5
|
+
|
|
6
|
+
async function pid_kill_grace(pid, {grace_timeout_ms = 5000, log = ignore} = {})
|
|
7
|
+
{
|
|
8
|
+
log('Sending SIGTERM');
|
|
9
|
+
if (!process.kill(pid, 'SIGTERM')) {
|
|
10
|
+
throw new Error('SIGTERM Failed');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const end = Date.now() + grace_timeout_ms;
|
|
14
|
+
while (Date.now() < end) {
|
|
15
|
+
if (!pid_exists(pid)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
log(`Process is still there, waiting for 100ms, ${format_thousands(end - Date.now())}ms left`);
|
|
19
|
+
await Promise.delay(100);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log('Sending SIGKILL');
|
|
23
|
+
if (!process.kill(pid, 'SIGKILL')) {
|
|
24
|
+
throw new Error('SIGKILL Failed');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = pid_kill_grace;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const Promise = require('bluebird');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* - Always closes the socket.
|
|
6
|
+
* - Throws on any error or timeout.
|
|
7
|
+
* - Doesn’t hang if the server sends nothing.
|
|
8
|
+
*/
|
|
9
|
+
function ping_socket(socket_path, data = 'PING', timeout_ms = 1000)
|
|
10
|
+
{
|
|
11
|
+
return new Promise(function (resolve, reject) {
|
|
12
|
+
let done = false;
|
|
13
|
+
const buf = [];
|
|
14
|
+
const socket = net.connect(socket_path);
|
|
15
|
+
socket.setTimeout(timeout_ms, () => finish(new Error('Socket Timeout')));
|
|
16
|
+
socket.on('end', () => finish(null, Buffer.concat(buf)))
|
|
17
|
+
socket.on('error', finish);
|
|
18
|
+
socket.on('connect', () => socket.end(data));
|
|
19
|
+
socket.on('data', chunk => buf.push(chunk));
|
|
20
|
+
function finish(error, out) {
|
|
21
|
+
if (done) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
done = true;
|
|
25
|
+
socket.removeAllListeners();
|
|
26
|
+
socket.destroy();
|
|
27
|
+
error ? reject(error) : resolve(out);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = ping_socket;
|
package/src/plural.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
function plural(n,
|
|
1
|
+
function plural(n, singular, plural, zero)
|
|
2
2
|
{
|
|
3
|
-
|
|
3
|
+
if (n === 0 && typeof zero === 'string') {
|
|
4
|
+
return zero;
|
|
5
|
+
}
|
|
6
|
+
if (n % 10 === 1 && n % 100 !== 11) {
|
|
7
|
+
return singular.split('#').join(n);
|
|
8
|
+
}
|
|
9
|
+
return plural.split('#').join(n);
|
|
4
10
|
}
|
|
5
11
|
|
|
6
12
|
module.exports = plural;
|
package/src/plural.test.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
const assert = require('assert');
|
|
2
2
|
const plural = require('./plural');
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
['1 apple', 1, '# apple', '# apples'
|
|
4
|
+
const tests = [
|
|
5
|
+
['1 apple', 1, '# apple', '# apples'],
|
|
6
6
|
['2 apples', 2, '# apple', '# apples'],
|
|
7
7
|
['10 apples', 10, '# apple', '# apples'],
|
|
8
8
|
['21 apple', 21, '# apple', '# apples'],
|
|
9
|
+
['0 apples', 0, '# apple', '# apples'],
|
|
10
|
+
['No apples', 0, '# apple', '# apples', 'No apples'],
|
|
9
11
|
];
|
|
10
12
|
|
|
11
13
|
describe('plural', function () {
|
|
12
|
-
|
|
14
|
+
tests.forEach(function ([expected, ...args],) {
|
|
13
15
|
it(expected, function () {
|
|
14
16
|
const actual = plural(...args);
|
|
15
17
|
assert.deepStrictEqual(actual, expected);
|
package/src/sanitize_var_name.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const camel_to_snake = require('./camel_to_snake');
|
|
2
1
|
const sanitize_dash_name = require('./sanitize_dash_name');
|
|
2
|
+
const str_camel_to_snake = require('./str_camel_to_snake');
|
|
3
3
|
|
|
4
4
|
function sanitize_var_name(var_name)
|
|
5
5
|
{
|
|
6
|
-
const out = sanitize_dash_name(
|
|
6
|
+
const out = sanitize_dash_name(str_camel_to_snake(var_name)).replace(/[^a-z0-9]+/g, '_');
|
|
7
7
|
if (out.length === 0) {
|
|
8
8
|
return '_';
|
|
9
9
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function str_camel_to_snake(s)
|
|
2
2
|
{
|
|
3
3
|
return s
|
|
4
4
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2') // Insert underscore between lowercase/number and uppercase
|
|
@@ -6,4 +6,4 @@ function camel_to_snake(s)
|
|
|
6
6
|
.toLowerCase();
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
module.exports =
|
|
9
|
+
module.exports = str_camel_to_snake;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const assert = require('assert');
|
|
2
|
-
const
|
|
2
|
+
const str_camel_to_snake = require('./str_camel_to_snake');
|
|
3
3
|
|
|
4
4
|
const items = [
|
|
5
5
|
['UPC', 'upc'],
|
|
@@ -8,10 +8,10 @@ const items = [
|
|
|
8
8
|
['XMLHTTPRequest', 'xmlhttp_request'],
|
|
9
9
|
];
|
|
10
10
|
|
|
11
|
-
describe('
|
|
11
|
+
describe('str_camel_to_snake', function () {
|
|
12
12
|
items.forEach(function ([input, expected],) {
|
|
13
13
|
it(`${input === '' ? '☒' : input} → ${expected}`, function () {
|
|
14
|
-
assert.deepStrictEqual(
|
|
14
|
+
assert.deepStrictEqual(str_camel_to_snake(input), expected);
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
17
|
});
|
package/src/fcmp_strings.js
DELETED
|
File without changes
|