@miso.ai/server-commons 0.5.4-beta.6 → 0.6.0-beta.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 +5 -2
- package/src/buffer/base.js +67 -0
- package/src/buffer/index.js +2 -0
- package/src/buffer/json.js +66 -0
- package/src/index.js +5 -2
- package/src/log/index.js +1 -0
- package/src/{log.js → log/utils.js} +2 -2
- package/src/object.js +4 -0
- package/src/rate-limiting-queue.js +72 -0
- package/src/resolution.js +8 -0
- package/src/sink/bps.js +89 -0
- package/src/sink/index.js +1 -0
- package/src/stream/buffered-read.js +2 -0
- package/src/stream/buffered-write-state.js +257 -0
- package/src/stream/buffered-write.js +194 -0
- package/src/stream/index.js +2 -0
- package/src/stream/log-update.js +47 -0
- package/src/stream/output.js +10 -1
package/package.json
CHANGED
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
"simonpai <simon.pai@askmiso.com>"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"denque": "^2.1.0",
|
|
16
17
|
"js-yaml": "^4.1.0",
|
|
17
|
-
"
|
|
18
|
+
"log-update": "^5.0.1",
|
|
19
|
+
"toml": "^3.0.0",
|
|
20
|
+
"uuid": "^9.0.0"
|
|
18
21
|
},
|
|
19
|
-
"version": "0.
|
|
22
|
+
"version": "0.6.0-beta.0"
|
|
20
23
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export default class BaseBuffer {
|
|
2
|
+
|
|
3
|
+
constructor(type, { recordsLimit, bytesLimit }) {
|
|
4
|
+
this._options = {
|
|
5
|
+
type,
|
|
6
|
+
recordsLimit,
|
|
7
|
+
bytesLimit,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_reset() {
|
|
12
|
+
this._records = this._bytes = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get config() {
|
|
16
|
+
return Object.freeze({ ...this._options });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get limit() {
|
|
20
|
+
return this._recordsPerRequest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get empty() {
|
|
24
|
+
return this.records === 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get full() {
|
|
28
|
+
const { recordsLimit, bytesLimit } = this._options;
|
|
29
|
+
return (recordsLimit !== undefined && this.records >= recordsLimit) ||
|
|
30
|
+
(bytesLimit !== undefined && this.bytes >= bytesLimit);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get records() {
|
|
34
|
+
return this._records;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get bytes() {
|
|
38
|
+
return this._bytes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get payload() {
|
|
42
|
+
return this._payload;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get state() {
|
|
46
|
+
const { records, bytes, payload } = this;
|
|
47
|
+
return Object.freeze({ records, bytes, payload });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
push(record) {
|
|
51
|
+
const dispatches = this._push(record);
|
|
52
|
+
return this.full ? [...dispatches, ...this.flush()] : dispatches;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_push(record) {
|
|
56
|
+
throw new Error(`Unimplemented.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
flush() {
|
|
60
|
+
const { state } = this;
|
|
61
|
+
this._reset();
|
|
62
|
+
return state.records > 0 ? [state] : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
destroy() {}
|
|
66
|
+
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import BaseBuffer from './base.js';
|
|
2
|
+
|
|
3
|
+
function buildTransformFunction(transform, objectMode) {
|
|
4
|
+
// TODO: review this: v could be a buffer when objectMode = false
|
|
5
|
+
return objectMode ? (
|
|
6
|
+
transform ? v => JSON.stringify(transform(v)) : v => JSON.stringify(v)
|
|
7
|
+
) : (
|
|
8
|
+
transform ? transform : v => v
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default class JsonBuffer extends BaseBuffer {
|
|
13
|
+
|
|
14
|
+
constructor({
|
|
15
|
+
prefix = '[',
|
|
16
|
+
suffix = ']',
|
|
17
|
+
separator = ',',
|
|
18
|
+
transform,
|
|
19
|
+
objectMode,
|
|
20
|
+
recordsLimit,
|
|
21
|
+
bytesLimit,
|
|
22
|
+
}) {
|
|
23
|
+
super('json', { recordsLimit, bytesLimit });
|
|
24
|
+
this._format = { prefix, suffix, separator };
|
|
25
|
+
this._transform = buildTransformFunction(transform, objectMode);
|
|
26
|
+
this._affixSize = (prefix.length + suffix.length) * 2;
|
|
27
|
+
this._separatorSize = separator.length * 2;
|
|
28
|
+
this._reset();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_reset() {
|
|
32
|
+
this._records = 0;
|
|
33
|
+
this._bytes = this._affixSize;
|
|
34
|
+
this._payload = this._format.prefix;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get payload() {
|
|
38
|
+
return this._payload + this._format.suffix;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_push(record) {
|
|
42
|
+
let dispatches = [];
|
|
43
|
+
const payload = this._transform(record);
|
|
44
|
+
const { empty } = this;
|
|
45
|
+
const cententSize = payload.length * 2;
|
|
46
|
+
|
|
47
|
+
if (!empty) {
|
|
48
|
+
const separatorSize = this._separatorSize;
|
|
49
|
+
if (this._bytes + separatorSize + cententSize > this._options.bytesLimit) {
|
|
50
|
+
// if buffer is not empty and exceeds bytes limit after pushing, then flush right now
|
|
51
|
+
dispatches = this.flush();
|
|
52
|
+
} else {
|
|
53
|
+
// non-empty, not flushed
|
|
54
|
+
this._bytes += separatorSize;
|
|
55
|
+
this._payload += this._format.separator;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._records++;
|
|
60
|
+
this._bytes += cententSize;
|
|
61
|
+
this._payload += payload;
|
|
62
|
+
|
|
63
|
+
return dispatches;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,11 @@ export * from './file.js';
|
|
|
5
5
|
export * from './config.js';
|
|
6
6
|
export * from './async.js';
|
|
7
7
|
|
|
8
|
+
export * as sink from './sink/index.js';
|
|
9
|
+
export * as buffer from './buffer/index.js';
|
|
8
10
|
export * as stream from './stream/index.js';
|
|
9
|
-
export * as log from './log.js';
|
|
11
|
+
export * as log from './log/index.js';
|
|
10
12
|
|
|
11
|
-
export { default as TaskQueue } from './task-queue.js';
|
|
12
13
|
export { default as Resolution } from './resolution.js';
|
|
14
|
+
export { default as TaskQueue } from './task-queue.js';
|
|
15
|
+
export { default as RateLimitingQueue } from './rate-limiting-queue.js';
|
package/src/log/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './utils.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { padLeft, padRight } from '
|
|
1
|
+
import { padLeft, padRight } from '../string.js';
|
|
2
2
|
|
|
3
3
|
export const FATAL = 'fatal';
|
|
4
4
|
export const ERROR = 'error';
|
|
@@ -38,7 +38,7 @@ function compareLevel(a, b) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function formatDuration(value) {
|
|
41
|
-
if (
|
|
41
|
+
if (isNaN(value)) {
|
|
42
42
|
throw new Error(`Value must be a number: ${value}`);
|
|
43
43
|
}
|
|
44
44
|
value = Math.floor(value);
|
package/src/object.js
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import Denque from 'denque';
|
|
2
|
+
import Resolution from './resolution.js';
|
|
3
|
+
|
|
4
|
+
export default class RateLimitingQueue {
|
|
5
|
+
|
|
6
|
+
constructor({
|
|
7
|
+
actionsPerSecond,
|
|
8
|
+
} = {}) {
|
|
9
|
+
if (actionsPerSecond !== undefined && (isNaN(actionsPerSecond) || actionsPerSecond <= 0)) {
|
|
10
|
+
throw new Error(`Invalid value for actionsPerSecond: ${actionsPerSecond}`);
|
|
11
|
+
}
|
|
12
|
+
this._options = {
|
|
13
|
+
actionsPerSecond,
|
|
14
|
+
};
|
|
15
|
+
this._interval = actionsPerSecond ? 1000 / actionsPerSecond : 0;
|
|
16
|
+
this._queue = new Denque();
|
|
17
|
+
this._running = this._busy = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
push(fn) {
|
|
21
|
+
const res = new Resolution();
|
|
22
|
+
this._queue.push([fn, res]);
|
|
23
|
+
this.start();
|
|
24
|
+
return res.promise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get running() {
|
|
28
|
+
return this._running;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get busy() {
|
|
32
|
+
return this._busy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start() {
|
|
36
|
+
this._running = true;
|
|
37
|
+
if (!this._busy) {
|
|
38
|
+
this._next();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pause() {
|
|
43
|
+
this._running = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_next() {
|
|
47
|
+
const queue = this._queue;
|
|
48
|
+
if (queue.isEmpty()) {
|
|
49
|
+
this._running = false;
|
|
50
|
+
}
|
|
51
|
+
if (!this._running || this._busy) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this._busy = true;
|
|
55
|
+
const [fn, res] = queue.shift();
|
|
56
|
+
this._exec(fn, res);
|
|
57
|
+
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
this._busy = false;
|
|
60
|
+
this._next();
|
|
61
|
+
}, this._interval);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async _exec(fn, res) {
|
|
65
|
+
try {
|
|
66
|
+
res.resolve(await fn());
|
|
67
|
+
} catch(error) {
|
|
68
|
+
res.reject(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
package/src/resolution.js
CHANGED
package/src/sink/bps.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { trimObj } from '../object.js';
|
|
2
|
+
|
|
3
|
+
export default class BpsSink {
|
|
4
|
+
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this._options = this._normalizeOptions(options);
|
|
7
|
+
this._stats = {
|
|
8
|
+
started: { count: 0, records: 0, bytes: 0 },
|
|
9
|
+
finished: { count: 0, records: 0, bytes: 0 },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_normalizeOptions({
|
|
14
|
+
recordsPerSecord = 100000,
|
|
15
|
+
bytesPerSecond = 4 * 1024 * 1024,
|
|
16
|
+
...options
|
|
17
|
+
} = {}) {
|
|
18
|
+
return {
|
|
19
|
+
recordsPerSecord,
|
|
20
|
+
bytesPerSecond,
|
|
21
|
+
...options,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get config() {
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
...this._options,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get stats() {
|
|
32
|
+
return Object.freeze({ ...this._stats });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async write(payload, { records, bytes } = {}) {
|
|
36
|
+
const { started, finished } = this._stats;
|
|
37
|
+
if (started.count === 0) {
|
|
38
|
+
this._firstWriteAt = Date.now();
|
|
39
|
+
}
|
|
40
|
+
started.count++;
|
|
41
|
+
started.records += records;
|
|
42
|
+
started.bytes += bytes;
|
|
43
|
+
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await this._write(payload, { records, bytes });
|
|
47
|
+
} catch(error) {
|
|
48
|
+
response = trimObj({ errors: true, cause: error.message });
|
|
49
|
+
}
|
|
50
|
+
finished.count++;
|
|
51
|
+
finished.records += records;
|
|
52
|
+
finished.bytes += bytes;
|
|
53
|
+
|
|
54
|
+
return trimObj({
|
|
55
|
+
...response,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
blockedTime() {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const elapsed = now - this._firstWriteAt;
|
|
63
|
+
const targetBps = this._targetBps(now);
|
|
64
|
+
const targetRps = this._targetRps(now);
|
|
65
|
+
|
|
66
|
+
const { started } = this._stats;
|
|
67
|
+
const shallElapsed = Math.max(started.records / targetRps, started.bytes / targetBps) * 1000;
|
|
68
|
+
|
|
69
|
+
const blockedTime = shallElapsed - elapsed;
|
|
70
|
+
if (blockedTime <= 1000) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
// TODO: review this
|
|
74
|
+
return Math.ceil(blockedTime);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _write(payload) {
|
|
78
|
+
throw new Error(`Unimplemented.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_targetBps(timestamp) {
|
|
82
|
+
return this._options.bytesPerSecond;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_targetRps(timestamp) {
|
|
86
|
+
return this._options.recordsPerSecord;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as BpsSink } from './bps.js';
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { trimObj } from '../object.js';
|
|
2
|
+
import Resolution from '../resolution.js';
|
|
3
|
+
|
|
4
|
+
const STATUS = Object.freeze({
|
|
5
|
+
WAITING: 'waiting',
|
|
6
|
+
RUNNING: 'running',
|
|
7
|
+
PAUSED: 'paused',
|
|
8
|
+
FINISHED: 'finished',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default class State {
|
|
12
|
+
|
|
13
|
+
static get STATUS() {
|
|
14
|
+
return STATUS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this._status = 'waiting';
|
|
19
|
+
this._resolutions = new WeakMap();
|
|
20
|
+
this._time = new Time(Date.now());
|
|
21
|
+
|
|
22
|
+
this._next = Object.freeze({
|
|
23
|
+
request: 0,
|
|
24
|
+
record: 0,
|
|
25
|
+
});
|
|
26
|
+
this._pending = [];
|
|
27
|
+
this._successful = {
|
|
28
|
+
requests: 0,
|
|
29
|
+
records: 0,
|
|
30
|
+
bytes: 0,
|
|
31
|
+
};
|
|
32
|
+
this._failed = {
|
|
33
|
+
requests: 0,
|
|
34
|
+
records: 0,
|
|
35
|
+
bytes: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// status //
|
|
40
|
+
get status() {
|
|
41
|
+
return this._status;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pause(pausedAt, pauseTime) {
|
|
45
|
+
if (this._status === STATUS.PAUSED) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this._time.pause(pausedAt, pauseTime);
|
|
49
|
+
this._status = STATUS.PAUSED;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resume(resumeAt) {
|
|
53
|
+
if (this._status !== STATUS.PAUSED) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this._time.resume(resumeAt);
|
|
57
|
+
this._status = this._next.request === 0 ? STATUS.WAITING : STATUS.RUNNING;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async finish() {
|
|
61
|
+
await Promise.all(this._pending.map(r => this._resolutions.get(r).promise));
|
|
62
|
+
this._status = STATUS.FINISHED;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// data stats //
|
|
66
|
+
get next() {
|
|
67
|
+
return this._next;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get pending() {
|
|
71
|
+
return Object.freeze(this._pending.slice());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get successful() {
|
|
75
|
+
return Object.freeze({ ...this._successful });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get failed() {
|
|
79
|
+
return Object.freeze({ ...this._failed });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get time() {
|
|
83
|
+
return this._time;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// speed management //
|
|
87
|
+
bps(timestamp) {
|
|
88
|
+
const elapsed = this._time.elapsed(timestamp || Date.now());
|
|
89
|
+
return elapsed > 0 ? this.sent.bytes / elapsed * 1000 : NaN;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// operation //
|
|
93
|
+
request({ records, bytes }) {
|
|
94
|
+
const timestamp = Date.now();
|
|
95
|
+
const { next } = this;
|
|
96
|
+
if (next.request === 0) {
|
|
97
|
+
this._time.setFirstWrite(timestamp);
|
|
98
|
+
}
|
|
99
|
+
const request = Object.freeze({
|
|
100
|
+
index: next.request,
|
|
101
|
+
recordOffset: next.record,
|
|
102
|
+
records,
|
|
103
|
+
bytes,
|
|
104
|
+
timestamp,
|
|
105
|
+
});
|
|
106
|
+
this._resolutions.set(request, new Resolution());
|
|
107
|
+
this._next = Object.freeze({
|
|
108
|
+
request: next.request + 1,
|
|
109
|
+
record: next.record + records,
|
|
110
|
+
});
|
|
111
|
+
this._pending.push(request);
|
|
112
|
+
return request;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
resolve(request, response) {
|
|
116
|
+
this._pending = this._pending.filter(r => r.index !== request.index);
|
|
117
|
+
|
|
118
|
+
const category = response.errors ? this._failed : this._successful;
|
|
119
|
+
category.requests++;
|
|
120
|
+
category.records += request.records;
|
|
121
|
+
category.bytes += request.bytes;
|
|
122
|
+
|
|
123
|
+
this._time.addWrite(response.timestamp - request.timestamp);
|
|
124
|
+
|
|
125
|
+
this._resolutions.get(request).resolve();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// summary //
|
|
129
|
+
get sent() {
|
|
130
|
+
const { _pending, completed } = this;
|
|
131
|
+
return Object.freeze(_pending.reduce((acc, request) => {
|
|
132
|
+
acc.requests++;
|
|
133
|
+
acc.records += request.records;
|
|
134
|
+
acc.bytes += request.bytes;
|
|
135
|
+
return acc;
|
|
136
|
+
}, {
|
|
137
|
+
requests: completed.requests,
|
|
138
|
+
records: completed.records,
|
|
139
|
+
bytes: completed.bytes,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get completed() {
|
|
144
|
+
const { _successful, _failed } = this;
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
requests: _successful.requests + _failed.requests,
|
|
147
|
+
records: _successful.records + _failed.records,
|
|
148
|
+
bytes: _successful.bytes + _failed.bytes,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
snapshot(timestamp) {
|
|
153
|
+
timestamp = timestamp || Date.now();
|
|
154
|
+
const { status, next, pending, successful, failed, sent, completed } = this;
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
status, next, pending, sent, successful, failed, completed,
|
|
157
|
+
time: this._time.snapshot(timestamp),
|
|
158
|
+
bps: this.bps(timestamp),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class Time {
|
|
165
|
+
|
|
166
|
+
constructor(timestamp) {
|
|
167
|
+
Object.defineProperty(this, 'constructedAt', { value: timestamp });
|
|
168
|
+
this._firstWriteAt = undefined;
|
|
169
|
+
this._write = 0;
|
|
170
|
+
this._pausedAt = undefined;
|
|
171
|
+
this._willResumeAt = undefined;
|
|
172
|
+
this._paused = 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// operation //
|
|
176
|
+
setFirstWrite(timestamp) {
|
|
177
|
+
if (this._firstWriteAt !== undefined) {
|
|
178
|
+
throw new Error(`First write time already set: ${this._firstWriteAt}`);
|
|
179
|
+
}
|
|
180
|
+
this._firstWriteAt = timestamp;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
addWrite(duration) {
|
|
184
|
+
this._write += duration;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
pause(pausedAt, pauseTime) {
|
|
188
|
+
if (this._firstWriteAt === undefined || this._pausedAt !== undefined) {
|
|
189
|
+
throw new Error(`Cannot pause() before first write or when paused.`);
|
|
190
|
+
}
|
|
191
|
+
this._pausedAt = pausedAt;
|
|
192
|
+
this._willResumeAt = pausedAt + pauseTime;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
resume(resumeAt) {
|
|
196
|
+
if (this._firstWriteAt === undefined || this._pausedAt === undefined) {
|
|
197
|
+
throw new Error(`Cannot resume() before first write or when not paused.`);
|
|
198
|
+
}
|
|
199
|
+
this._paused += resumeAt - this._pausedAt;
|
|
200
|
+
this._pausedAt = this._willResumeAt = undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// stats //
|
|
204
|
+
get firstWriteAt() {
|
|
205
|
+
return this._firstWriteAt;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get pausedAt() {
|
|
209
|
+
return this._pausedAt;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
get willResumeAt() {
|
|
213
|
+
return this._willResumeAt;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get write() {
|
|
217
|
+
return this._write;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
total(timestamp) {
|
|
221
|
+
return timestamp - this.constructedAt;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
waiting(timestamp) {
|
|
225
|
+
return (this._firstWriteAt !== undefined ? this._firstWriteAt : timestamp) - this.constructedAt;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
elapsed(timestamp) {
|
|
229
|
+
return this._firstWriteAt === undefined ? 0 : timestamp - this._firstWriteAt;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
paused(timestamp) {
|
|
233
|
+
return this._paused + (this._pausedAt !== undefined ? timestamp - this._pausedAt : 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
running(timestamp) {
|
|
237
|
+
return this.elapsed(timestamp) - this.paused(timestamp);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
snapshot(timestamp) {
|
|
241
|
+
const { constructedAt, firstWriteAt, pausedAt, willResumeAt, write } = this;
|
|
242
|
+
return Object.freeze(trimObj({
|
|
243
|
+
currentTime: timestamp,
|
|
244
|
+
constructedAt,
|
|
245
|
+
firstWriteAt,
|
|
246
|
+
pausedAt,
|
|
247
|
+
willResumeAt,
|
|
248
|
+
write,
|
|
249
|
+
total: this.total(timestamp),
|
|
250
|
+
waiting: this.waiting(timestamp),
|
|
251
|
+
elapsed: this.elapsed(timestamp),
|
|
252
|
+
paused: this.paused(timestamp),
|
|
253
|
+
running: this.running(timestamp),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import { trimObj } from '../object.js';
|
|
4
|
+
import { delay } from '../async.js';
|
|
5
|
+
import * as log from '../log/index.js';
|
|
6
|
+
import State from './buffered-write-state.js';
|
|
7
|
+
|
|
8
|
+
const MIN_HREATBEAT_INTERVAL = 100;
|
|
9
|
+
|
|
10
|
+
export default class BufferedWriteStream extends Transform {
|
|
11
|
+
|
|
12
|
+
static DEFAULTS = {
|
|
13
|
+
// merge everything except for payload
|
|
14
|
+
output: (message, { payload: _, ...args } = {}) => ({ ...message, ...args }),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
constructor({
|
|
18
|
+
objectMode,
|
|
19
|
+
sink,
|
|
20
|
+
buffer,
|
|
21
|
+
output,
|
|
22
|
+
heartbeatInterval,
|
|
23
|
+
...options
|
|
24
|
+
} = {}) {
|
|
25
|
+
super({
|
|
26
|
+
readableObjectMode: true,
|
|
27
|
+
writableObjectMode: objectMode,
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, 'id', { value: uuid() });
|
|
30
|
+
|
|
31
|
+
this._sink = sink;
|
|
32
|
+
this._buffer = buffer;
|
|
33
|
+
|
|
34
|
+
this._fns = {
|
|
35
|
+
output,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this._options = this._normalizeOptions({
|
|
39
|
+
objectMode,
|
|
40
|
+
heartbeatInterval,
|
|
41
|
+
...options,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this._state = new State();
|
|
45
|
+
|
|
46
|
+
// log functions
|
|
47
|
+
for (const level of log.LEVELS) {
|
|
48
|
+
this[`_${level}`] = this._log.bind(this, level);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_normalizeOptions({
|
|
53
|
+
objectMode,
|
|
54
|
+
heartbeatInterval = false,
|
|
55
|
+
...options
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (heartbeatInterval !== false && (isNaN(heartbeatInterval) || heartbeatInterval < MIN_HREATBEAT_INTERVAL)) {
|
|
58
|
+
throw new Error(`Heartbeat interval must be a number in milliseconds at least ${MIN_HREATBEAT_INTERVAL}: ${heartbeatInterval}`);
|
|
59
|
+
}
|
|
60
|
+
return trimObj({
|
|
61
|
+
objectMode: !!objectMode,
|
|
62
|
+
heartbeatInterval,
|
|
63
|
+
...options,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_construct(done) {
|
|
68
|
+
const heartbeatInterval = this._options.heartbeatInterval;
|
|
69
|
+
if (heartbeatInterval) {
|
|
70
|
+
this._heartbeatIntervalId = setInterval(this._heartbeat.bind(this), heartbeatInterval);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { config } = this;
|
|
74
|
+
this._info('construct', { config });
|
|
75
|
+
|
|
76
|
+
done();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async _transform(record, _) {
|
|
80
|
+
this._pushStartEventIfNecessary();
|
|
81
|
+
this._dispatchAll(this._buffer.push(record));
|
|
82
|
+
await this._pauseIfNecessary();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async _flush(done) {
|
|
86
|
+
this._dispatchAll(this._buffer.flush());
|
|
87
|
+
await this._state.finish();
|
|
88
|
+
this._buffer.destroy();
|
|
89
|
+
|
|
90
|
+
// stop heartbeat before end event
|
|
91
|
+
if (this._heartbeatIntervalId) {
|
|
92
|
+
clearInterval(this._heartbeatIntervalId);
|
|
93
|
+
delete this._heartbeatIntervalId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// in case of empty stream, we still want a start event
|
|
97
|
+
this._pushStartEventIfNecessary();
|
|
98
|
+
|
|
99
|
+
const { successful, failed } = this._state;
|
|
100
|
+
this._info('end', { successful, failed });
|
|
101
|
+
|
|
102
|
+
done();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get state() {
|
|
106
|
+
return this._state.snapshot();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get config() {
|
|
110
|
+
return Object.freeze(this._exportConfig());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// can be overwritten //
|
|
114
|
+
_exportConfig() {
|
|
115
|
+
return {
|
|
116
|
+
id: this.id.substring(0, 8),
|
|
117
|
+
sink: this._sink.config,
|
|
118
|
+
buffer: this._buffer.config,
|
|
119
|
+
...this._options,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_output(message, args) {
|
|
124
|
+
return (this._fns.output || BufferedWriteStream.DEFAULTS.output)(message, args);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// helper //
|
|
128
|
+
_heartbeat() {
|
|
129
|
+
this._log(log.DEBUG, 'heartbeat');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_log(level, event, args) {
|
|
133
|
+
const message = trimObj({
|
|
134
|
+
level,
|
|
135
|
+
event,
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
state: this.state,
|
|
138
|
+
});
|
|
139
|
+
this.push(this._output(message, args));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_pushStartEventIfNecessary() {
|
|
143
|
+
if (this._state.next.request === 0 && this._buffer.records === 0) {
|
|
144
|
+
this._info('start');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _pauseIfNecessary() {
|
|
149
|
+
const pauseTime = this._sink.blockedTime();
|
|
150
|
+
if (pauseTime > 0) {
|
|
151
|
+
const pausedAt = Date.now();
|
|
152
|
+
this._state.pause(pausedAt, pauseTime);
|
|
153
|
+
this._debug('pause', { pausedAt, pauseTime });
|
|
154
|
+
await delay(pauseTime);
|
|
155
|
+
const resumeAt = Date.now();
|
|
156
|
+
this._state.resume(resumeAt);
|
|
157
|
+
this._debug('resume', { resumeAt });
|
|
158
|
+
} else if (this._state._pending.length > 15) {
|
|
159
|
+
// TODO: figure out best strategy on this
|
|
160
|
+
// release event loop for downstream
|
|
161
|
+
await delay();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_dispatchAll(dispatches) {
|
|
166
|
+
for (const dispatch of dispatches) {
|
|
167
|
+
this._dispatch(dispatch); // don't wait
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async _dispatch({ records, bytes, payload }) {
|
|
172
|
+
const request = this._state.request({ records, bytes });
|
|
173
|
+
this._debug('request', { request, payload });
|
|
174
|
+
|
|
175
|
+
const response = await this._writeToSink(payload, request);
|
|
176
|
+
|
|
177
|
+
this._state.resolve(request, response);
|
|
178
|
+
const failed = response.errors;
|
|
179
|
+
(failed ? this._error : this._debug)('response', { request, response, payload });
|
|
180
|
+
|
|
181
|
+
this._info('write', {
|
|
182
|
+
result: failed ? 'failed' : 'successful',
|
|
183
|
+
index: request.index,
|
|
184
|
+
records: request.records,
|
|
185
|
+
bytes: request.bytes,
|
|
186
|
+
time: response.timestamp - request.timestamp,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async _writeToSink(payload, request) {
|
|
191
|
+
return this._sink.write(payload, request);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
}
|
package/src/stream/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export * from './misc.js';
|
|
2
2
|
export { default as BufferedReadStream } from './buffered-read.js';
|
|
3
|
+
export { default as BufferedWriteStream } from './buffered-write.js';
|
|
3
4
|
export { default as OutputStream } from './output.js';
|
|
5
|
+
export { default as LogUpdateStream } from './log-update.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Writable } from 'stream';
|
|
2
|
+
import { createLogUpdate } from 'log-update';
|
|
3
|
+
import { isError } from '../log/index.js'
|
|
4
|
+
import { unimplemented } from '../object.js';
|
|
5
|
+
|
|
6
|
+
function defaultRenderError(data = {}) {
|
|
7
|
+
if (data.level && isError(data.level)) {
|
|
8
|
+
return JSON.stringify(data) + '\n';
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default class LogUpdateStream extends Writable {
|
|
14
|
+
|
|
15
|
+
constructor({
|
|
16
|
+
out = process.stdout,
|
|
17
|
+
err = process.stderr,
|
|
18
|
+
render = unimplemented,
|
|
19
|
+
renderError = defaultRenderError,
|
|
20
|
+
} = {}) {
|
|
21
|
+
super({
|
|
22
|
+
objectMode: true,
|
|
23
|
+
});
|
|
24
|
+
this._update = createLogUpdate(out);
|
|
25
|
+
this._err = err;
|
|
26
|
+
this._fns = {
|
|
27
|
+
render,
|
|
28
|
+
renderError,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_write(data, _, next) {
|
|
33
|
+
const errorMsg = this._renderError(data);
|
|
34
|
+
errorMsg && this._err.write(errorMsg);
|
|
35
|
+
this._update(this._render(data));
|
|
36
|
+
next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_render(data) {
|
|
40
|
+
return this._fns.render(data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_renderError(data) {
|
|
44
|
+
return this._fns.renderError(data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
}
|
package/src/stream/output.js
CHANGED
|
@@ -11,7 +11,7 @@ export default class OutputStream extends Writable {
|
|
|
11
11
|
super({
|
|
12
12
|
objectMode,
|
|
13
13
|
});
|
|
14
|
-
this._format = format || (
|
|
14
|
+
this._format = format || (objectMode ? defaultObjectModeFormat : defaultNonObjectModeFormat);
|
|
15
15
|
this._out = out;
|
|
16
16
|
this._err = err;
|
|
17
17
|
}
|
|
@@ -22,3 +22,12 @@ export default class OutputStream extends Writable {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
function defaultObjectModeFormat(v) {
|
|
27
|
+
typeof v === 'object' ? JSON.stringify(v) : `${v}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function defaultNonObjectModeFormat(v) {
|
|
31
|
+
// TODO: handle buffer
|
|
32
|
+
return v;
|
|
33
|
+
}
|