@miso.ai/server-sdk 0.6.0-beta.1 → 0.6.0
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/cli/transform.js +16 -1
- package/cli/upload.js +20 -9
- package/package.json +2 -2
- package/src/api/base.js +62 -0
- package/src/api/experiments.js +20 -0
- package/src/api/helpers.js +63 -0
- package/src/api/index.js +19 -0
- package/src/api/interactions.js +11 -0
- package/src/api/products.js +9 -0
- package/src/api/recommendation.js +33 -0
- package/src/api/search.js +21 -0
- package/src/api/users.js +9 -0
- package/src/client.js +2 -6
- package/src/index.js +1 -0
- package/src/logger/api-progress.js +37 -4
- package/src/logger/index.js +0 -5
- package/src/normalize/index.js +74 -0
- package/src/normalize/shim.js +72 -0
- package/src/stream/api-sink.js +1 -1
- package/src/stream/delete-sink.js +2 -2
- package/src/stream/delete.js +1 -1
- package/src/stream/{deletion-state.js → deletion-stats.js} +0 -0
- package/src/stream/upload-sink.js +3 -2
- package/src/stream/upload.js +2 -0
- package/src/version.js +1 -1
- package/src/logger/progress.legacy.js +0 -60
- package/src/stream/upload.legacy.js +0 -410
package/cli/transform.js
CHANGED
|
@@ -32,7 +32,22 @@ async function run({ file }) {
|
|
|
32
32
|
|
|
33
33
|
async function getTransformStream(loc) {
|
|
34
34
|
const mod = await import(join(PWD, loc));
|
|
35
|
-
return mod.default ? new mod.default() : new Transform(
|
|
35
|
+
return mod.default ? new mod.default() : new Transform(normalizeOptions(mod));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeOptions({
|
|
39
|
+
objectMode,
|
|
40
|
+
readableObjectMode,
|
|
41
|
+
writableObjectMode,
|
|
42
|
+
...options
|
|
43
|
+
} = {}) {
|
|
44
|
+
readableObjectMode = readableObjectMode !== undefined ? readableObjectMode : objectMode !== undefined ? objectMode : true;
|
|
45
|
+
writableObjectMode = writableObjectMode !== undefined ? writableObjectMode : objectMode !== undefined ? objectMode : true;
|
|
46
|
+
return {
|
|
47
|
+
readableObjectMode,
|
|
48
|
+
writableObjectMode,
|
|
49
|
+
...options
|
|
50
|
+
};
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export default {
|
package/cli/upload.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import split2 from 'split2';
|
|
2
2
|
import { log, stream } from '@miso.ai/server-commons';
|
|
3
|
-
import { MisoClient, logger } from '../src/index.js';
|
|
3
|
+
import { MisoClient, logger, normalize } from '../src/index.js';
|
|
4
4
|
|
|
5
5
|
function build(yargs) {
|
|
6
6
|
return yargs
|
|
@@ -12,6 +12,10 @@ function build(yargs) {
|
|
|
12
12
|
alias: ['dry'],
|
|
13
13
|
describe: 'Dry run mode',
|
|
14
14
|
})
|
|
15
|
+
.option('lenient', {
|
|
16
|
+
describe: 'Accept some enient record schema',
|
|
17
|
+
type: 'boolean',
|
|
18
|
+
})
|
|
15
19
|
.option('records-per-request', {
|
|
16
20
|
alias: ['rpr'],
|
|
17
21
|
describe: 'How many records to send in a request',
|
|
@@ -37,11 +41,6 @@ function build(yargs) {
|
|
|
37
41
|
alias: ['name'],
|
|
38
42
|
describe: 'Stream name that shows up in log messages',
|
|
39
43
|
})
|
|
40
|
-
.option('legacy', {
|
|
41
|
-
type: 'boolean',
|
|
42
|
-
default: false,
|
|
43
|
-
})
|
|
44
|
-
.hide('legacy')
|
|
45
44
|
.option('log-level', {
|
|
46
45
|
describe: 'Log level',
|
|
47
46
|
})
|
|
@@ -56,6 +55,7 @@ const run = type => async ({
|
|
|
56
55
|
param: params,
|
|
57
56
|
async,
|
|
58
57
|
['dry-run']: dryRun,
|
|
58
|
+
lenient,
|
|
59
59
|
['records-per-request']: recordsPerRequest,
|
|
60
60
|
['bytes-per-request']: bytesPerRequest,
|
|
61
61
|
['bytes-per-second']: bytesPerSecond,
|
|
@@ -63,7 +63,6 @@ const run = type => async ({
|
|
|
63
63
|
debug,
|
|
64
64
|
progress,
|
|
65
65
|
['stream-name']: name,
|
|
66
|
-
legacy,
|
|
67
66
|
['log-level']: loglevel,
|
|
68
67
|
['log-format']: logFormat,
|
|
69
68
|
}) => {
|
|
@@ -73,8 +72,10 @@ const run = type => async ({
|
|
|
73
72
|
|
|
74
73
|
const client = new MisoClient({ key, server });
|
|
75
74
|
|
|
75
|
+
const uploadStreamObjectMode = lenient;
|
|
76
|
+
|
|
76
77
|
const uploadStream = client.createUploadStream(type, {
|
|
77
|
-
|
|
78
|
+
objectMode: uploadStreamObjectMode,
|
|
78
79
|
name,
|
|
79
80
|
async,
|
|
80
81
|
dryRun,
|
|
@@ -85,19 +86,29 @@ const run = type => async ({
|
|
|
85
86
|
bytesPerRequest,
|
|
86
87
|
bytesPerSecond,
|
|
87
88
|
experimentId,
|
|
89
|
+
extra: {
|
|
90
|
+
lenient,
|
|
91
|
+
},
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
const logStream = logger.createLogStream({
|
|
91
95
|
api: 'upload',
|
|
92
96
|
type,
|
|
93
|
-
legacy,
|
|
94
97
|
level: loglevel,
|
|
95
98
|
format: logFormat,
|
|
96
99
|
});
|
|
97
100
|
|
|
101
|
+
// standard: stdin -> split2 -> upload -> log
|
|
102
|
+
// lenient: stdin -> split2 -> parse -> normalize -> upload -> log
|
|
103
|
+
// notice that the output of split2 are strings, while input/output of normalize are objects
|
|
104
|
+
|
|
98
105
|
await stream.pipeline(
|
|
99
106
|
process.stdin,
|
|
100
107
|
split2(),
|
|
108
|
+
...(lenient ? [
|
|
109
|
+
stream.parse(),
|
|
110
|
+
new normalize.Stream(type),
|
|
111
|
+
] : []),
|
|
101
112
|
uploadStream,
|
|
102
113
|
logStream,
|
|
103
114
|
);
|
package/package.json
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
"simonpai <simon.pai@askmiso.com>"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@miso.ai/server-commons": "0.6.0
|
|
19
|
+
"@miso.ai/server-commons": "0.6.0",
|
|
20
20
|
"axios": "^0.27.2",
|
|
21
21
|
"dotenv": "^16.0.1",
|
|
22
22
|
"split2": "^4.1.0",
|
|
23
23
|
"yargs": "^17.5.1"
|
|
24
24
|
},
|
|
25
|
-
"version": "0.6.0
|
|
25
|
+
"version": "0.6.0"
|
|
26
26
|
}
|
package/src/api/base.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { upload, batchDelete, buildUrl } from './helpers.js';
|
|
3
|
+
import UploadStream from '../stream/upload.js';
|
|
4
|
+
import DeleteStream from '../stream/delete.js';
|
|
5
|
+
|
|
6
|
+
export class Queries {
|
|
7
|
+
|
|
8
|
+
constructor(client, group) {
|
|
9
|
+
this._client = client;
|
|
10
|
+
this._group = group;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async _run(path, payload, options) {
|
|
14
|
+
// TODO: options
|
|
15
|
+
const url = buildUrl(this._client, `${this._group}/${path}`);
|
|
16
|
+
return (await axios.post(url, payload)).data.data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Writable {
|
|
22
|
+
|
|
23
|
+
constructor(client, type) {
|
|
24
|
+
this._client = client;
|
|
25
|
+
this._type = type;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async upload(records, options) {
|
|
29
|
+
return (await upload(this._client, this._type, records, options)).data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
uploadStream(options = {}) {
|
|
33
|
+
return new UploadStream(this._client, this._type, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Entities extends Writable {
|
|
39
|
+
|
|
40
|
+
constructor(client, type) {
|
|
41
|
+
super(client, type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async get(id) {
|
|
45
|
+
const url = buildUrl(this._client, `${this._type}/${id}`);
|
|
46
|
+
return (await axios.get(url)).data.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async ids() {
|
|
50
|
+
const url = buildUrl(this._client, `${this._type}/_ids`);
|
|
51
|
+
return (await axios.get(url)).data.data.ids;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(ids, options = {}) {
|
|
55
|
+
return batchDelete(this._client, this._type, ids, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
deleteStream(options = {}) {
|
|
59
|
+
return new DeleteStream(this._client, this._type, options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { buildUrl } from './helpers.js';
|
|
3
|
+
|
|
4
|
+
export default class Experiments {
|
|
5
|
+
|
|
6
|
+
constructor(client) {
|
|
7
|
+
this._client = client;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async uploadEvent(experimentId, record) {
|
|
11
|
+
// TODO: support non-string record
|
|
12
|
+
const url = buildUrl(this._client, `experiments/${experimentId}/events`);
|
|
13
|
+
// TODO: make content type header global
|
|
14
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
15
|
+
const response = await axios.post(url, record, { headers });
|
|
16
|
+
// 200 response body does not have .data layer
|
|
17
|
+
return response.data ? response : { data: response };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { asArray } from '@miso.ai/server-commons';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
|
|
5
|
+
export async function upload(client, type, records, options = {}) {
|
|
6
|
+
const url = buildUrl(client, type, options);
|
|
7
|
+
const payload = buildUploadPayload(records);
|
|
8
|
+
return axios.post(url, payload);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function batchDelete(client, type, ids, options = {}) {
|
|
12
|
+
const url = buildUrl(client, `${type}/_delete`, options);
|
|
13
|
+
const payload = buildBatchDeletePayload(type, ids);
|
|
14
|
+
// TODO: organize axios
|
|
15
|
+
const { data } = await axios.post(url, payload, {
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildUrl(client, path, { async, dryRun, params: extraParams } = {}) {
|
|
24
|
+
let { server, key } = client._options;
|
|
25
|
+
let params = `?api_key=${key}`;
|
|
26
|
+
if (dryRun) {
|
|
27
|
+
params += '&dry_run=1';
|
|
28
|
+
} else if (async) {
|
|
29
|
+
params += '&async=1';
|
|
30
|
+
}
|
|
31
|
+
if (extraParams) {
|
|
32
|
+
for (const key in extraParams) {
|
|
33
|
+
// TODO: deal with encodeURIComponent
|
|
34
|
+
params += `&${key}=${extraParams[key]}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return `${server}/v1/${path}${params}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildUploadPayload(records) {
|
|
41
|
+
return typeof records === 'string' ? records :
|
|
42
|
+
Buffer.isBuffer(records) ? records.toString() :
|
|
43
|
+
{ data: Array.isArray(records)? records : [records] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildBatchDeletePayload(type, ids) {
|
|
47
|
+
if (type !== 'products' && type !== 'users' && type !== 'interactions') {
|
|
48
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
49
|
+
}
|
|
50
|
+
if ((typeof ids === 'string' && ids[0] === '{') || typeof ids === 'object' && !Array.isArray(ids)) {
|
|
51
|
+
return ids;
|
|
52
|
+
}
|
|
53
|
+
ids = asArray(ids);
|
|
54
|
+
if (ids.length === 0) {
|
|
55
|
+
return { data: {} };
|
|
56
|
+
}
|
|
57
|
+
// interactions are deleted by user_ids
|
|
58
|
+
return {
|
|
59
|
+
data: {
|
|
60
|
+
[type === 'products' ? 'product_ids' : 'user_ids']: ids,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/api/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Products from './products.js';
|
|
2
|
+
import Users from './users.js';
|
|
3
|
+
import Interactions from './interactions.js';
|
|
4
|
+
import Experiments from './experiments.js';
|
|
5
|
+
import Search from './search.js';
|
|
6
|
+
import Recommendation from './recommendation.js';
|
|
7
|
+
|
|
8
|
+
export default class Api {
|
|
9
|
+
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.products = new Products(client);
|
|
12
|
+
this.users = new Users(client);
|
|
13
|
+
this.interactions = new Interactions(client);
|
|
14
|
+
this.experiments = new Experiments(client);
|
|
15
|
+
this.search = new Search(client);
|
|
16
|
+
this.recommendation = new Recommendation(client);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Queries } from './base.js';
|
|
2
|
+
|
|
3
|
+
export default class Recommendation extends Queries {
|
|
4
|
+
|
|
5
|
+
constructor(client) {
|
|
6
|
+
super(client, 'recommendation');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async userToProducts(payload, options) {
|
|
10
|
+
return this._run('user_to_products', payload, options);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async userToCategories(payload, options) {
|
|
14
|
+
return this._run('user_to_categories', payload, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async userToAttributes(payload, options) {
|
|
18
|
+
return this._run('user_to_attributes', payload, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async userToTrending(payload, options) {
|
|
22
|
+
return this._run('user_to_trending', payload, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async userToHistory(payload, options) {
|
|
26
|
+
return this._run('user_to_history', payload, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async productToProducts(payload, options) {
|
|
30
|
+
return this._run('product_to_products', payload, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Queries } from './base.js';
|
|
2
|
+
|
|
3
|
+
export default class Search extends Queries {
|
|
4
|
+
|
|
5
|
+
constructor(client) {
|
|
6
|
+
super(client, 'search');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async search(payload, options = {}) {
|
|
10
|
+
return this._run('search', payload, options);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async autocomplete(payload, options = {}) {
|
|
14
|
+
return this._run('autocomplete', payload, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async multipleGet(payload, options = {}) {
|
|
18
|
+
return this._run('mget', payload, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
package/src/api/users.js
ADDED
package/src/client.js
CHANGED
|
@@ -3,9 +3,9 @@ import { asArray } from '@miso.ai/server-commons';
|
|
|
3
3
|
import { Buffer } from 'buffer';
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import version from './version.js';
|
|
6
|
-
import LegacyUploadStream from './stream/upload.legacy.js';
|
|
7
6
|
import UploadStream from './stream/upload.js';
|
|
8
7
|
import DeleteStream from './stream/delete.js';
|
|
8
|
+
import Api from './api/index.js';
|
|
9
9
|
|
|
10
10
|
export default class MisoClient {
|
|
11
11
|
|
|
@@ -14,6 +14,7 @@ export default class MisoClient {
|
|
|
14
14
|
constructor(options) {
|
|
15
15
|
this._options = normalizeOptions(options);
|
|
16
16
|
this.version = version;
|
|
17
|
+
this.api = new Api(this);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async upload(type, records, options) {
|
|
@@ -65,11 +66,6 @@ export default class MisoClient {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
createUploadStream(type, options) {
|
|
68
|
-
if (options.legacy) {
|
|
69
|
-
options.heartbeat = options.heartbeatInvertal;
|
|
70
|
-
delete options.heartbeatInvertal;
|
|
71
|
-
return new LegacyUploadStream(this, type, options);
|
|
72
|
-
}
|
|
73
69
|
return new UploadStream(this, type, options);
|
|
74
70
|
}
|
|
75
71
|
|
package/src/index.js
CHANGED
|
@@ -24,22 +24,55 @@ export default class ApiProgressLogStream extends stream.LogUpdateStream {
|
|
|
24
24
|
|
|
25
25
|
_sections({ config, state }) {
|
|
26
26
|
return [
|
|
27
|
-
this.
|
|
27
|
+
this._configTableCached(config),
|
|
28
28
|
this._statusTable(state),
|
|
29
29
|
this._timeStatsTable(state),
|
|
30
30
|
this._dataStatsTable(state),
|
|
31
31
|
];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
_configTableCached(config) {
|
|
35
|
+
return this._configTableString || (this._configTableString = this._configTable(config));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_configTable(config = {}) {
|
|
39
|
+
const { name, id, client = {}, experimentId } = config;
|
|
40
|
+
const rows = [];
|
|
41
|
+
if (experimentId) {
|
|
42
|
+
rows.push(['Exp. ID:', experimentId]);
|
|
43
|
+
}
|
|
44
|
+
const props = this._configProps(config);
|
|
45
|
+
if (props.length) {
|
|
46
|
+
rows.push(['Config:', props.join(', ')]);
|
|
47
|
+
}
|
|
36
48
|
return formatTable([
|
|
37
|
-
['Job:', `${name}`],
|
|
49
|
+
['Job:', `${name || id}`],
|
|
50
|
+
...rows,
|
|
38
51
|
['Server:', `${client.server || '(default)'}`],
|
|
39
52
|
['API Key:', `${client.keyMasked}`],
|
|
40
53
|
]);
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
_configProps(config = {}) {
|
|
57
|
+
const { sink = {}, extra = {} } = config;
|
|
58
|
+
const { dryRun, async, params } = sink;
|
|
59
|
+
const { lenient } = extra;
|
|
60
|
+
const props = [];
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
props.push('dry-run');
|
|
63
|
+
}
|
|
64
|
+
if (async) {
|
|
65
|
+
props.push('async');
|
|
66
|
+
}
|
|
67
|
+
if (lenient) {
|
|
68
|
+
props.push('lenient');
|
|
69
|
+
}
|
|
70
|
+
if (params && Object.keys(params).length) {
|
|
71
|
+
props.push(`params = ${JSON.stringify(params)}`);
|
|
72
|
+
}
|
|
73
|
+
return props;
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
_statusTable(state) {
|
|
44
77
|
const { bps, stats } = state;
|
|
45
78
|
const serviceBps = stats && stats.service && stats.service.bps;
|
package/src/logger/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { FORMAT } from './constants.js';
|
|
2
2
|
import StandardLogStream from './standard.js';
|
|
3
|
-
import LegacyProgressLogStream from './progress.legacy.js';
|
|
4
3
|
import ApiProgressLogStream from './api-progress.js';
|
|
5
4
|
import DeleteProgressLogStream from './delete-progress.js';
|
|
6
5
|
|
|
@@ -8,7 +7,6 @@ export * from './constants.js';
|
|
|
8
7
|
|
|
9
8
|
export function createLogStream({
|
|
10
9
|
api,
|
|
11
|
-
legacy,
|
|
12
10
|
level,
|
|
13
11
|
format,
|
|
14
12
|
out,
|
|
@@ -18,9 +16,6 @@ export function createLogStream({
|
|
|
18
16
|
case FORMAT.PROGRESS:
|
|
19
17
|
switch (api) {
|
|
20
18
|
case 'upload':
|
|
21
|
-
if (legacy) {
|
|
22
|
-
return new LegacyProgressLogStream({ out, err });
|
|
23
|
-
}
|
|
24
19
|
return new ApiProgressLogStream({ out, err });
|
|
25
20
|
case 'delete':
|
|
26
21
|
return new DeleteProgressLogStream({ out, err });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import { trimObj } from '@miso.ai/server-commons';
|
|
3
|
+
import * as shim from './shim.js';
|
|
4
|
+
|
|
5
|
+
export function products({
|
|
6
|
+
cover_image,
|
|
7
|
+
url,
|
|
8
|
+
...record
|
|
9
|
+
}) {
|
|
10
|
+
// category -> assume single, categories -> assume multiple
|
|
11
|
+
return trimObj({
|
|
12
|
+
cover_image: shim.url(cover_image),
|
|
13
|
+
url: shim.url(url),
|
|
14
|
+
...record,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function users({
|
|
19
|
+
...record
|
|
20
|
+
}) {
|
|
21
|
+
return trimObj({
|
|
22
|
+
...record,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function interactions({
|
|
27
|
+
...record
|
|
28
|
+
}) {
|
|
29
|
+
return trimObj({
|
|
30
|
+
...record,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function experimentEvents({
|
|
35
|
+
...record
|
|
36
|
+
}) {
|
|
37
|
+
return trimObj({
|
|
38
|
+
...record,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getTransformFunction(type) {
|
|
43
|
+
switch (type) {
|
|
44
|
+
case 'products':
|
|
45
|
+
return products;
|
|
46
|
+
case 'users':
|
|
47
|
+
return users;
|
|
48
|
+
case 'interactions':
|
|
49
|
+
return interactions;
|
|
50
|
+
case 'experiment-events':
|
|
51
|
+
return experimentEvents;
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unrecognized record type: ${type}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class Stream extends Transform {
|
|
58
|
+
|
|
59
|
+
constructor(type) {
|
|
60
|
+
super({
|
|
61
|
+
objectMode: true,
|
|
62
|
+
});
|
|
63
|
+
this._transformFn = getTransformFunction(type);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async _transform(record, _, next) {
|
|
67
|
+
try {
|
|
68
|
+
next(undefined, this._transformFn(record));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
next(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { asArray } from '@miso.ai/server-commons';
|
|
2
|
+
|
|
3
|
+
export const number = _applyGeneralPass(_number);
|
|
4
|
+
export const string = _applyGeneralPass(_string);
|
|
5
|
+
export const array = _applyGeneralPass(_array);
|
|
6
|
+
export const doubleArray = _applyGeneralPass(_doubleArray);
|
|
7
|
+
export const time = _applyGeneralPass(_time);
|
|
8
|
+
export const url = _applyGeneralPass(_url);
|
|
9
|
+
|
|
10
|
+
function _number(value) {
|
|
11
|
+
value = Number(value);
|
|
12
|
+
if (isNaN(value)) {
|
|
13
|
+
throw new Error();
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _string(value) {
|
|
19
|
+
// TODO
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _array(value) {
|
|
24
|
+
value = asArray(value).filter(isNotBlank);
|
|
25
|
+
return value.length ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _doubleArray(value) {
|
|
29
|
+
// TODO
|
|
30
|
+
value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _time(value) {
|
|
34
|
+
// TODO: how to handle timezone
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _url(value) {
|
|
39
|
+
// blank safe
|
|
40
|
+
if (isBlank(value) || typeof value !== 'string') {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
// a wild guess on whether the URL has been encoded
|
|
44
|
+
if (!value.includes('%')) {
|
|
45
|
+
value = encodeURI(value);
|
|
46
|
+
}
|
|
47
|
+
// bounce on invalid URL
|
|
48
|
+
new URL(value);
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isNotBlank(value) {
|
|
54
|
+
return !isBlank(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isBlank(value) {
|
|
58
|
+
return value === undefined || value === null || value === '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _applyGeneralPass(fn) {
|
|
62
|
+
return value => {
|
|
63
|
+
if (isBlank(value)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return fn(value);
|
|
68
|
+
} catch(_) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/stream/api-sink.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ApiSink from './api-sink.js';
|
|
2
|
+
import { batchDelete } from '../api/helpers.js';
|
|
2
3
|
|
|
3
4
|
export default class DeleteSink extends ApiSink {
|
|
4
5
|
|
|
@@ -19,8 +20,7 @@ export default class DeleteSink extends ApiSink {
|
|
|
19
20
|
|
|
20
21
|
async _execute(payload) {
|
|
21
22
|
const { type, params } = this._options;
|
|
22
|
-
|
|
23
|
-
return response.data;
|
|
23
|
+
return batchDelete(this._client, type, payload, { params });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
}
|
package/src/stream/delete.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stream, buffer } from '@miso.ai/server-commons';
|
|
2
2
|
import version from '../version.js';
|
|
3
3
|
import DeleteSink from './delete-sink.js';
|
|
4
|
-
import DeletionStats from './deletion-
|
|
4
|
+
import DeletionStats from './deletion-stats.js';
|
|
5
5
|
|
|
6
6
|
export default class DeleteStream extends stream.BufferedWriteStream {
|
|
7
7
|
|
|
File without changes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ApiSink from './api-sink.js';
|
|
2
|
+
import { upload } from '../api/helpers.js';
|
|
2
3
|
|
|
3
4
|
class UploadSink extends ApiSink {
|
|
4
5
|
|
|
@@ -23,7 +24,7 @@ class UploadSink extends ApiSink {
|
|
|
23
24
|
|
|
24
25
|
async _execute(payload) {
|
|
25
26
|
const { type, async, dryRun, params } = this._options;
|
|
26
|
-
const response = await this._client
|
|
27
|
+
const response = await upload(this._client, type, payload, { async, dryRun, params });
|
|
27
28
|
return response.data;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -84,7 +85,7 @@ class ExperimentEventUploadSink extends UploadSink {
|
|
|
84
85
|
|
|
85
86
|
async _execute(payload) {
|
|
86
87
|
const { experimentId } = this._options;
|
|
87
|
-
const response = await this._client.
|
|
88
|
+
const response = await this._client.api.experiments.uploadEvent(experimentId, payload);
|
|
88
89
|
return response.data;
|
|
89
90
|
}
|
|
90
91
|
|
package/src/stream/upload.js
CHANGED
|
@@ -20,6 +20,7 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
20
20
|
// buffer
|
|
21
21
|
recordsPerRequest,
|
|
22
22
|
bytesPerRequest,
|
|
23
|
+
extra,
|
|
23
24
|
} = {}) {
|
|
24
25
|
super({
|
|
25
26
|
name,
|
|
@@ -27,6 +28,7 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
27
28
|
version,
|
|
28
29
|
objectMode,
|
|
29
30
|
heartbeatInterval,
|
|
31
|
+
extra,
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
this._client = client;
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '0.6.0
|
|
1
|
+
export default '0.6.0';
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { log, stream } from '@miso.ai/server-commons';
|
|
2
|
-
|
|
3
|
-
const { formatDuration, formatBytes, formatTable } = log;
|
|
4
|
-
|
|
5
|
-
export default class LegacyProgressLogStream extends stream.LogUpdateStream {
|
|
6
|
-
|
|
7
|
-
constructor({
|
|
8
|
-
out = process.stdout,
|
|
9
|
-
err = process.stderr,
|
|
10
|
-
} = {}) {
|
|
11
|
-
super({
|
|
12
|
-
out,
|
|
13
|
-
err,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
_renderError({ state: _, ...record }) {
|
|
18
|
-
return super._renderError(record);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
_render({ state }) {
|
|
22
|
-
const { elapsed, pending, successful, failed, apiBps, sentBps } = state;
|
|
23
|
-
|
|
24
|
-
// sum pending requests
|
|
25
|
-
const pendingSums = sumMetrics(pending);
|
|
26
|
-
|
|
27
|
-
const table = formatTable([
|
|
28
|
-
['', 'Requests', 'Records', 'Bytes', 'Server Time', 'Latency'],
|
|
29
|
-
['Pending', pendingSums.requests, pendingSums.records, formatBytes(pendingSums.bytes), '--', '--'],
|
|
30
|
-
['Successful', successful.requests, successful.records, formatBytes(successful.bytes), formatDuration(successful.took), formatDuration(successful.latency)],
|
|
31
|
-
['Failed', failed.requests, failed.records, formatBytes(failed.bytes), formatDuration(failed.took), formatDuration(failed.latency)],
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
return `
|
|
35
|
-
Time elapsed: ${formatDuration(elapsed)}
|
|
36
|
-
API Speed: ${formatSpeed(apiBps)}
|
|
37
|
-
Client Speed: ${formatSpeed(sentBps)}
|
|
38
|
-
|
|
39
|
-
${table}
|
|
40
|
-
`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function sumMetrics(requests) {
|
|
46
|
-
return requests.reduce((acc, req) => {
|
|
47
|
-
acc.requests ++;
|
|
48
|
-
acc.records += req.records;
|
|
49
|
-
acc.bytes += req.bytes;
|
|
50
|
-
return acc;
|
|
51
|
-
}, {
|
|
52
|
-
requests: 0,
|
|
53
|
-
records: 0,
|
|
54
|
-
bytes: 0,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function formatSpeed(value) {
|
|
59
|
-
return isNaN(value) ? 'N/A' : `${formatBytes(value)}/s`;
|
|
60
|
-
}
|
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import { Transform } from 'stream';
|
|
2
|
-
import { trimObj, log } from '@miso.ai/server-commons';
|
|
3
|
-
import version from '../version.js';
|
|
4
|
-
|
|
5
|
-
function getDefaultRecordsPerRequest(type) {
|
|
6
|
-
switch (type) {
|
|
7
|
-
case 'interactions':
|
|
8
|
-
return 1000;
|
|
9
|
-
default:
|
|
10
|
-
return 200;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function normalizeParams(params = []) {
|
|
15
|
-
return params.reduce((acc, param) => {
|
|
16
|
-
const [key, value = '1'] = param.split('=');
|
|
17
|
-
acc[key] = value;
|
|
18
|
-
return acc;
|
|
19
|
-
}, {});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const PAYLOAD_PREFIX = '{"data":[';
|
|
23
|
-
const PAYLOAD_SUFFIX = ']}';
|
|
24
|
-
const PAYLOAD_OVERHEAD_BYTES = (PAYLOAD_PREFIX.length + PAYLOAD_SUFFIX.length) * 2;
|
|
25
|
-
|
|
26
|
-
const MIN_HREATBEAT = 100;
|
|
27
|
-
|
|
28
|
-
const requestPromises = new WeakMap();
|
|
29
|
-
|
|
30
|
-
export default class LegacyUploadStream extends Transform {
|
|
31
|
-
|
|
32
|
-
constructor(client, type, {
|
|
33
|
-
name,
|
|
34
|
-
objectMode,
|
|
35
|
-
async,
|
|
36
|
-
dryRun,
|
|
37
|
-
params,
|
|
38
|
-
heartbeat,
|
|
39
|
-
recordsPerRequest,
|
|
40
|
-
bytesPerRequest,
|
|
41
|
-
bytesPerSecond,
|
|
42
|
-
experimentId,
|
|
43
|
-
} = {}) {
|
|
44
|
-
super({
|
|
45
|
-
readableObjectMode: true,
|
|
46
|
-
writableObjectMode: objectMode,
|
|
47
|
-
});
|
|
48
|
-
this._client = client;
|
|
49
|
-
this._type = type;
|
|
50
|
-
this._options = {
|
|
51
|
-
name,
|
|
52
|
-
objectMode: !!objectMode,
|
|
53
|
-
async: !!async,
|
|
54
|
-
dryRun: !!dryRun,
|
|
55
|
-
params: normalizeParams(params),
|
|
56
|
-
heartbeat,
|
|
57
|
-
recordsPerRequest: recordsPerRequest || getDefaultRecordsPerRequest(type),
|
|
58
|
-
bytesPerRequest: bytesPerRequest || 1024 * 1024,
|
|
59
|
-
bytesPerSecond: bytesPerSecond || 4 * 1024 * 1024,
|
|
60
|
-
experimentId,
|
|
61
|
-
};
|
|
62
|
-
if (heartbeat !== undefined && (typeof heartbeat !== 'number') || heartbeat < MIN_HREATBEAT) {
|
|
63
|
-
throw new Error(`Heartbeat must be a number at least ${MIN_HREATBEAT}: ${heartbeat}`);
|
|
64
|
-
}
|
|
65
|
-
this._singleRecordPerRequest = false;
|
|
66
|
-
|
|
67
|
-
switch (type) {
|
|
68
|
-
case 'experiment-events':
|
|
69
|
-
if (!experimentId) {
|
|
70
|
-
throw new Error(`Experiment id is required for experiment APIs`);
|
|
71
|
-
}
|
|
72
|
-
this._singleRecordPerRequest = true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
this._state = new State();
|
|
76
|
-
this._resetBuffer();
|
|
77
|
-
// log functions
|
|
78
|
-
for (const level of log.LEVELS) {
|
|
79
|
-
this[`_${level}`] = this._log.bind(this, level);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
_construct(done) {
|
|
84
|
-
const { config } = this;
|
|
85
|
-
this._info('construct', { config });
|
|
86
|
-
const heartbeat = this._options.heartbeat;
|
|
87
|
-
if (heartbeat) {
|
|
88
|
-
this._heartbeatIntervalId = setInterval(this._heartbeat.bind(this), heartbeat);
|
|
89
|
-
}
|
|
90
|
-
done();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
_transform(record, _, next) {
|
|
94
|
-
this._pushStartEventIfNecessary();
|
|
95
|
-
const { objectMode, bytesPerRequest } = this._options;
|
|
96
|
-
const str = objectMode ? JSON.stringify(record) : record;
|
|
97
|
-
const newSize = str.length * 2;
|
|
98
|
-
|
|
99
|
-
if (this._singleRecordPerRequest) {
|
|
100
|
-
this._buffer.content = str;
|
|
101
|
-
this._buffer.bytes = newSize;
|
|
102
|
-
this._buffer.records = 1;
|
|
103
|
-
this._dispatch();
|
|
104
|
-
|
|
105
|
-
} else {
|
|
106
|
-
if (this._buffer.records && this._buffer.bytes + newSize >= bytesPerRequest) {
|
|
107
|
-
// flush previous records if this record is large enough to exceed BPR threshold
|
|
108
|
-
this._dispatch();
|
|
109
|
-
}
|
|
110
|
-
if (this._buffer.records > 0) {
|
|
111
|
-
this._buffer.content += ',';
|
|
112
|
-
}
|
|
113
|
-
this._buffer.content += str;
|
|
114
|
-
this._buffer.bytes += newSize;
|
|
115
|
-
this._buffer.records++;
|
|
116
|
-
|
|
117
|
-
this._dispatchIfNecessary();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const restTime = this._state.restTime(this._getBpsLimit());
|
|
121
|
-
if (restTime > 0) {
|
|
122
|
-
this._debug('rest', { restTime });
|
|
123
|
-
setTimeout(next, restTime);
|
|
124
|
-
} else if (this._state._pending.length > 15) {
|
|
125
|
-
// TODO: figure out best strategy on this
|
|
126
|
-
// release event loop for downstream
|
|
127
|
-
setTimeout(next);
|
|
128
|
-
} else {
|
|
129
|
-
next();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
_log(level, event, args = {}) {
|
|
134
|
-
this.push(trimObj({
|
|
135
|
-
name: this._options.name,
|
|
136
|
-
level,
|
|
137
|
-
event,
|
|
138
|
-
timestamp: Date.now(),
|
|
139
|
-
...args,
|
|
140
|
-
state: this.state,
|
|
141
|
-
}));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
_heartbeat() {
|
|
145
|
-
this._log(log.DEBUG, 'heartbeat');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async _flush(done) {
|
|
149
|
-
this._pushStartEventIfNecessary();
|
|
150
|
-
this._dispatch();
|
|
151
|
-
await Promise.all(this._state.pending.map(r => requestPromises.get(r)));
|
|
152
|
-
const { successful, failed } = this.state;
|
|
153
|
-
|
|
154
|
-
if (this._heartbeatIntervalId) {
|
|
155
|
-
clearInterval(this._heartbeatIntervalId);
|
|
156
|
-
delete this._heartbeatIntervalId;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
this._info('end', { successful, failed });
|
|
160
|
-
done();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
get state() {
|
|
164
|
-
return this._state.export();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
get config() {
|
|
168
|
-
return Object.freeze({
|
|
169
|
-
version,
|
|
170
|
-
type: this._type,
|
|
171
|
-
...this._client.options,
|
|
172
|
-
...this._options,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// helper //
|
|
177
|
-
_pushStartEventIfNecessary() {
|
|
178
|
-
if (this._state.next.request === 0 && this._buffer.records === 0) {
|
|
179
|
-
this._info('start');
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
_dispatchIfNecessary() {
|
|
184
|
-
const { records, bytes } = this._buffer;
|
|
185
|
-
const { recordsPerRequest, bytesPerRequest } = this._options;
|
|
186
|
-
if (records > 0 && (records >= recordsPerRequest || bytes >= bytesPerRequest)) {
|
|
187
|
-
this._dispatch();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
_dispatch() {
|
|
192
|
-
const singleRecord = this._singleRecordPerRequest;
|
|
193
|
-
const { records, bytes, content } = this._resetBuffer();
|
|
194
|
-
if (records === 0) {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const request = this._state.createRequest(records, bytes);
|
|
198
|
-
|
|
199
|
-
let requestResolve;
|
|
200
|
-
requestPromises.set(request, new Promise(r => {
|
|
201
|
-
requestResolve = r;
|
|
202
|
-
}));
|
|
203
|
-
|
|
204
|
-
this._debug('request', { request });
|
|
205
|
-
|
|
206
|
-
this._state.open(request);
|
|
207
|
-
|
|
208
|
-
const payload = singleRecord ? content : content + PAYLOAD_SUFFIX;
|
|
209
|
-
|
|
210
|
-
(async () => {
|
|
211
|
-
let response;
|
|
212
|
-
try {
|
|
213
|
-
response = await this._upload(payload);
|
|
214
|
-
} catch(error) {
|
|
215
|
-
response = !error.response ? trimObj({ errors: true, cause: error.message }) :
|
|
216
|
-
typeof error.response.data !== 'object' ? trimObj({ errors: true, cause: error.response.data }) :
|
|
217
|
-
error.response.data;
|
|
218
|
-
}
|
|
219
|
-
response.timestamp = Date.now();
|
|
220
|
-
response.took = response.took || 0; // TODO: ad-hoc
|
|
221
|
-
|
|
222
|
-
requestResolve();
|
|
223
|
-
this._state.close(request, response);
|
|
224
|
-
|
|
225
|
-
const failed = response.errors;
|
|
226
|
-
|
|
227
|
-
(failed ? this._error : this._debug)('response', { request, response, payload: failed ? JSON.parse(payload) : undefined });
|
|
228
|
-
this._info('upload', {
|
|
229
|
-
result: failed ? 'failed' : 'successful',
|
|
230
|
-
index: request.index,
|
|
231
|
-
records: request.records,
|
|
232
|
-
bytes: request.bytes,
|
|
233
|
-
took: response.took,
|
|
234
|
-
latency: response.timestamp - request.timestamp - response.took
|
|
235
|
-
});
|
|
236
|
-
})();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async _upload(payload) {
|
|
240
|
-
switch (this._type) {
|
|
241
|
-
case 'experiment-events':
|
|
242
|
-
const { experimentId } = this._options;
|
|
243
|
-
return (await this._client.uploadExperimentEvent(experimentId, payload)).data;
|
|
244
|
-
default:
|
|
245
|
-
const { async, dryRun, params } = this._options;
|
|
246
|
-
const response = await this._client.upload(this._type, payload, { async, dryRun, params });
|
|
247
|
-
return response.data;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
_resetBuffer() {
|
|
252
|
-
const buffer = { ...this._buffer };
|
|
253
|
-
this._buffer = {
|
|
254
|
-
records: 0,
|
|
255
|
-
bytes: PAYLOAD_OVERHEAD_BYTES,
|
|
256
|
-
content: PAYLOAD_PREFIX,
|
|
257
|
-
};
|
|
258
|
-
return buffer;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Note that when API is effectively in async mode, apiBps will be overestimated, but there is no harm to respect it.
|
|
263
|
-
*/
|
|
264
|
-
_getBpsLimit() {
|
|
265
|
-
const { bytesPerSecond } = this._options;
|
|
266
|
-
const { pending, apiBps, completed } = this.state;
|
|
267
|
-
// TODO: threshold to switch to real API BPS should be based on bytes, not requests
|
|
268
|
-
// respect API BPS only when
|
|
269
|
-
// 1. has pending requests
|
|
270
|
-
// 2. has enouch data points from completed requests
|
|
271
|
-
return pending.length > 0 && completed.requests > 9 && !isNaN(apiBps) && apiBps < bytesPerSecond ? apiBps : bytesPerSecond;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
class State {
|
|
277
|
-
|
|
278
|
-
constructor() {
|
|
279
|
-
this._start = Date.now();
|
|
280
|
-
this._next = Object.freeze({
|
|
281
|
-
request: 0,
|
|
282
|
-
record: 0,
|
|
283
|
-
});
|
|
284
|
-
this._pending = [];
|
|
285
|
-
this._successful = {
|
|
286
|
-
requests: 0,
|
|
287
|
-
records: 0,
|
|
288
|
-
bytes: 0,
|
|
289
|
-
took: 0,
|
|
290
|
-
latency: 0,
|
|
291
|
-
};
|
|
292
|
-
this._failed = {
|
|
293
|
-
requests: 0,
|
|
294
|
-
records: 0,
|
|
295
|
-
bytes: 0,
|
|
296
|
-
took: 0,
|
|
297
|
-
latency: 0,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
get start() {
|
|
302
|
-
return this._start;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
get next() {
|
|
306
|
-
return this._next;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
get pending() {
|
|
310
|
-
return Object.freeze(this._pending.slice());
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
get successful() {
|
|
314
|
-
return Object.freeze({ ...this._successful });
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
get failed() {
|
|
318
|
-
return Object.freeze({ ...this._failed });
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
elapsed(time) {
|
|
322
|
-
return (time || Date.now()) - this.start;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
createRequest(records, bytes) {
|
|
326
|
-
const { next } = this;
|
|
327
|
-
const request = Object.freeze({
|
|
328
|
-
index: next.request,
|
|
329
|
-
recordOffset: next.record,
|
|
330
|
-
records,
|
|
331
|
-
bytes,
|
|
332
|
-
timestamp: Date.now(),
|
|
333
|
-
});
|
|
334
|
-
this._next = Object.freeze({
|
|
335
|
-
request: next.request + 1,
|
|
336
|
-
record: next.record + records,
|
|
337
|
-
});
|
|
338
|
-
return request;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
get sent() {
|
|
342
|
-
const { _pending, completed } = this;
|
|
343
|
-
return Object.freeze(_pending.reduce((acc, request) => {
|
|
344
|
-
acc.requests++;
|
|
345
|
-
acc.records += request.records;
|
|
346
|
-
acc.bytes += request.bytes;
|
|
347
|
-
return acc;
|
|
348
|
-
}, {
|
|
349
|
-
requests: completed.requests,
|
|
350
|
-
records: completed.records,
|
|
351
|
-
bytes: completed.bytes,
|
|
352
|
-
}));
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
get completed() {
|
|
356
|
-
const { _successful, _failed } = this;
|
|
357
|
-
return Object.freeze({
|
|
358
|
-
requests: _successful.requests + _failed.requests,
|
|
359
|
-
records: _successful.records + _failed.records,
|
|
360
|
-
bytes: _successful.bytes + _failed.bytes,
|
|
361
|
-
took: _successful.took + _failed.took,
|
|
362
|
-
latency: _successful.latency + _failed.latency,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
sentBps(timestamp) {
|
|
367
|
-
const { sent } = this;
|
|
368
|
-
const elapsed = this.elapsed(timestamp);
|
|
369
|
-
return elapsed > 0 ? sent.bytes / elapsed * 1000 : NaN;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
get apiBps() {
|
|
373
|
-
const { _successful, _failed } = this;
|
|
374
|
-
const took = _successful.took + _failed.took;
|
|
375
|
-
const bytes = _successful.bytes + _failed.bytes;
|
|
376
|
-
return took > 0 ? bytes / took * 1000 : NaN;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
restTime(bps) {
|
|
380
|
-
const elapsed = this.elapsed();
|
|
381
|
-
const { sent } = this;
|
|
382
|
-
return Math.max(0, sent.bytes / bps * 1000 - elapsed) * 1.05;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export(timestamp) {
|
|
386
|
-
const { next, pending, successful, failed, sent, completed, apiBps } = this;
|
|
387
|
-
timestamp = timestamp || Date.now();
|
|
388
|
-
return Object.freeze({
|
|
389
|
-
next, pending, sent, successful, failed, completed,
|
|
390
|
-
elapsed: this.elapsed(timestamp),
|
|
391
|
-
apiBps,
|
|
392
|
-
sentBps: this.sentBps(timestamp),
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
open(request) {
|
|
397
|
-
this._pending.push(request);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
close(request, response) {
|
|
401
|
-
this._pending = this._pending.filter(r => r.index !== request.index);
|
|
402
|
-
const category = response.errors ? this._failed : this._successful;
|
|
403
|
-
category.requests++;
|
|
404
|
-
category.records += request.records;
|
|
405
|
-
category.bytes += request.bytes;
|
|
406
|
-
category.took += response.took;
|
|
407
|
-
category.latency += response.timestamp - request.timestamp - response.took;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
}
|