@vbarbarosh/node-helpers 3.5.2 → 3.6.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 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.2",
5
+ "version": "3.6.0",
6
6
  "description": "A set of helpers for JavaScript/Node.js",
7
7
  "files": [
8
8
  "src"
@@ -27,14 +27,15 @@
27
27
  "bluebird": "^3.7.2",
28
28
  "form-data": "^4.0.0",
29
29
  "json-stable-stringify": "^1.0.2",
30
- "sanitize-filename": "^1.6.3"
30
+ "sanitize-filename": "^1.6.3",
31
+ "split": "^1.0.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "mocha": "^10.2.0",
34
- "throttle": "^1.0.3",
35
35
  "redis-v1": "npm:redis@1",
36
36
  "redis-v2": "npm:redis@2",
37
37
  "redis-v3": "npm:redis@3",
38
- "redis-v4": "npm:redis@4"
38
+ "redis-v4": "npm:redis@4",
39
+ "throttle": "^1.0.3"
39
40
  }
40
41
  }
@@ -0,0 +1,8 @@
1
+ class NotImplemented extends Error
2
+ {
3
+ constructor(message = 'Not Implemented') {
4
+ super(message);
5
+ }
6
+ }
7
+
8
+ module.exports = NotImplemented;
package/src/fastdl.js ADDED
@@ -0,0 +1,86 @@
1
+ const format_bytes = require('./format_bytes');
2
+ const format_seconds = require('./format_seconds');
3
+ const format_thousands = require('./format_thousands');
4
+ const fs = require('fs');
5
+ const make_progress = require('./progress');
6
+ const parallel = require('./parallel');
7
+ const stream = require('stream');
8
+
9
+ /**
10
+ * Download a file in several connections in parallel.
11
+ *
12
+ * await fastdl({
13
+ * file: fs_path_basename(new URL(url).pathname),
14
+ * read_stream_with_range: (first, last) => http_get_stream_range(url, first, last),
15
+ * });
16
+ */
17
+ async function fastdl({file, read_stream_with_range, concurrency = 60, log = v => console.log(v)})
18
+ {
19
+ const M = 1024*1024;
20
+ const chunk_min_bytes = M;
21
+ const chunk_max_bytes = 50*M;
22
+
23
+ log(`Truncating destination file [${file}]...`);
24
+ await fs.promises.writeFile(file, '');
25
+
26
+ log('Requesting first chunk to determine total size...');
27
+ const rs0 = await read_stream_with_range(0, chunk_min_bytes);
28
+
29
+ const total = rs0.content_range.total;
30
+ const chunk_size = Math.max(chunk_min_bytes, Math.min(chunk_max_bytes, Math.trunc(total/concurrency)));
31
+ const progress = make_progress(total);
32
+
33
+ let connections = 0;
34
+ let next_first = 0;
35
+ let total_written = 0;
36
+
37
+ const timer = setInterval(tick, 1000);
38
+ function tick() {
39
+ const p = progress;
40
+ const bb = format_bytes;
41
+ const ss = format_seconds;
42
+ log(`${bb(p.done)} of ${bb(p.total)} ${(p.percentage*100).toFixed(2)}% at ${bb(p.rate)}/s ETA ${ss(p.eta)} duration=${ss(p.duration)} connections=${connections}`);
43
+ }
44
+
45
+ log(`${format_bytes(total)} [${format_thousands(total)} bytes] to download`);
46
+
47
+ try {
48
+ await parallel({concurrency, spawn});
49
+ }
50
+ finally {
51
+ tick();
52
+ clearInterval(timer);
53
+ }
54
+
55
+ if (total !== total_written) {
56
+ throw new Error(`Total bytes written differs from size of a file: total[${total}] - total_written[${total_written}] = ${total - total_written}`);
57
+ }
58
+
59
+ function spawn() {
60
+ if (next_first >= total) {
61
+ return;
62
+ }
63
+ const first = next_first;
64
+ const last = Math.min(total - 1, first + (first === 0 ? rs0.content_range.last : chunk_size));
65
+ next_first = last + 1;
66
+ connections++;
67
+ return Promise.resolve(run()).finally(() => connections--);
68
+ async function run() {
69
+ const rs = (first === 0) ? rs0 : await read_stream_with_range(first, last);
70
+ const acc = new stream.PassThrough({
71
+ transform(buf, enc, cb) {
72
+ total_written += buf.length;
73
+ progress.add(buf.length);
74
+ cb(null, buf);
75
+ }
76
+ });
77
+ const ws = fs.createWriteStream(file, {
78
+ flags: fs.constants.O_WRONLY, // |fs.constants.O_CREAT,
79
+ start: first,
80
+ });
81
+ await stream.promises.pipeline(rs, acc, ws);
82
+ }
83
+ }
84
+ }
85
+
86
+ module.exports = fastdl;
@@ -2,10 +2,10 @@ const axios = require('axios');
2
2
  const format_thousands = require('./format_thousands');
