@miso.ai/server-commons 0.5.4-beta.3 → 0.5.4-beta.5
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 +1 -1
- package/src/async.js +4 -0
- package/src/index.js +7 -1
- package/src/log.js +96 -0
- package/src/resolution.js +12 -0
- package/src/stream/buffered-read.js +299 -0
- package/src/stream/index.js +3 -0
- package/src/{stream.js → stream/misc.js} +11 -1
- package/src/stream/output.js +24 -0
- package/src/string.js +32 -0
package/package.json
CHANGED
package/src/async.js
ADDED
package/src/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export * from './object.js';
|
|
2
|
-
export *
|
|
2
|
+
export * from './string.js';
|
|
3
3
|
export * from './date.js';
|
|
4
4
|
export * from './file.js';
|
|
5
5
|
export * from './config.js';
|
|
6
|
+
export * from './async.js';
|
|
7
|
+
|
|
8
|
+
export * as stream from './stream/index.js';
|
|
9
|
+
export * as log from './log.js';
|
|
10
|
+
|
|
6
11
|
export { default as TaskQueue } from './task-queue.js';
|
|
12
|
+
export { default as Resolution } from './resolution.js';
|
package/src/log.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { padLeft, padRight } from './string.js';
|
|
2
|
+
|
|
3
|
+
export const FATAL = 'fatal';
|
|
4
|
+
export const ERROR = 'error';
|
|
5
|
+
export const WARNING = 'warning';
|
|
6
|
+
export const INFO = 'info';
|
|
7
|
+
export const DEBUG = 'debug';
|
|
8
|
+
|
|
9
|
+
const LEVEL_VALUES = {
|
|
10
|
+
[FATAL]: 2,
|
|
11
|
+
[ERROR]: 4,
|
|
12
|
+
[WARNING]: 6,
|
|
13
|
+
[INFO]: 8,
|
|
14
|
+
[DEBUG]: 50,
|
|
15
|
+
};
|
|
16
|
+
const FALLBACK_LEVEL_VALUE = 40;
|
|
17
|
+
|
|
18
|
+
export const LEVELS = Object.keys(LEVEL_VALUES);
|
|
19
|
+
|
|
20
|
+
LEVELS.sort(compareLevel);
|
|
21
|
+
|
|
22
|
+
export function reachesThreshold(level, threshold) {
|
|
23
|
+
if (!LEVEL_VALUES[threshold]) {
|
|
24
|
+
throw new Error(`Unrecognized log level: ${threshold}`);
|
|
25
|
+
}
|
|
26
|
+
if (!level) {
|
|
27
|
+
throw new Error(`Level is absent: ${level}`);
|
|
28
|
+
}
|
|
29
|
+
return compareLevel(level, threshold) <= 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isError(level) {
|
|
33
|
+
return reachesThreshold(level, ERROR);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compareLevel(a, b) {
|
|
37
|
+
return (LEVEL_VALUES[a] || FALLBACK_LEVEL_VALUE) - (LEVEL_VALUES[b] || FALLBACK_LEVEL_VALUE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatDuration(value) {
|
|
41
|
+
if (typeof value !== 'number') {
|
|
42
|
+
throw new Error(`Value must be a number: ${value}`);
|
|
43
|
+
}
|
|
44
|
+
value = Math.floor(value);
|
|
45
|
+
const ms = padLeft(`${value % 60000}`, 5, '0');
|
|
46
|
+
let min = '00';
|
|
47
|
+
let hour = '00';
|
|
48
|
+
if (value >= 60000) {
|
|
49
|
+
value = Math.floor(value / 60000);
|
|
50
|
+
min = padLeft(`${value % 60}`, 2, '0');
|
|
51
|
+
if (value >= 60) {
|
|
52
|
+
hour = padLeft(Math.floor(value / 60), 2, '0');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return `${hour}:${min}:${ms.substring(0, 2)}.${ms.substring(2, 5)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatBytes(value) {
|
|
59
|
+
let i = 0;
|
|
60
|
+
for (; i < 4; i++) {
|
|
61
|
+
if (value < 1024) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
value /= 1024;
|
|
65
|
+
}
|
|
66
|
+
return `${Math.floor(value * 100) / 100} ${UNITS[i]}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
70
|
+
|
|
71
|
+
export function formatTable(rows, { columnPadding: columnSpacing = 3 } = {}) {
|
|
72
|
+
const maxLens = [];
|
|
73
|
+
for (const cells of rows) {
|
|
74
|
+
for (let j = 0, len = cells.length; j < len; j++) {
|
|
75
|
+
const len = `${cells[j]}`.length;
|
|
76
|
+
if (!maxLens[j] || maxLens[j] < len) {
|
|
77
|
+
maxLens[j] = len;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const colSpc = ' '.repeat(columnSpacing);
|
|
82
|
+
let str = '';
|
|
83
|
+
for (let i = 0, len = rows.length; i < len; i++) {
|
|
84
|
+
if (i > 0) {
|
|
85
|
+
str += '\n';
|
|
86
|
+
}
|
|
87
|
+
const cells = rows[i];
|
|
88
|
+
for (let j = 0, len = cells.length; j < len; j++) {
|
|
89
|
+
if (j > 0) {
|
|
90
|
+
str += colSpc;
|
|
91
|
+
}
|
|
92
|
+
str += padRight(`${cells[j]}`, maxLens[j], ' ');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return str;
|
|
96
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
import Denque from 'denque';
|
|
3
|
+
import { trimObj } from '../object.js';
|
|
4
|
+
import TaskQueue from '../task-queue.js';
|
|
5
|
+
import Resolution from '../resolution.js';
|
|
6
|
+
|
|
7
|
+
export default class BufferedReadStream extends Readable {
|
|
8
|
+
|
|
9
|
+
constructor(source, { strategy, filter, transform, onLoad, debug } = {}) {
|
|
10
|
+
super({ objectMode: true });
|
|
11
|
+
this._debug = debug || (() => {});
|
|
12
|
+
this._source = source;
|
|
13
|
+
this._strategy = new Strategy(strategy);
|
|
14
|
+
this._state = new State();
|
|
15
|
+
this._loads = new TaskQueue();
|
|
16
|
+
this._buckets = new Denque();
|
|
17
|
+
|
|
18
|
+
this._filter = filter || (() => true);
|
|
19
|
+
this._transform = transform || (v => v);
|
|
20
|
+
this._onLoad = onLoad || (() => {});
|
|
21
|
+
this._index = 0;
|
|
22
|
+
|
|
23
|
+
this._debug(`[BufferedReadStream] strategy: ${this._strategy}`);
|
|
24
|
+
this._strategy.initialize(this, source);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async _construct() {
|
|
28
|
+
if (this._source.init) {
|
|
29
|
+
this._debug(`[BufferedReadStream] init source start`);
|
|
30
|
+
await this._source.init();
|
|
31
|
+
this._debug(`[BufferedReadStream] init source done`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async _read() {
|
|
36
|
+
const record = await this._next();
|
|
37
|
+
this.push(record != null ? record : null);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async _next() {
|
|
41
|
+
// TODO: put in action queue to support parallel call
|
|
42
|
+
const bucket = await this._peekBucket();
|
|
43
|
+
if (!bucket) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const record = bucket[this._index++];
|
|
47
|
+
this._state.serve();
|
|
48
|
+
if (this._index >= bucket.length) {
|
|
49
|
+
this._buckets.shift();
|
|
50
|
+
this._index = 0;
|
|
51
|
+
}
|
|
52
|
+
return record;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async peek() {
|
|
56
|
+
// TODO: put in action queue to support parallel call
|
|
57
|
+
const bucket = await this._peekBucket();
|
|
58
|
+
return bucket && bucket[this._index];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _peekBucket() {
|
|
62
|
+
if (this._buckets.isEmpty()) {
|
|
63
|
+
if (this._state.terminated) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
this._loadIfNecessary();
|
|
67
|
+
await this._waitForData();
|
|
68
|
+
}
|
|
69
|
+
return this._buckets.isEmpty() ? undefined : this._buckets.peekFront();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_loadIfNecessaryNextTick() {
|
|
73
|
+
process.nextTick(() => this._loadIfNecessary());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_loadIfNecessary() {
|
|
77
|
+
if (this._shallLoad()) {
|
|
78
|
+
this._load();
|
|
79
|
+
this._loadIfNecessaryNextTick(true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async _load() {
|
|
84
|
+
const request = this._state.request(this._source.request());
|
|
85
|
+
|
|
86
|
+
this._debug(`[BufferedReadStream] Load request: ${request}`);
|
|
87
|
+
const { data, ...info } = await this._source.get(request);
|
|
88
|
+
const response = new Response(request, info);
|
|
89
|
+
this._debug(`[BufferedReadStream] Load response: ${JSON.stringify(response)} => data = ${data && data.length}`);
|
|
90
|
+
|
|
91
|
+
// TODO: support strategy option: keepOrder = false
|
|
92
|
+
this._loads.push(request.index, () => this._resolveLoad(response, data));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_resolveLoad(response, records) {
|
|
96
|
+
const state = this._state;
|
|
97
|
+
const strategy = this._strategy;
|
|
98
|
+
|
|
99
|
+
state.resolve(response);
|
|
100
|
+
this._debug(`[BufferedReadStream] Load resolved: ${response}`);
|
|
101
|
+
|
|
102
|
+
// apply terminate and filter function
|
|
103
|
+
let terminate = false;
|
|
104
|
+
const accepted = [];
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
terminate = terminate || strategy.terminate(record, state);
|
|
107
|
+
if (!terminate && this._filter(record)) {
|
|
108
|
+
state.accept();
|
|
109
|
+
accepted.push(this._transform(record));
|
|
110
|
+
}
|
|
111
|
+
if (terminate) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (terminate || (this._state.pendingLoads === 0 && this._state.exhausted)) {
|
|
116
|
+
state.terminate();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (accepted.length > 0) {
|
|
120
|
+
this._buckets.push(accepted);
|
|
121
|
+
this._onLoad(accepted);
|
|
122
|
+
} else {
|
|
123
|
+
this._loadIfNecessaryNextTick(); // just in case
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (accepted.length > 0 || state.terminated) {
|
|
127
|
+
this._resolveDataPromise();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_waitForData() {
|
|
132
|
+
if (this._state.pendingLoads === 0) {
|
|
133
|
+
throw new Error(`No pending loads.`);
|
|
134
|
+
}
|
|
135
|
+
if (this._dataRes) {
|
|
136
|
+
throw new Error(`Parallel bucket peek.`);
|
|
137
|
+
}
|
|
138
|
+
return (this._dataRes = new Resolution()).promise;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_resolveDataPromise() {
|
|
142
|
+
if (this._dataRes) {
|
|
143
|
+
this._dataRes.resolve();
|
|
144
|
+
this._dataRes = undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_shallLoad() {
|
|
149
|
+
const state = this._state;
|
|
150
|
+
if (state.exhausted) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return !state.exhausted && this._strategy.shallLoad(state);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class Request {
|
|
159
|
+
|
|
160
|
+
constructor(info) {
|
|
161
|
+
this.timestamp = Date.now();
|
|
162
|
+
Object.assign(this, info);
|
|
163
|
+
Object.freeze(this);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toString() {
|
|
167
|
+
return JSON.stringify(this);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class Response {
|
|
173
|
+
|
|
174
|
+
constructor({ timestamp, ...request }, info) {
|
|
175
|
+
const now = this.timestamp = Date.now();
|
|
176
|
+
this.took = now - timestamp;
|
|
177
|
+
Object.assign(this, request);
|
|
178
|
+
Object.assign(this, info);
|
|
179
|
+
Object.freeze(this);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
toString() {
|
|
183
|
+
return JSON.stringify(this);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class Strategy {
|
|
189
|
+
|
|
190
|
+
constructor({
|
|
191
|
+
highWatermark = 1000,
|
|
192
|
+
eagerLoad = false,
|
|
193
|
+
initialize,
|
|
194
|
+
shallLoad,
|
|
195
|
+
terminate,
|
|
196
|
+
} = {}) {
|
|
197
|
+
this.options = Object.freeze({ highWatermark, eagerLoad });
|
|
198
|
+
// overwrite methods
|
|
199
|
+
Object.assign(this, trimObj({ initialize, shallLoad, terminate }));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
initialize(stream) {
|
|
203
|
+
if (this.options.eagerLoad) {
|
|
204
|
+
stream._loadIfNecessaryNextTick();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
shallLoad(state) {
|
|
209
|
+
// TODO: we can have a slower start
|
|
210
|
+
return state.watermark < this.options.highWatermark;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
terminate(record, state) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
toString() {
|
|
218
|
+
return JSON.stringify(this);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class State {
|
|
224
|
+
|
|
225
|
+
constructor() {
|
|
226
|
+
this.records = {
|
|
227
|
+
requested: 0,
|
|
228
|
+
resolved: 0,
|
|
229
|
+
accepted: 0,
|
|
230
|
+
served: 0,
|
|
231
|
+
};
|
|
232
|
+
this.loads = {
|
|
233
|
+
requested: 0,
|
|
234
|
+
resolved: 0,
|
|
235
|
+
};
|
|
236
|
+
this.took = 0;
|
|
237
|
+
this.exhausted = false;
|
|
238
|
+
this.terminated = false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
request({ records, exhaust, ...data }) {
|
|
242
|
+
// note that we may not know how many records will come form this request, so record sum may become NaN
|
|
243
|
+
this.records.requested += records;
|
|
244
|
+
const index = this.loads.requested++;
|
|
245
|
+
if (exhaust) {
|
|
246
|
+
this.exhaust();
|
|
247
|
+
}
|
|
248
|
+
return new Request({ ...data, index, records, exhaust });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
resolve({ records, took, terminate }) {
|
|
252
|
+
// note that we may not know how many records will come form this request, so record sum may become NaN
|
|
253
|
+
this.records.resolved += records;
|
|
254
|
+
this.loads.resolved++;
|
|
255
|
+
this.took += took;
|
|
256
|
+
if (terminate) {
|
|
257
|
+
this.terminate();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
accept() {
|
|
262
|
+
this.records.accepted++;;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
serve() {
|
|
266
|
+
this.records.served++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exhaust() {
|
|
270
|
+
this.exhausted = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
terminate() {
|
|
274
|
+
this.exhaust();
|
|
275
|
+
this.terminated = true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
get pendingLoads() {
|
|
279
|
+
const { loads: loads } = this;
|
|
280
|
+
return loads.requested - loads.resolved;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get pendingRecords() {
|
|
284
|
+
const { records } = this;
|
|
285
|
+
return records.requested - records.resolved;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get unservedRecords() {
|
|
289
|
+
const { records } = this;
|
|
290
|
+
return records.accepted - records.served;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get watermark() {
|
|
294
|
+
let { pendingRecords } = this;
|
|
295
|
+
// TODO: better estimated pendingRecords when NaN
|
|
296
|
+
return (isNaN(pendingRecords) ? 0 : pendingRecords) + this.unservedRecords;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
}
|
|
@@ -12,12 +12,22 @@ export function parse() {
|
|
|
12
12
|
export function stringify() {
|
|
13
13
|
return new Transform({
|
|
14
14
|
transform(chunk, _, callback) {
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
callback(null, JSON.stringify(chunk) + '\n');
|
|
17
|
+
} catch(e) {
|
|
18
|
+
callback(null, `${chunk}` + '\n');
|
|
19
|
+
}
|
|
16
20
|
},
|
|
17
21
|
writableObjectMode: true,
|
|
18
22
|
});
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
export async function pipeline(...streams) {
|
|
26
|
+
return new Promise((resolve, reject) =>
|
|
27
|
+
_pipeline(...streams, err => err ? reject(err) : resolve())
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export async function pipelineToStdout(...streams) {
|
|
22
32
|
return new Promise((resolve, reject) =>
|
|
23
33
|
_pipeline(...streams, err => err ? reject(err) : resolve())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Writable } from 'stream';
|
|
2
|
+
|
|
3
|
+
export default class OutputStream extends Writable {
|
|
4
|
+
|
|
5
|
+
constructor({
|
|
6
|
+
out = process.stdout,
|
|
7
|
+
err = process.stderr,
|
|
8
|
+
format,
|
|
9
|
+
objectMode = true,
|
|
10
|
+
} = {}) {
|
|
11
|
+
super({
|
|
12
|
+
objectMode,
|
|
13
|
+
});
|
|
14
|
+
this._format = format || (v => `${v}`);
|
|
15
|
+
this._out = out;
|
|
16
|
+
this._err = err;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_write(record, _, next) {
|
|
20
|
+
this._out.write(this._format(record) + '\n');
|
|
21
|
+
next();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
}
|
package/src/string.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// From:
|
|
2
|
+
// https://github.com/jonschlinkert/pad-right
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PAD = ' ';
|
|
5
|
+
const DEFAULT_PAD_5 = DEFAULT_PAD.repeat(5);
|
|
6
|
+
const DEFAULT_PAD_25 = DEFAULT_PAD.repeat(25);
|
|
7
|
+
|
|
8
|
+
function pad(left, val, num, str) {
|
|
9
|
+
const diff = num - val.length;
|
|
10
|
+
if (diff <= 0) {
|
|
11
|
+
return val;
|
|
12
|
+
}
|
|
13
|
+
const padding = buildPadding(diff, str);
|
|
14
|
+
return left ? padding + val : val + padding;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildPadding(count, str) {
|
|
18
|
+
// Breakpoints based on benchmarks to use the fastest approach
|
|
19
|
+
// for the given number of zeros
|
|
20
|
+
return str ? str.repeat(count) :
|
|
21
|
+
count <= 5 ? DEFAULT_PAD_5.slice(0, count) :
|
|
22
|
+
count <= 25 ? DEFAULT_PAD_25.slice(0, count) :
|
|
23
|
+
DEFAULT_PAD.repeat(count);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function padLeft(val, num, str) {
|
|
27
|
+
return pad(true, val, num, str);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function padRight(val, num, str) {
|
|
31
|
+
return pad(false, val, num, str);
|
|
32
|
+
}
|