@miso.ai/server-commons 0.6.6-beta.17 → 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 CHANGED
@@ -24,5 +24,5 @@
24
24
  "uuid": "^9.0.0",
25
25
  "yargs": "^17.5.1"
26
26
  },
27
- "version": "0.6.6-beta.17"
27
+ "version": "0.6.6-beta.18"
28
28
  }
@@ -0,0 +1,115 @@
1
+ function defaultSerialize({ payload }) {
2
+ return typeof payload === 'string' ? payload : JSON.stringify(payload);
3
+ }
4
+
5
+ function normalizeOptions({
6
+ payloadPrefix = '[',
7
+ payloadSuffix = ']',
8
+ payloadDelimiter = ',',
9
+ serialize = defaultSerialize,
10
+ recordCap,
11
+ byteCap,
12
+ } = {}) {
13
+ if (!recordCap && !byteCap) {
14
+ throw new Error('At least one of recordCap or byteCap must be specified.');
15
+ }
16
+ if (typeof serialize !== 'function') {
17
+ throw new Error('serialize must be a function.');
18
+ }
19
+ return {
20
+ payloadPrefix,
21
+ payloadSuffix,
22
+ payloadDelimiter,
23
+ serialize,
24
+ recordCap,
25
+ byteCap,
26
+ };
27
+ }
28
+
29
+ export default class WriteChannelBuffer {
30
+
31
+ constructor(options) {
32
+ this._options = options = normalizeOptions(options);
33
+ this._affixSize = (options.payloadPrefix.length + options.payloadSuffix.length) * 2;
34
+ this._clear();
35
+ }
36
+
37
+ _clear() {
38
+ this._records = 0;
39
+ this._bytes = this._affixSize;
40
+ this._payload = this._options.payloadPrefix;
41
+ this._data = [];
42
+ }
43
+
44
+ /**
45
+ * Returns true if the buffer is empty.
46
+ */
47
+ get empty() {
48
+ return this._records === 0;
49
+ }
50
+
51
+ /**
52
+ * Returns true if the buffer is full.
53
+ */
54
+ get full() {
55
+ const { recordCap, byteCap } = this._options;
56
+ return (recordCap !== undefined && this._records >= recordCap) ||
57
+ (byteCap !== undefined && this._bytes >= byteCap);
58
+ }
59
+
60
+ /**
61
+ * Take a data event and push it into the buffer.
62
+ * Returns an array of requests if the buffer is full, or an empty array otherwise.
63
+ * @returns An array of requests.
64
+ */
65
+ push(event) {
66
+ let dispatches = [];
67
+ const serializedPayload = this._options.serialize(event);
68
+ const contentSize = serializedPayload.length * 2;
69
+ const { empty } = this;
70
+
71
+ if (!empty) {
72
+ const delimiterSize = this._options.payloadDelimiter.length * 2;
73
+ if (this._bytes + delimiterSize + contentSize > this._options.byteCap) {
74
+ // if buffer is not empty and exceeds bytes limit after pushing, then flush right now
75
+ dispatches = this.flush();
76
+ } else {
77
+ // non-empty, not flushed
78
+ this._bytes += delimiterSize;
79
+ this._payload += this._options.payloadDelimiter;
80
+ }
81
+ }
82
+
83
+ this._records++;
84
+ this._bytes += contentSize;
85
+ this._payload += serializedPayload;
86
+ this._data.push(event);
87
+
88
+ // it's possible that the new record is so large that the buffer is instantly full even after a flush
89
+ // in this case, the buffer generates two dispatches from the push
90
+ if (this.full) {
91
+ dispatches = [...dispatches, ...this.flush()];
92
+ }
93
+
94
+ return dispatches;
95
+ }
96
+
97
+ /**
98
+ * Flush the buffer, which returns an array of requests (which can be empty) and reset the buffer to empty state.
99
+ * @returns An array of requests.
100
+ */
101
+ flush() {
102
+ if (this.empty) {
103
+ return [];
104
+ }
105
+ const records = this._records;
106
+ const bytes = this._bytes;
107
+ const payload = this._payload + this._options.payloadSuffix;
108
+ const data = this._data;
109
+ this._clear();
110
+ return [{ payload, records, bytes, data }];
111
+ }
112
+
113
+ destroy() {}
114
+
115
+ }
@@ -0,0 +1,105 @@
1
+ import { ChannelBase } from './component.js';
2
+ import { LOG_LEVEL, ChannelOutput, createStartEvent, createEndEvent } from './events.js';
3
+
4
+ export default class Channel extends ChannelBase {
5
+
6
+ constructor({ transform, flush, ...options } = {}) {
7
+ super(options);
8
+
9
+ if (transform) {
10
+ if (typeof transform !== 'function') {
11
+ throw new Error('Transform must be a function');
12
+ }
13
+ this._runCustomTransform = transform.bind(this);
14
+ }
15
+ if (flush) {
16
+ if (typeof flush !== 'function') {
17
+ throw new Error('Flush must be a function');
18
+ }
19
+ this._runCustomFlush = flush.bind(this);
20
+ }
21
+
22
+ this._upstream = {};
23
+ this.out = new ChannelOutput(this);
24
+ }
25
+
26
+ // customization //
27
+ async _runCustomTransform(event) {
28
+ this.out.pass(event);
29
+ }
30
+
31
+ async _runCustomFlush() {}
32
+
33
+ async _runFlush() {
34
+ await this._runCustomFlush();
35
+ // clear heartbeat if necessary
36
+ // TODO
37
+ if (!this._upstream.start) {
38
+ // not seen start event from upstream, so the upstream is empty
39
+ throw new Error('Start event not received from upstream');
40
+ }
41
+ if (!this._upstream.end) {
42
+ // not seen end event from upstream, warn
43
+ this.out.log(LOG_LEVEL.WARNING, 'End event not received from upstream');
44
+ }
45
+ // write my own end event
46
+ this.out.write(this._createEndEvent(this._upstream.end));
47
+
48
+ await super._runFlush();
49
+ }
50
+
51
+ _readUpstreamStartEvent(event) {
52
+ // take the heartbeat setting
53
+ // TODO
54
+ }
55
+
56
+
57
+
58
+ // internal //
59
+ async _runTransform(event) {
60
+ // expect the start event from upstream
61
+ if (!this._upstream.start) {
62
+ if (event.type !== 'start') {
63
+ throw new Error(`The first event received from upstream must be a start event: ${JSON.stringify(event)}`);
64
+ }
65
+ this._upstream.start = event;
66
+ this.out.write(this._createStartEvent(event));
67
+ this._readUpstreamStartEvent(event);
68
+ return;
69
+ }
70
+ switch (event.type) {
71
+ case 'start':
72
+ throw new Error(`Received a second start event from upstream: ${JSON.stringify(event)}`);
73
+ case 'end':
74
+ if (this._upstream.end) {
75
+ // seen end event from upstream, warn
76
+ this.out.log(LOG_LEVEL.WARNING, `Received a second end event from upstream: ${JSON.stringify(event)}`);
77
+ } else {
78
+ this._upstream.end = event;
79
+ }
80
+ break;
81
+ default:
82
+ await this._runCustomTransform(event);
83
+ }
84
+ }
85
+
86
+ _writePulseEvent() {
87
+ try {
88
+ this.out.write({
89
+ type: 'pulse',
90
+ ...this.pulse,
91
+ });
92
+ } catch (error) {
93
+ this.out.log(LOG_LEVEL.ERROR, `Failed to write pulse event: ${error.message}`, { error });
94
+ }
95
+ }
96
+
97
+ _createStartEvent(event) {
98
+ return createStartEvent(this, event);
99
+ }
100
+
101
+ _createEndEvent(event) {
102
+ return createEndEvent(this, event);
103
+ }
104
+
105
+ }
@@ -0,0 +1,71 @@
1
+ import EasyTransform from '../stream/easy-transform.js';
2
+ import { trimObj } from '../object.js';
3
+ import { stackEventInfo } from './events.js';
4
+
5
+ // TODO: camelCase to kebab-case on options keys
6
+ function exportOptions(options) {
7
+ return trimObj({ ...options });
8
+ }
9
+
10
+ export class ChannelComponent {
11
+
12
+ constructor(options = {}) {
13
+ this._options = this._normalizeOptions(options);
14
+ }
15
+
16
+ _normalizeOptions({
17
+ ...options
18
+ }) {
19
+ return trimObj({
20
+ ...options,
21
+ });
22
+ }
23
+
24
+ get options() {
25
+ return exportOptions(this._options);
26
+ }
27
+
28
+ }
29
+
30
+ // TODO: by mixin
31
+
32
+ export class ChannelBase extends EasyTransform {
33
+
34
+ constructor({ name, ...options } = {}) {
35
+ super({ ...options, objectMode: true });
36
+ this._options = this._normalizeOptions(options);
37
+ this._name = name || this.constructor.name;
38
+ }
39
+
40
+ _normalizeOptions({
41
+ objectMode,
42
+ writableObjectMode,
43
+ readableObjectMode,
44
+ ...options
45
+ }) {
46
+ return trimObj({
47
+ ...options,
48
+ });
49
+ }
50
+
51
+ get name() {
52
+ return this._name;
53
+ }
54
+
55
+ get options() {
56
+ return exportOptions(this._options);
57
+ }
58
+
59
+ get pulse() {
60
+ return {};
61
+ }
62
+
63
+ get result() {
64
+ return {};
65
+ }
66
+
67
+ _stackEventInfo(event, field, info) {
68
+ return stackEventInfo(this, event, field, info);
69
+ }
70
+
71
+ }
@@ -0,0 +1,6 @@
1
+ export const LOG_LEVEL = {
2
+ DEBUG: 'debug',
3
+ INFO: 'info',
4
+ WARNING: 'warning',
5
+ ERROR: 'error',
6
+ };
@@ -0,0 +1,27 @@
1
+ import EasyTransform from '../stream/easy-transform.js';
2
+
3
+ export default class DowngradeChannel extends EasyTransform {
4
+
5
+ constructor({ objectMode, ...options } = {}) {
6
+ super({
7
+ ...options,
8
+ writableObjectMode: true,
9
+ readableObjectMode: objectMode,
10
+ });
11
+ this._objectMode = objectMode;
12
+ }
13
+
14
+ async _runTransform({ type, payload }) {
15
+ switch (type) {
16
+ case 'data':
17
+ payload !== undefined && this._pushBuffer(this._format(payload));
18
+ break;
19
+ }
20
+ }
21
+
22
+ _format(payload) {
23
+ // TODO: review this
24
+ return (this._objectMode || typeof payload === 'string') ? payload : JSON.stringify(payload);
25
+ }
26
+
27
+ }
@@ -0,0 +1,117 @@
1
+ import { trimObj } from '../object.js';
2
+
3
+ export const LOG_LEVEL = {
4
+ DEBUG: 'debug',
5
+ INFO: 'info',
6
+ WARNING: 'warning',
7
+ ERROR: 'error',
8
+ };
9
+
10
+ export function createStartEvent(channel, event = { type: 'start' }) {
11
+ return stackEventInfo(channel, event, 'options', channel.options);
12
+ }
13
+
14
+ export function createEndEvent(channel, event = { type: 'end' }) {
15
+ return stackEventInfo(channel, event, 'results', channel.result);
16
+ }
17
+
18
+ export function stackEventInfo(channel, event = {}, field, info) {
19
+ info = normalizeChannelInfo({
20
+ ...info,
21
+ channel: channel.name,
22
+ });
23
+ return {
24
+ ...event,
25
+ [field]: [...(event[field] || []), info],
26
+ };
27
+ }
28
+
29
+ export function writeChannelInfo(channel, array) {
30
+ if (array === undefined) {
31
+ return undefined;
32
+ }
33
+ return array.map(info => normalizeChannelInfo({
34
+ ...info,
35
+ channel: channel.name,
36
+ }));
37
+ }
38
+
39
+ export function normalizeChannelInfo({ channel, timestamp, ...info }) {
40
+ return trimObj({
41
+ channel,
42
+ ...info,
43
+ });
44
+ }
45
+
46
+ export function normalizeEvent({
47
+ type,
48
+ channel,
49
+ index,
50
+ timestamp,
51
+ ...rest
52
+ } = {}) {
53
+ // re-order properties
54
+ return trimObj({
55
+ type,
56
+ channel,
57
+ index,
58
+ ...rest,
59
+ timestamp,
60
+ });
61
+ }
62
+
63
+ export function validateEvent(event) {
64
+ switch (event.type) {
65
+ case 'data':
66
+ if (!event.id) {
67
+ throw new Error('Id is required for data events');
68
+ }
69
+ break;
70
+ }
71
+ }
72
+
73
+ export function generateDefaultSinkResponse({ records, data }) {
74
+ // default implementation: assume all successful
75
+ return {
76
+ writes: 1,
77
+ successful: {
78
+ records,
79
+ data,
80
+ },
81
+ failed: {
82
+ records: 0,
83
+ data: [],
84
+ },
85
+ };
86
+ }
87
+
88
+ export class ChannelOutput {
89
+
90
+ constructor(channel) {
91
+ this._channel = channel;
92
+ }
93
+
94
+ pass(event) {
95
+ this._channel._pushBuffer(normalizeEvent(event));
96
+ }
97
+
98
+ write({ timestamp = Date.now(), ...rest }) {
99
+ const event = {
100
+ ...rest,
101
+ channel: this._channel.name,
102
+ timestamp,
103
+ };
104
+ validateEvent(event);
105
+ this._channel._pushBuffer(normalizeEvent(event));
106
+ }
107
+
108
+ log(level, message, data) {
109
+ this.write({
110
+ type: 'log',
111
+ logLevel: level,
112
+ message,
113
+ ...data,
114
+ });
115
+ }
116
+
117
+ }
@@ -0,0 +1,8 @@
1
+ export { default as Channel } from './channel.js';
2
+ export { default as WriteChannel } from './write.js';
3
+ export { default as WriteChannelBuffer } from './buffer.js';
4
+ export { default as WriteChannelSink } from './sink.js';
5
+ export { default as WriteChannelSinkGate } from './sink-gate.js';
6
+ export { default as UpgradeChannel } from './upgrade.js';
7
+ export { default as DowngradeChannel } from './downgrade.js';
8
+ export { default as TransformChannel } from './transform.js';
@@ -0,0 +1,39 @@
1
+ export default class WriteChannelSinkGate {
2
+
3
+ constructor(options = {}) {
4
+ this._options = options;
5
+ }
6
+
7
+ blockedTime(sinkState, now = Date.now()) {
8
+ const { start, started } = sinkState;
9
+ if (start === undefined) {
10
+ return 0;
11
+ }
12
+ const elapsed = now - start;
13
+ const targetBps = this._targetBps(now);
14
+ const targetRps = this._targetRps(now);
15
+ const targetWps = this._targetWps(now);
16
+
17
+ const shallElapsed = Math.max(started.records / targetRps, started.bytes / targetBps, started.writes / targetWps) * 1000;
18
+
19
+ const blockedTime = shallElapsed - elapsed;
20
+ if (blockedTime <= 1000) {
21
+ return 0;
22
+ }
23
+ // TODO: review this
24
+ return Math.ceil(blockedTime);
25
+ }
26
+
27
+ _targetBps(timestamp) {
28
+ return this._options.bytesPerSecond;
29
+ }
30
+
31
+ _targetRps(timestamp) {
32
+ return this._options.recordsPerSecond;
33
+ }
34
+
35
+ _targetWps(timestamp) {
36
+ return this._options.writesPerSecond;
37
+ }
38
+
39
+ }
@@ -0,0 +1,103 @@
1
+ import { trimObj } from '../object.js';
2
+ import Resolution from '../resolution.js';
3
+ import { ChannelComponent } from './component.js';
4
+ import { generateDefaultSinkResponse } from './events.js';
5
+
6
+ function clone(obj) {
7
+ return Object.freeze(trimObj({ ...obj }));
8
+ }
9
+
10
+ export default class WriteChannelSink extends ChannelComponent {
11
+
12
+ constructor({ write, ...options } = {}) {
13
+ super(options);
14
+ if (write) {
15
+ if (typeof write !== 'function') {
16
+ throw new Error('write must be a function.');
17
+ }
18
+ this._write = write.bind(this);
19
+ }
20
+ this._state = {
21
+ start: undefined,
22
+ started: { index: -1, writes: 0, records: 0, bytes: 0 },
23
+ finished: { index: -1, writes: 0, records: 0, bytes: 0, successful: 0, failed: 0 },
24
+ };
25
+ }
26
+
27
+ get state() {
28
+ const { start, started, finished } = this._state;
29
+ return clone({
30
+ start,
31
+ started: clone(started),
32
+ finished: clone(finished),
33
+ });
34
+ }
35
+
36
+ async write(request) {
37
+ if (this._finishedRes) {
38
+ this._finishedRes.reject(new Error(`More data written after finished call`));
39
+ this._finishedRes = undefined;
40
+ }
41
+ const { records, bytes } = request;
42
+ const now = Date.now();
43
+ const { started, finished } = this._state;
44
+ if (started.writes === 0) {
45
+ this._state.start = now;
46
+ }
47
+ started.index++;
48
+ started.writes++;
49
+ started.records += records;
50
+ started.bytes += bytes;
51
+
52
+ const { index } = started;
53
+ const response = await this._write(request);
54
+
55
+ // sanity check
56
+ if (!response || !response.successful || !response.failed) {
57
+ throw new Error(`Invalid response: ${JSON.stringify(response)}`);
58
+ }
59
+ if (response.successful.records + response.failed.records !== records) {
60
+ throw new Error(`Invalid response: successful records (${response.successful.records}) + failed records (${response.failed.records}) !== records (${records})`);
61
+ }
62
+
63
+ // a _write call may use more than 1 write
64
+ const writes = response.writes || 1;
65
+ if (writes > 1) {
66
+ started.writes += writes - 1; // patch the number
67
+ }
68
+ finished.index = index;
69
+ finished.writes += writes;
70
+ finished.records += records;
71
+ finished.bytes += bytes;
72
+ finished.successful += response.successful.records;
73
+ finished.failed += response.failed.records;
74
+
75
+ if (this._finishedRes && finished.writes === started.writes) {
76
+ this._finishedRes.resolve();
77
+ this._finishedRes = undefined;
78
+ }
79
+
80
+ return response;
81
+ }
82
+
83
+ async _write(event) {
84
+ // default implementation: do nothing but assume all successful
85
+ return generateDefaultSinkResponse(event);
86
+ }
87
+
88
+ get finished() {
89
+ const { started, finished } = this._state;
90
+ if (started.writes === finished.writes) {
91
+ return Promise.resolve();
92
+ }
93
+ return (this._finishedRes || (this._finishedRes = new Resolution())).promise;
94
+ }
95
+
96
+ destroy() {
97
+ if (this._finishedRes) {
98
+ this._finishedRes.reject(new Error(`Stream is destroyed.`));
99
+ this._finishedRes = undefined;
100
+ }
101
+ }
102
+
103
+ }
@@ -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
+ }