@miso.ai/server-commons 0.5.4-beta.5 → 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 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
- "toml": "^3.0.0"
18
+ "log-update": "^5.0.1",
19
+ "toml": "^3.0.0",
20
+ "uuid": "^9.0.0"
18
21
  },
19
- "version": "0.5.4-beta.5"
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,2 @@
1
+ export { default as BaseBuffer } from './base.js';
2
+ export { default as JsonBuffer } from './json.js';
@@ -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';
@@ -0,0 +1 @@
1
+ export * from './utils.js';
@@ -1,4 +1,4 @@
1
- import { padLeft, padRight } from './string.js';
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 (typeof value !== 'number') {
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
@@ -62,3 +62,7 @@ export function copyValues(target, source, propNames) {
62
62
  }
63
63
  return target;
64
64
  }
65
+
66
+ export function unimplemented() {
67
+ throw new Error(`Unimplemented!`);
68
+ }
@@ -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
@@ -9,4 +9,12 @@ export default class Resolution {
9
9
  Object.freeze(this);
10
10
  }
11
11
 
12
+ async execute(fn) {
13
+ try {
14
+ this.resolve(await fn());
15
+ } catch(error) {
16
+ this.reject(error);
17
+ }
18
+ }
19
+
12
20
  }
@@ -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';
@@ -187,6 +187,8 @@ class Response {
187
187
 
188
188
  class Strategy {
189
189
 
190
+ // TODO: introduce RPS
191
+
190
192
  constructor({
191
193
  highWatermark = 1000,
192
194
  eagerLoad = false,
@@ -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
+ }
@@ -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
+ }
@@ -11,7 +11,7 @@ export default class OutputStream extends Writable {
11
11
  super({
12
12
  objectMode,
13
13
  });
14
- this._format = format || (v => `${v}`);
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
+ }