3
3
  const parse_http_content_range = require('./parse_http_content_range');
4
4
 
5
- async function http_get_stream_range(url, begin, end)
5
+ async function http_get_stream_range(url, first, last)
6
6
  {
7
- const a = Number.isInteger(begin) ? begin : '';
8
- const b = Number.isInteger(end) ? (end - 1) : '';
7
+ const a = Number.isInteger(first) ? first : '';
8
+ const b = Number.isInteger(last) ? last : '';
9
9
  const headers = {Range: `bytes=${a}-${b}`};
10
10
  const res = await axios.get(url, {responseType: 'stream', headers});
11
11
  const out = res.data;
package/src/logger.js ADDED
@@ -0,0 +1,25 @@
1
+ const NotImplemented = require('./errors/NotImplemented');
2
+
3
+ // grep_cle1grnu10000tu72diepbfet
4
+ const re = /^((?:\[\s*[^\]]*\])*)\s*(.*)$/m;
5
+
6
+ /**
7
+ * log = logger();
8
+ * log.add('hey')
9
+ * log.add('[tag] heyhey')
10
+ */
11
+ function logger(options = {})
12
+ {
13
+ function out(message) {
14
+ const v = typeof message === 'string' ? JSON.stringify(message.trim()).slice(1, -1) : JSON.stringify(message);
15
+ const [, a, m] = v.match(re);
16
+ const time = new Date().toJSON();
17
+ console.log(`${a} ${m}`.trim());
18
+ };
19
+ out.spawn = function () {
20
+ throw new NotImplemented();
21
+ };
22
+ return out;
23
+ }
24
+
25
+ module.exports = logger;
@@ -0,0 +1,51 @@
1
+ const NotImplemented = require('./errors/NotImplemented');
2
+
3
+ /**
4
+ * - Run until `spawn` return `null`.
5
+ * - Keep no more than `concurrency` number of workers at a time.
6
+ */
7
+ async function parallel({concurrency, spawn, progress})
8
+ {
9
+ const running = [];
10
+ return new Promise(function (resolve, reject) {
11
+ const timer = setInterval(tick, 1000);
12
+ schedule();
13
+ if (running.length) {
14
+ tick();
15
+ }
16
+ function schedule() {
17
+ while (running.length < concurrency) {
18
+ const w = spawn();
19
+ if (!w) {
20
+ break;
21
+ }
22
+ const item = Promise.resolve(w).then(resolved, rejected);
23
+ running.push(item);
24
+ function resolved() {
25
+ const i = running.indexOf(item);
26
+ if (i === -1) {
27
+ throw new NotImplemented();
28
+ }
29
+ running.splice(i, 1);
30
+ schedule();
31
+ }
32
+ function rejected(error) {
33
+ console.log(error);
34
+ throw new NotImplemented();
35
+ }
36
+ }
37
+ if (running.length === 0) {
38
+ tick();
39
+ clearInterval(timer);
40
+ resolve();
41
+ }
42
+ }
43
+ function tick() {
44
+ if (progress) {
45
+ progress(running);
46
+ }
47
+ }
48
+ });
49
+ }
50
+
51
+ module.exports = parallel;
@@ -1,15 +1,25 @@
1
1
  const assert = require('assert');
2
2
  const fs = require('fs');
3
+ const split = require('split');
4
+ const stream = require('stream');
3
5
  const stream_data_ln = require('./stream_data_ln');
4
6
  const stream_promise = require('./stream_promise');
5
7
 
6
8
  describe('stream_data_ln', function () {
7
9
  it('should handle basic input', async function () {
8
- let tmp = '';
10
+ let actual = '';
9
11
  const s = fs.createReadStream(__filename);
10
- const off = stream_data_ln(s, v => tmp += v + '\n');
12
+ const off = stream_data_ln(s, v => actual += v + '\n');
11
13
  await stream_promise(s);
12
14
  off();
13
- assert.deepStrictEqual(tmp, fs.readFileSync(__filename, {encoding: 'utf8'}));
15
+ assert.deepStrictEqual(actual, fs.readFileSync(__filename, {encoding: 'utf8'}));
16
+ });
17
+ it('split', async function () {
18
+ let actual = '';
19
+ const s = fs.createReadStream(__filename);
20
+ const ss = s.pipe(split('\n', null, {trailing: false}));
21
+ ss.on('data', v => actual += v + '\n');
22
+ await stream.promises.finished(ss);
23
+ assert.deepStrictEqual(actual, fs.readFileSync(__filename, {encoding: 'utf8'}));
14
24
  });
15
25
  });