@miso.ai/server-commons 0.6.6-beta.16 → 0.6.6-beta.18
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/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-write.js +9 -1
- package/src/stream/easy-transform.js +66 -0
- package/src/stream/index.js +1 -0
- package/src/stream/misc.js +7 -2
- package/test/channel.test.js +268 -0
- package/test/easy-transform.js +64 -0
|
@@ -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
|
@@ -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
|
@@ -8,3 +8,4 @@ export { default as LogUpdateStream } from './log-update.js';
|
|
|
8
8
|
export { default as DiffStream } from './diff.js';
|
|
9
9
|
export { default as ParallelTransform } from './parallel-transform.js';
|
|
10
10
|
export { default as XmlParseStream } from './xml.js';
|
|
11
|
+
export { default as EasyTransform } from './easy-transform.js';
|
package/src/stream/misc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Transform
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import { pipeline as _pipeline } from 'stream/promises';
|
|
2
3
|
import { chain as _chain } from 'stream-chain';
|
|
3
4
|
|
|
4
5
|
export const chain = _chain;
|
|
@@ -34,8 +35,12 @@ export function stringify() {
|
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use native stream/promises pipeline instead.
|
|
40
|
+
*/
|
|
37
41
|
export async function pipeline(...streams) {
|
|
38
|
-
|
|
42
|
+
const flattened = streams.length === 1 && Array.isArray(streams[0]) ? streams[0] : streams;
|
|
43
|
+
return _pipeline(flattened.filter(Boolean));
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
export async function collect(stream) {
|