@miso.ai/server-commons 0.5.4-beta.4 → 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/index.js +1 -1
- package/src/stream/buffered-read.js +299 -0
- package/src/stream/index.js +3 -0
- package/src/{stream.js → stream/misc.js} +0 -0
- package/src/stream/output.js +24 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export * from './file.js';
|
|
|
5
5
|
export * from './config.js';
|
|
6
6
|
export * from './async.js';
|
|
7
7
|
|
|
8
|
-
export * as stream from './stream.js';
|
|
8
|
+
export * as stream from './stream/index.js';
|
|
9
9
|
export * as log from './log.js';
|
|
10
10
|
|
|
11
11
|
export { default as TaskQueue } from './task-queue.js';
|
|
@@ -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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|