@miso.ai/server-commons 0.6.6-beta.2 → 0.6.6-beta.20
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 +3 -2
- package/src/channel/buffer.js +115 -0
- package/src/channel/channel.js +105 -0
- package/src/channel/component.js +71 -0
- package/src/channel/constants.js +6 -0
- package/src/channel/downgrade.js +27 -0
- package/src/channel/events.js +117 -0
- package/src/channel/index.js +8 -0
- package/src/channel/sink-gate.js +39 -0
- package/src/channel/sink.js +103 -0
- package/src/channel/time.js +105 -0
- package/src/channel/transform.js +83 -0
- package/src/channel/upgrade.js +99 -0
- package/src/channel/write.js +154 -0
- package/src/index.js +1 -0
- package/src/stream/buffered-read.js +6 -4
- package/src/stream/buffered-write.js +9 -1
- package/src/stream/easy-transform.js +66 -0
- package/src/stream/index.js +2 -0
- package/src/stream/joint-stream.js +23 -0
- package/src/stream/misc.js +10 -2
- package/test/channel.test.js +268 -0
- package/test/easy-transform.js +64 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { delay } from '../async.js';
|
|
2
|
+
|
|
3
|
+
export default class TimeTracker {
|
|
4
|
+
|
|
5
|
+
constructor(timestamp = Date.now()) {
|
|
6
|
+
Object.defineProperty(this, 'constructedAt', { value: timestamp });
|
|
7
|
+
this._firstWriteAt = undefined;
|
|
8
|
+
this._write = 0;
|
|
9
|
+
this._pausedAt = undefined;
|
|
10
|
+
this._willResumeAt = undefined;
|
|
11
|
+
this._paused = 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// operation //
|
|
15
|
+
setFirstWrite(timestamp = Date.now()) {
|
|
16
|
+
if (this._firstWriteAt !== undefined) {
|
|
17
|
+
throw new Error(`First write time already set: ${this._firstWriteAt}`);
|
|
18
|
+
}
|
|
19
|
+
this._firstWriteAt = timestamp;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addWrite(duration) {
|
|
23
|
+
this._write += duration;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async pause(pauseTime, pausedAt = Date.now()) {
|
|
27
|
+
if (pauseTime <= 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (this._firstWriteAt === undefined || this._pausedAt !== undefined) {
|
|
31
|
+
throw new Error(`Cannot pause() before first write or when paused.`);
|
|
32
|
+
}
|
|
33
|
+
this._pausedAt = pausedAt;
|
|
34
|
+
this._willResumeAt = pausedAt + pauseTime;
|
|
35
|
+
await delay(pauseTime);
|
|
36
|
+
this.paused && this.resume();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resume(resumeAt = Date.now()) {
|
|
40
|
+
if (this._firstWriteAt === undefined || this._pausedAt === undefined) {
|
|
41
|
+
throw new Error(`Cannot resume() before first write or when not paused.`);
|
|
42
|
+
}
|
|
43
|
+
this._paused += resumeAt - this._pausedAt;
|
|
44
|
+
this._pausedAt = this._willResumeAt = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// stats //
|
|
48
|
+
get paused() {
|
|
49
|
+
return !!this._pausedAt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get firstWriteAt() {
|
|
53
|
+
return this._firstWriteAt;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get pausedAt() {
|
|
57
|
+
return this._pausedAt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get willResumeAt() {
|
|
61
|
+
return this._willResumeAt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get write() {
|
|
65
|
+
return this._write;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
total(timestamp) {
|
|
69
|
+
return timestamp - this.constructedAt;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
waiting(timestamp) {
|
|
73
|
+
return (this._firstWriteAt !== undefined ? this._firstWriteAt : timestamp) - this.constructedAt;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
elapsed(timestamp) {
|
|
77
|
+
return this._firstWriteAt === undefined ? 0 : timestamp - this._firstWriteAt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
paused(timestamp) {
|
|
81
|
+
return this._paused + (this._pausedAt !== undefined ? timestamp - this._pausedAt : 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
running(timestamp) {
|
|
85
|
+
return this.elapsed(timestamp) - this.paused(timestamp);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
snapshot(timestamp = Date.now()) {
|
|
89
|
+
const { constructedAt, firstWriteAt, pausedAt, willResumeAt, write } = this;
|
|
90
|
+
return Object.freeze(trimObj({
|
|
91
|
+
currentTime: timestamp,
|
|
92
|
+
constructedAt,
|
|
93
|
+
firstWriteAt,
|
|
94
|
+
pausedAt,
|
|
95
|
+
willResumeAt,
|
|
96
|
+
write,
|
|
97
|
+
total: this.total(timestamp),
|
|
98
|
+
waiting: this.waiting(timestamp),
|
|
99
|
+
elapsed: this.elapsed(timestamp),
|
|
100
|
+
paused: this.paused(timestamp),
|
|
101
|
+
running: this.running(timestamp),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Channel from './channel.js';
|
|
2
|
+
import { writeChannelInfo } from './events.js';
|
|
3
|
+
import { trimObj } from '../object.js';
|
|
4
|
+
|
|
5
|
+
export default class TransformChannel extends Channel {
|
|
6
|
+
|
|
7
|
+
constructor({
|
|
8
|
+
transform,
|
|
9
|
+
name = 'transform',
|
|
10
|
+
...options
|
|
11
|
+
}) {
|
|
12
|
+
super({
|
|
13
|
+
name,
|
|
14
|
+
...options,
|
|
15
|
+
});
|
|
16
|
+
if (transform) {
|
|
17
|
+
if (typeof transform !== 'function') {
|
|
18
|
+
throw new Error(`Transform must be a function.`);
|
|
19
|
+
}
|
|
20
|
+
this._transformData = transform;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async _transformData(event) {
|
|
25
|
+
throw new Error('Not implemented');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _runCustomTransform(event) {
|
|
29
|
+
switch (event.type) {
|
|
30
|
+
case 'data':
|
|
31
|
+
await this._runData(event);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await super._runCustomTransform(event);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async _runData(event) {
|
|
38
|
+
let transformed;
|
|
39
|
+
try {
|
|
40
|
+
transformed = this._transformData(event);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// write error to source event and pass through
|
|
43
|
+
const { type, message } = error;
|
|
44
|
+
transformed = {
|
|
45
|
+
errors: writeChannelInfo(this, [{ type, message }]),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
this._validateEvent(transformed);
|
|
49
|
+
this.out.write(this._createDataEvent(event, transformed));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_createDataEvent({ sources = [], logs, warnings, errors, ...event } = {}, transformed) {
|
|
53
|
+
sources = [...sources, event];
|
|
54
|
+
logs = mergeInfoArray(logs, writeChannelInfo(this, transformed.logs));
|
|
55
|
+
warnings = mergeInfoArray(warnings, writeChannelInfo(this, transformed.warnings));
|
|
56
|
+
errors = mergeInfoArray(errors, writeChannelInfo(this, transformed.errors));
|
|
57
|
+
return trimObj({
|
|
58
|
+
...transformed,
|
|
59
|
+
type: 'data',
|
|
60
|
+
sources,
|
|
61
|
+
logs,
|
|
62
|
+
warnings,
|
|
63
|
+
errors,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_validateEvent(event) {
|
|
68
|
+
if (typeof event !== 'object') {
|
|
69
|
+
throw new Error('the _transformData method must return an event');
|
|
70
|
+
}
|
|
71
|
+
if (event.payload === undefined && !event.ignored) {
|
|
72
|
+
throw new Error('Event payload is undefined');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeInfoArray(a = [], b = []) {
|
|
79
|
+
if (a.length === 0 && b.length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return [...a, ...b];
|
|
83
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ChannelBase } from './component.js';
|
|
2
|
+
import { ChannelOutput, createStartEvent, createEndEvent } from './events.js';
|
|
3
|
+
|
|
4
|
+
export default class UpgradeChannel extends ChannelBase {
|
|
5
|
+
|
|
6
|
+
constructor({ name = 'upgrade', upgrade, objectMode, ...options } = {}) {
|
|
7
|
+
super({
|
|
8
|
+
...options,
|
|
9
|
+
name,
|
|
10
|
+
writableObjectMode: objectMode,
|
|
11
|
+
readableObjectMode: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
this._objectMode = objectMode;
|
|
15
|
+
|
|
16
|
+
if (upgrade) {
|
|
17
|
+
if (typeof upgrade !== 'function') {
|
|
18
|
+
throw new Error('Upgrade must be a function');
|
|
19
|
+
}
|
|
20
|
+
this._upgrade = upgrade.bind(this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this._started = false;
|
|
24
|
+
this.out = new ChannelOutput(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async _parse(payload, encoding) {
|
|
28
|
+
if (this._objectMode) {
|
|
29
|
+
return payload;
|
|
30
|
+
}
|
|
31
|
+
if (Buffer.isBuffer(payload)) {
|
|
32
|
+
payload = payload.toString().trim();
|
|
33
|
+
}
|
|
34
|
+
if (this._options.asId) {
|
|
35
|
+
// convert to string
|
|
36
|
+
payload = `${payload}`.trim();
|
|
37
|
+
} else {
|
|
38
|
+
if (typeof payload === 'string' && payload.charAt(0) === '{' && payload.charAt(payload.length - 1) === '}') {
|
|
39
|
+
// should be an object
|
|
40
|
+
// TODO: ad-hoc!
|
|
41
|
+
payload = JSON.parse(payload);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return payload;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async _id(payload) {
|
|
48
|
+
if (this._options.asId) {
|
|
49
|
+
return payload;
|
|
50
|
+
}
|
|
51
|
+
if (typeof payload !== 'object') {
|
|
52
|
+
throw new Error('Cannot determine id from payload');
|
|
53
|
+
}
|
|
54
|
+
const id = this._options.idField;
|
|
55
|
+
if (id) {
|
|
56
|
+
return payload[id];
|
|
57
|
+
}
|
|
58
|
+
// ad-hoc!
|
|
59
|
+
return payload.product_id || payload.user_id || payload.id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async _upgrade(payload) {
|
|
63
|
+
const id = await this._id(payload);
|
|
64
|
+
if (!id) {
|
|
65
|
+
throw new Error('Id not found in payload');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
payload,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_createStartEvent() {
|
|
74
|
+
return createStartEvent(this);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_createEndEvent() {
|
|
78
|
+
return createEndEvent(this);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async _runTransform(payload, encoding) {
|
|
82
|
+
// first read -> write start event
|
|
83
|
+
if (!this._started) {
|
|
84
|
+
this._started = true;
|
|
85
|
+
this.out.write(this._createStartEvent());
|
|
86
|
+
}
|
|
87
|
+
payload = await this._parse(payload, encoding);
|
|
88
|
+
this.out.write({
|
|
89
|
+
...(await this._upgrade(payload)),
|
|
90
|
+
type: 'data',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async _runFlush() {
|
|
95
|
+
// write end event
|
|
96
|
+
this.out.write(this._createEndEvent());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import Channel from './channel.js';
|
|
2
|
+
import TimeTracker from './time.js';
|
|
3
|
+
import WriteChannelBuffer from './buffer.js';
|
|
4
|
+
import WriteChannelSinkGate from './sink-gate.js';
|
|
5
|
+
import WriteChannelSink from './sink.js';
|
|
6
|
+
|
|
7
|
+
function normalizeBuffer(buffer) {
|
|
8
|
+
if (typeof buffer !== 'object') {
|
|
9
|
+
throw new Error('A buffer instance or buffer options is required.');
|
|
10
|
+
}
|
|
11
|
+
if (typeof buffer.push !== 'function') {
|
|
12
|
+
buffer = new WriteChannelBuffer(buffer);
|
|
13
|
+
}
|
|
14
|
+
return buffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeSink(sink) {
|
|
18
|
+
if (typeof sink !== 'object') {
|
|
19
|
+
throw new Error('A sink instance or sink options is required.');
|
|
20
|
+
}
|
|
21
|
+
if (typeof sink._write !== 'function') {
|
|
22
|
+
sink = new WriteChannelSink(sink);
|
|
23
|
+
}
|
|
24
|
+
return sink;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSinkGate(sinkGate) {
|
|
28
|
+
if (!sinkGate) {
|
|
29
|
+
return undefined; // can be undefined
|
|
30
|
+
}
|
|
31
|
+
if (typeof sinkGate.blockedTime !== 'function') {
|
|
32
|
+
sinkGate = new WriteChannelSinkGate(sinkGate);
|
|
33
|
+
}
|
|
34
|
+
return sinkGate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO: we may as well split the buffering to a preceding channel
|
|
38
|
+
|
|
39
|
+
export default class WriteChannel extends Channel {
|
|
40
|
+
|
|
41
|
+
constructor({ buffer, sink, sinkGate, ...options } = {}) {
|
|
42
|
+
super(options);
|
|
43
|
+
this._payloadBuffer = normalizeBuffer(buffer);
|
|
44
|
+
this._sink = normalizeSink(sink);
|
|
45
|
+
this._sink._channel = this;
|
|
46
|
+
this._sinkGate = normalizeSinkGate(sinkGate);
|
|
47
|
+
this._time = new TimeTracker();
|
|
48
|
+
this._index = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get pulse() {
|
|
52
|
+
return {
|
|
53
|
+
...super.pulse,
|
|
54
|
+
status: this._events.end ? 'finished' : this._time.firstWriteAt === undefined ? 'waiting' : this._time.paused ? 'paused' : 'running',
|
|
55
|
+
time: this._time.snapshot(Date.now()),
|
|
56
|
+
write: this._sink.state,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async _runCustomTransform(event) {
|
|
61
|
+
switch (event.type) {
|
|
62
|
+
case 'data':
|
|
63
|
+
if (event.payload !== undefined) {
|
|
64
|
+
await this._runData(event);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
await super._runCustomTransform(event);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _runData(event) {
|
|
73
|
+
await this.writeData(event);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async writeData(event) {
|
|
77
|
+
// TODO: dedupe
|
|
78
|
+
await this._dispatchAll(this._payloadBuffer.push(event));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async _runFlush() {
|
|
82
|
+
await this._dispatchAll(this._payloadBuffer.flush());
|
|
83
|
+
await this._sink.finished;
|
|
84
|
+
|
|
85
|
+
// end event is here
|
|
86
|
+
await super._runFlush();
|
|
87
|
+
|
|
88
|
+
this._sink.destroy();
|
|
89
|
+
this._payloadBuffer.destroy();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async _pauseIfNecessary() {
|
|
93
|
+
if (!this._sinkGate) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const pauseTime = this._sinkGate.blockedTime(this._sink.state);
|
|
97
|
+
await this._time.pause(pauseTime);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async _dispatchAll(requests) {
|
|
101
|
+
for (const request of requests) {
|
|
102
|
+
await this._pauseIfNecessary();
|
|
103
|
+
this._dispatch(request); // don't wait
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _dispatch(request) {
|
|
108
|
+
const index = this._index++;
|
|
109
|
+
if (this._time.firstWriteAt === undefined) {
|
|
110
|
+
this._time.setFirstWrite();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { data: _0, payload: _1, ...restOfRequest } = request;
|
|
114
|
+
// TODO: we want to write real request/response event, so these have to be buried into the sink
|
|
115
|
+
this.out.write({
|
|
116
|
+
...restOfRequest,
|
|
117
|
+
type: 'request',
|
|
118
|
+
index,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { successful, failed, ...response } = await this._sink.write(request);
|
|
122
|
+
this._time.addWrite(response.timestamp - request.timestamp);
|
|
123
|
+
|
|
124
|
+
this.out.write({
|
|
125
|
+
...response,
|
|
126
|
+
type: 'response',
|
|
127
|
+
index,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// write write event
|
|
131
|
+
const successfulIds = (successful && successful.data && successful.data.map(d => d.id)) || [];
|
|
132
|
+
if (successfulIds.length > 0) {
|
|
133
|
+
this.out.write({
|
|
134
|
+
...this._createWriteEvent({ index, request, response, successfulIds }),
|
|
135
|
+
type: 'write',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// TODO: dedupe
|
|
139
|
+
|
|
140
|
+
// recover failed data events and pass through
|
|
141
|
+
for (const event of failed.data || []) {
|
|
142
|
+
// TODO: review this
|
|
143
|
+
this.out.pass(event);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_createWriteEvent({ index, successfulIds }) {
|
|
148
|
+
return {
|
|
149
|
+
index,
|
|
150
|
+
ids: successfulIds,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
package/src/index.js
CHANGED
|
@@ -91,10 +91,10 @@ export default class BufferedReadStream extends Readable {
|
|
|
91
91
|
this._debug(`[BufferedReadStream] Load response: ${JSON.stringify(response)} => data = ${data && data.length}`);
|
|
92
92
|
|
|
93
93
|
// TODO: support strategy option: keepOrder = false
|
|
94
|
-
this._loads.push(request.index, () => this._resolveLoad(response, data));
|
|
94
|
+
this._loads.push(request.index, () => this._resolveLoad(response, data, request));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
_resolveLoad(response, records) {
|
|
97
|
+
_resolveLoad(response, records, request) {
|
|
98
98
|
const state = this._state;
|
|
99
99
|
const strategy = this._strategy;
|
|
100
100
|
|
|
@@ -104,15 +104,17 @@ export default class BufferedReadStream extends Readable {
|
|
|
104
104
|
// apply terminate and filter function
|
|
105
105
|
let terminate = false;
|
|
106
106
|
const accepted = [];
|
|
107
|
+
let index = 0;
|
|
107
108
|
for (const record of records) {
|
|
108
|
-
terminate = terminate || strategy.terminate(record, state);
|
|
109
|
-
if (!terminate && this._filter(record)) {
|
|
109
|
+
terminate = terminate || strategy.terminate(record, state, request, index);
|
|
110
|
+
if (!terminate && this._filter(record, state, request, index)) {
|
|
110
111
|
state.accept();
|
|
111
112
|
accepted.push(this._transform(record));
|
|
112
113
|
}
|
|
113
114
|
if (terminate) {
|
|
114
115
|
break;
|
|
115
116
|
}
|
|
117
|
+
index++;
|
|
116
118
|
}
|
|
117
119
|
if (terminate || (this._state.pendingLoads === 0 && this._state.exhausted)) {
|
|
118
120
|
state.terminate();
|
|
@@ -183,7 +183,7 @@ export default class BufferedWriteStream extends Transform {
|
|
|
183
183
|
result: failed ? 'failed' : 'successful',
|
|
184
184
|
index: request.index,
|
|
185
185
|
records: request.records,
|
|
186
|
-
recovered: response.recovered
|
|
186
|
+
recovered: summarizeRecovered(response.recovered),
|
|
187
187
|
bytes: request.bytes,
|
|
188
188
|
time: response.timestamp - request.timestamp,
|
|
189
189
|
});
|
|
@@ -194,3 +194,11 @@ export default class BufferedWriteStream extends Transform {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
function summarizeRecovered(recovered) {
|
|
199
|
+
if (!recovered) {
|
|
200
|
+
return { records: 0, bytes: 0 };
|
|
201
|
+
}
|
|
202
|
+
const { records, bytes } = recovered;
|
|
203
|
+
return { records, bytes };
|
|
204
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
|
|
3
|
+
export default class EasyTransform extends Transform {
|
|
4
|
+
|
|
5
|
+
constructor({ transform, flush, ...options } = {}) {
|
|
6
|
+
super(options);
|
|
7
|
+
if (transform) {
|
|
8
|
+
if (typeof transform !== 'function') {
|
|
9
|
+
throw new Error(`transform must be a function`);
|
|
10
|
+
}
|
|
11
|
+
this._runTransform = transform.bind(this);
|
|
12
|
+
}
|
|
13
|
+
if (flush) {
|
|
14
|
+
if (typeof flush !== 'function') {
|
|
15
|
+
throw new Error(`flush must be a function`);
|
|
16
|
+
}
|
|
17
|
+
this._runFlush = flush.bind(this);
|
|
18
|
+
}
|
|
19
|
+
this._buffer = [];
|
|
20
|
+
this._callback = undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// customization //
|
|
24
|
+
async _runTransform(chunk, encoding) {
|
|
25
|
+
throw new Error(`Unimplemented.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _runFlush() {}
|
|
29
|
+
|
|
30
|
+
// stream internal //
|
|
31
|
+
async _transform(chunk, encoding, next) {
|
|
32
|
+
await this._runTransform(chunk, encoding);
|
|
33
|
+
this._callback = next;
|
|
34
|
+
this._flushBuffer();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async _flush(done) {
|
|
38
|
+
await this._runFlush();
|
|
39
|
+
this._callback = done;
|
|
40
|
+
this._flushBuffer();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_read() {
|
|
44
|
+
this._flushBuffer();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_pushBuffer(chunk, encoding) {
|
|
48
|
+
this._buffer.push([chunk, encoding]);
|
|
49
|
+
this._flushBuffer();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_flushBuffer() {
|
|
53
|
+
const buffer = this._buffer;
|
|
54
|
+
while (buffer.length > 0) {
|
|
55
|
+
if (!this.push(...buffer.shift())) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (this._callback) {
|
|
60
|
+
const callback = this._callback;
|
|
61
|
+
this._callback = undefined;
|
|
62
|
+
callback();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}
|
package/src/stream/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './misc.js';
|
|
2
2
|
export * from './transform.js';
|
|
3
|
+
export * from './joint-stream.js';
|
|
3
4
|
export { default as BufferedReadStream } from './buffered-read.js';
|
|
4
5
|
export { default as BufferedWriteStream } from './buffered-write.js';
|
|
5
6
|
export { default as OutputStream } from './output.js';
|
|
@@ -7,3 +8,4 @@ export { default as LogUpdateStream } from './log-update.js';
|
|
|
7
8
|
export { default as DiffStream } from './diff.js';
|
|
8
9
|
export { default as ParallelTransform } from './parallel-transform.js';
|
|
9
10
|
export { default as XmlParseStream } from './xml.js';
|
|
11
|
+
export { default as EasyTransform } from './easy-transform.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
|
|
3
|
+
export function joinStreams(streams, joinFn = defaultJoinFn) {
|
|
4
|
+
if (typeof joinFn !== 'function') {
|
|
5
|
+
throw new Error(`joinFn must be a function: ${joinFn}`);
|
|
6
|
+
}
|
|
7
|
+
return Readable.from(generate(streams, joinFn));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function defaultJoinFn(...args) {
|
|
11
|
+
return args;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function * generate(streams, joinFn) {
|
|
15
|
+
const iterators = streams.map(stream => stream[Symbol.asyncIterator]());
|
|
16
|
+
while (true) {
|
|
17
|
+
const entries = await Promise.all(iterators.map(iterator => iterator.next()));
|
|
18
|
+
if (entries.every(entry => entry.done)) {
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
yield joinFn(...entries.map(entry => entry.value));
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/stream/misc.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { Transform
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import { pipeline as _pipeline } from 'stream/promises';
|
|
3
|
+
import { chain as _chain } from 'stream-chain';
|
|
4
|
+
|
|
5
|
+
export const chain = _chain;
|
|
2
6
|
|
|
3
7
|
export function parse({ lenient } = {}) {
|
|
4
8
|
const parsnFn = lenient ? parseJsonIfPossible : JSON.parse;
|
|
@@ -31,8 +35,12 @@ export function stringify() {
|
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use native stream/promises pipeline instead.
|
|
40
|
+
*/
|
|
34
41
|
export async function pipeline(...streams) {
|
|
35
|
-
|
|
42
|
+
const flattened = streams.length === 1 && Array.isArray(streams[0]) ? streams[0] : streams;
|
|
43
|
+
return _pipeline(flattened.filter(Boolean));
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export async function collect(stream) {
|