@api-client/core 0.3.5 → 0.3.8

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.
Files changed (84) hide show
  1. package/build/browser.d.ts +5 -0
  2. package/build/browser.js +14 -0
  3. package/build/browser.js.map +1 -1
  4. package/build/index.d.ts +13 -1
  5. package/build/index.js +25 -1
  6. package/build/index.js.map +1 -1
  7. package/build/src/lib/calculators/DataCalculator.d.ts +27 -0
  8. package/build/src/lib/calculators/DataCalculator.js +88 -0
  9. package/build/src/lib/calculators/DataCalculator.js.map +1 -0
  10. package/build/src/lib/fs/Fs.d.ts +52 -0
  11. package/build/src/lib/fs/Fs.js +245 -0
  12. package/build/src/lib/fs/Fs.js.map +1 -0
  13. package/build/src/lib/parsers/UrlEncoder.d.ts +51 -0
  14. package/build/src/lib/parsers/UrlEncoder.js +74 -0
  15. package/build/src/lib/parsers/UrlEncoder.js.map +1 -0
  16. package/build/src/lib/parsers/UrlParser.d.ts +104 -0
  17. package/build/src/lib/parsers/UrlParser.js +189 -0
  18. package/build/src/lib/parsers/UrlParser.js.map +1 -0
  19. package/build/src/lib/parsers/UrlValueParser.d.ts +92 -0
  20. package/build/src/lib/parsers/UrlValueParser.js +172 -0
  21. package/build/src/lib/parsers/UrlValueParser.js.map +1 -0
  22. package/build/src/lib/timers/Timers.d.ts +5 -0
  23. package/build/src/lib/timers/Timers.js +10 -0
  24. package/build/src/lib/timers/Timers.js.map +1 -0
  25. package/build/src/mocking/ProjectMock.d.ts +13 -0
  26. package/build/src/mocking/ProjectMock.js +16 -0
  27. package/build/src/mocking/ProjectMock.js.map +1 -0
  28. package/build/src/mocking/lib/Request.d.ts +32 -0
  29. package/build/src/mocking/lib/Request.js +63 -0
  30. package/build/src/mocking/lib/Request.js.map +1 -0
  31. package/build/src/mocking/lib/Response.d.ts +33 -0
  32. package/build/src/mocking/lib/Response.js +79 -0
  33. package/build/src/mocking/lib/Response.js.map +1 -0
  34. package/build/src/runtime/node/BaseRunner.d.ts +21 -0
  35. package/build/src/runtime/node/BaseRunner.js +27 -0
  36. package/build/src/runtime/node/BaseRunner.js.map +1 -0
  37. package/build/src/runtime/node/ProjectParallelRunner.d.ts +81 -0
  38. package/build/src/runtime/node/ProjectParallelRunner.js +173 -0
  39. package/build/src/runtime/node/ProjectParallelRunner.js.map +1 -0
  40. package/build/src/runtime/node/ProjectRequestRunner.d.ts +125 -0
  41. package/build/src/runtime/node/ProjectRequestRunner.js +185 -0
  42. package/build/src/runtime/node/ProjectRequestRunner.js.map +1 -0
  43. package/build/src/runtime/node/ProjectRunner.d.ts +164 -62
  44. package/build/src/runtime/node/ProjectRunner.js +191 -146
  45. package/build/src/runtime/node/ProjectRunner.js.map +1 -1
  46. package/build/src/runtime/node/ProjectRunnerWorker.d.ts +1 -0
  47. package/build/src/runtime/node/ProjectRunnerWorker.js +58 -0
  48. package/build/src/runtime/node/ProjectRunnerWorker.js.map +1 -0
  49. package/build/src/runtime/node/ProjectSerialRunner.d.ts +11 -0
  50. package/build/src/runtime/node/ProjectSerialRunner.js +34 -0
  51. package/build/src/runtime/node/ProjectSerialRunner.js.map +1 -0
  52. package/build/src/runtime/reporters/ProjectRunCliReporter.d.ts +7 -0
  53. package/build/src/runtime/reporters/ProjectRunCliReporter.js +73 -0
  54. package/build/src/runtime/reporters/ProjectRunCliReporter.js.map +1 -0
  55. package/build/src/runtime/reporters/Reporter.d.ts +62 -0
  56. package/build/src/runtime/reporters/Reporter.js +98 -0
  57. package/build/src/runtime/reporters/Reporter.js.map +1 -0
  58. package/build/src/testing/TestCliHelper.d.ts +29 -0
  59. package/build/src/testing/TestCliHelper.js +80 -0
  60. package/build/src/testing/TestCliHelper.js.map +1 -0
  61. package/build/src/testing/getPort.d.ts +52 -0
  62. package/build/src/testing/getPort.js +169 -0
  63. package/build/src/testing/getPort.js.map +1 -0
  64. package/package.json +3 -2
  65. package/src/lib/calculators/DataCalculator.ts +91 -0
  66. package/src/lib/fs/Fs.ts +258 -0
  67. package/src/lib/parsers/UrlEncoder.ts +74 -0
  68. package/src/lib/parsers/UrlParser.ts +201 -0
  69. package/src/lib/parsers/UrlValueParser.ts +211 -0
  70. package/src/lib/timers/Timers.ts +9 -0
  71. package/src/mocking/LegacyInterfaces.ts +1 -1
  72. package/src/mocking/ProjectMock.ts +20 -0
  73. package/src/mocking/lib/Request.ts +85 -0
  74. package/src/mocking/lib/Response.ts +101 -0
  75. package/src/runtime/node/BaseRunner.ts +29 -0
  76. package/src/runtime/node/ProjectParallelRunner.ts +234 -0
  77. package/src/runtime/node/ProjectRequestRunner.ts +281 -0
  78. package/src/runtime/node/ProjectRunner.ts +279 -186
  79. package/src/runtime/node/ProjectRunnerWorker.ts +62 -0
  80. package/src/runtime/node/ProjectSerialRunner.ts +36 -0
  81. package/src/runtime/reporters/ProjectRunCliReporter.ts +79 -0
  82. package/src/runtime/reporters/Reporter.ts +142 -0
  83. package/src/testing/TestCliHelper.ts +87 -0
  84. package/src/testing/getPort.ts +212 -0
@@ -0,0 +1,211 @@
1
+ export interface IUrlValueParserOptions {
2
+ /**
3
+ * A query string delimiter to use when processing query parameters.
4
+ */
5
+ queryDelimiter?: string;
6
+ }
7
+
8
+ interface DataValues {
9
+ /**
10
+ * A protocol value in format `protocol` + ':'
11
+ */
12
+ protocol?: string;
13
+ /**
14
+ * The authority part of the URL value
15
+ */
16
+ host?: string;
17
+ /**
18
+ * Path part of the URL.
19
+ */
20
+ path?: string;
21
+ /**
22
+ * Anchor part of the URL.
23
+ */
24
+ anchor?: string;
25
+ /**
26
+ * Search part of the URL.
27
+ */
28
+ search?: string;
29
+ opts?: IUrlValueParserOptions | undefined;
30
+ }
31
+
32
+ /**
33
+ * Implements logic for parsing URL string.
34
+ */
35
+ export class UrlValueParser {
36
+ protected __data: DataValues;
37
+
38
+ constructor(opts?: IUrlValueParserOptions) {
39
+ this.__data = {};
40
+ this.opts = opts;
41
+ }
42
+
43
+ /**
44
+ * @returns Class options.
45
+ */
46
+ get opts(): IUrlValueParserOptions {
47
+ return this.__data.opts || {
48
+ queryDelimiter: '&',
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Sets parser options.
54
+ * Unknown options are ignored.
55
+ */
56
+ set opts(opts: IUrlValueParserOptions | undefined) {
57
+ const options = (opts || {}) as IUrlValueParserOptions;
58
+ this.__data.opts = {
59
+ queryDelimiter: options.queryDelimiter || '&'
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Returns protocol value in format `protocol` + ':'
65
+ *
66
+ * @param value URL to parse.
67
+ * @return Value of the protocol or undefined if value not set
68
+ */
69
+ protected _parseProtocol(value: string): string | undefined {
70
+ if (!value) {
71
+ return undefined;
72
+ }
73
+ const delimiterIndex = value.indexOf('://');
74
+ if (delimiterIndex !== -1) {
75
+ return value.substring(0, delimiterIndex + 1);
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ /**
81
+ * Gets a host value from the url.
82
+ * It reads the whole authority value of given `value`. It doesn't parses it
83
+ * to host, port and
84
+ * credentials parts. For URL panel it's enough.
85
+ *
86
+ * @param value The URL to parse
87
+ * @return Value of the host or undefined.
88
+ */
89
+ protected _parseHost(value: string): string | undefined {
90
+ if (!value) {
91
+ return undefined;
92
+ }
93
+ let result = value;
94
+ const delimiterIndex = result.indexOf('://');
95
+ if (delimiterIndex !== -1) {
96
+ result = result.substring(delimiterIndex + 3);
97
+ }
98
+ if (!result) {
99
+ return undefined;
100
+ }
101
+ // We don't need specifics here (username, password, port)
102
+ const host = result.split('/')[0];
103
+ return host;
104
+ }
105
+
106
+ /**
107
+ * Parses the path part of the URL.
108
+ *
109
+ * @param value URL value
110
+ * @returns Path part of the URL
111
+ */
112
+ protected _parsePath(value: string): string | undefined {
113
+ if (!value) {
114
+ return undefined;
115
+ }
116
+ let result = value;
117
+ const isBasePath = result[0] === '/';
118
+ if (!isBasePath) {
119
+ const index = result.indexOf('://');
120
+ if (index !== -1) {
121
+ result = result.substring(index + 3);
122
+ }
123
+ }
124
+ let index = result.indexOf('?');
125
+ if (index !== -1) {
126
+ result = result.substring(0, index);
127
+ }
128
+ index = result.indexOf('#');
129
+ if (index !== -1) {
130
+ result = result.substring(0, index);
131
+ }
132
+ const lastIsSlash = result[result.length - 1] === '/';
133
+ const parts = result.split('/').filter((part) => !!part);
134
+ if (!isBasePath) {
135
+ parts.shift();
136
+ }
137
+ let path = `/${ parts.join('/')}`;
138
+ if (lastIsSlash && parts.length > 1) {
139
+ path += '/';
140
+ }
141
+ return path;
142
+ }
143
+
144
+ /**
145
+ * Returns query parameters string (without the '?' sign) as a whole.
146
+ *
147
+ * @param value The URL to parse
148
+ * @returns Value of the search string or undefined.
149
+ */
150
+ protected _parseSearch(value: string): string | undefined {
151
+ if (!value) {
152
+ return undefined;
153
+ }
154
+ let index = value.indexOf('?');
155
+ if (index === -1) {
156
+ return undefined;
157
+ }
158
+ const result = value.substring(index + 1);
159
+ index = result.indexOf('#');
160
+ if (index === -1) {
161
+ return result;
162
+ }
163
+ return result.substring(0, index);
164
+ }
165
+
166
+ /**
167
+ * Reads a value of the anchor (or hash) parameter without the `#` sign.
168
+ *
169
+ * @param value The URL to parse
170
+ * @returns Value of the anchor (hash) or undefined.
171
+ */
172
+ protected _parseAnchor(value: string): string | undefined {
173
+ if (!value) {
174
+ return undefined;
175
+ }
176
+ const index = value.indexOf('#');
177
+ if (index === -1) {
178
+ return undefined;
179
+ }
180
+ return value.substring(index + 1);
181
+ }
182
+
183
+ /**
184
+ * Returns an array of items where each item is an array where first
185
+ * item is param name and second is it's value. Both always strings.
186
+ *
187
+ * @param search Parsed search parameter
188
+ * @returns Always returns an array.
189
+ */
190
+ protected _parseSearchParams(search?: string): string[][] {
191
+ const result: string[][] = [];
192
+ if (!search) {
193
+ return result;
194
+ }
195
+ const parts = search.split(this.opts.queryDelimiter as string);
196
+ parts.forEach((item) => {
197
+ const _part = ['', ''];
198
+ const _params = item.split('=');
199
+ let _name = _params.shift();
200
+ if (!_name && _name !== '') {
201
+ return;
202
+ }
203
+ _name = _name.trim();
204
+ const _value = _params.join('=').trim();
205
+ _part[0] = _name;
206
+ _part[1] = _value;
207
+ result.push(_part);
208
+ });
209
+ return result;
210
+ }
211
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Awaits the set number of milliseconds before resolving the promise.
3
+ * @param timeout The number of milliseconds to wait.
4
+ */
5
+ export function sleep(timeout = 0): Promise<void> {
6
+ return new Promise((resolve) => {
7
+ setTimeout(() => resolve(), timeout);
8
+ });
9
+ }
@@ -1,4 +1,4 @@
1
- import { DataMockInit, HarTimingInit, HttpRequestInit, HttpResponseRedirectStatusInit } from '@pawel-up/data-mock/types'
1
+ import { DataMockInit, HarTimingInit, HttpResponseRedirectStatusInit } from '@pawel-up/data-mock/types'
2
2
 
3
3
  export interface ArcDataMockInit extends DataMockInit {
4
4
  }
@@ -0,0 +1,20 @@
1
+ import { DataMock, DataMockInit } from '@pawel-up/data-mock';
2
+ import { Request } from './lib/Request.js';
3
+ import { Response } from './lib/Response.js';
4
+
5
+ export { IRequestLogInit } from './lib/Request.js';
6
+ export { IArcResponseInit } from './lib/Response.js';
7
+
8
+ export class ProjectMock extends DataMock {
9
+ projectRequest: Request;
10
+ response: Response;
11
+
12
+ /**
13
+ * @param init The library init options.
14
+ */
15
+ constructor(init?: DataMockInit) {
16
+ super(init);
17
+ this.projectRequest = new Request(init);
18
+ this.response = new Response(init);
19
+ }
20
+ }
@@ -0,0 +1,85 @@
1
+ import { Http, Types, Lorem, Time, DataMockInit, HttpRequestInit } from '@pawel-up/data-mock';
2
+ // import { randomValue } from '@pawel-up/data-mock/src/lib/Http.js';
3
+ import { IHttpRequest, Kind as HttpRequestKind } from '../../models/HttpRequest.js';
4
+ import { IRequest, Kind as RequestKind } from '../../models/Request.js';
5
+ import { ISentRequest } from '../../models/SentRequest.js';
6
+ import { IRequestLog } from '../../models/RequestLog.js';
7
+ import { IArcResponseInit, Response } from './Response.js';
8
+
9
+ export interface IRequestLogInit {
10
+ request?: HttpRequestInit;
11
+ response?: IArcResponseInit;
12
+ /**
13
+ * When set it ignores size information
14
+ */
15
+ noSize?: boolean;
16
+ /**
17
+ * Adds redirects to the request
18
+ */
19
+ redirects?: boolean;
20
+ noResponse?: boolean;
21
+ noRequest?: boolean;
22
+ }
23
+
24
+ export class Request {
25
+ types: Types;
26
+ lorem: Lorem;
27
+ time: Time;
28
+ http: Http;
29
+ response: Response;
30
+
31
+ constructor(init: DataMockInit={}) {
32
+ this.types = new Types(init.seed);
33
+ this.lorem = new Lorem(init);
34
+ this.time = new Time(init);
35
+ this.http = new Http(init);
36
+ this.response = new Response(init);
37
+ }
38
+
39
+ request(init?: HttpRequestInit): IRequest {
40
+ return {
41
+ kind: RequestKind,
42
+ expects: this.httpRequest(init),
43
+ info: {
44
+ name: this.lorem.words(2),
45
+ description: this.lorem.paragraph(),
46
+ },
47
+ }
48
+ }
49
+
50
+ httpRequest(init?: HttpRequestInit): IHttpRequest {
51
+ const request = this.http.request(init);
52
+ return {
53
+ kind: HttpRequestKind,
54
+ ...request,
55
+ }
56
+ }
57
+
58
+ sentRequest(init?: HttpRequestInit): ISentRequest {
59
+ const start = this.time.timestamp();
60
+ return {
61
+ startTime: start,
62
+ endTime: this.time.timestamp({ min: start + 1 }),
63
+ ...this.httpRequest(init),
64
+ };
65
+ }
66
+
67
+ log(init: IRequestLogInit = {}): IRequestLog {
68
+ const result: IRequestLog = {
69
+ kind: 'ARC#ResponseLog',
70
+ };
71
+ if (!init.noRequest) {
72
+ result.request = this.sentRequest(init.request);
73
+ }
74
+ if (!init.noResponse) {
75
+ result.response = this.response.arcResponse(init.response);
76
+ }
77
+ if (init.redirects) {
78
+ result.redirects = this.response.redirects();
79
+ }
80
+ if (!init.noSize) {
81
+ result.size = this.response.size();
82
+ }
83
+ return result;
84
+ }
85
+ }
@@ -0,0 +1,101 @@
1
+ import { Http, Har, Types, Lorem, Time, DataMockInit, HttpResponseInit, HarTimingInit, Internet } from '@pawel-up/data-mock';
2
+ import { IHttpResponse, Kind as HttpResponseKind } from '../../models/HttpResponse.js';
3
+ import { IArcResponse, Kind as ArcResponseKind } from '../../models/ArcResponse.js';
4
+ import { IRequestsSize } from '../../models/RequestsSize.js';
5
+ import { IResponseRedirect, Kind as ResponseRedirectKind } from '../../models/ResponseRedirect.js';
6
+
7
+ export interface IArcResponseInit extends HttpResponseInit, HarTimingInit {
8
+ /**
9
+ * When set it does not generate a response payload.
10
+ */
11
+ noBody?: boolean;
12
+ /**
13
+ * The first number of the status group. Other 2 are auto generated
14
+ */
15
+ statusGroup?: number;
16
+ /**
17
+ * Whether to generate timings object
18
+ */
19
+ timings?: boolean;
20
+ }
21
+
22
+ export class Response {
23
+ types: Types;
24
+ lorem: Lorem;
25
+ time: Time;
26
+ http: Http;
27
+ har: Har;
28
+ internet: Internet;
29
+
30
+ constructor(init: DataMockInit={}) {
31
+ this.types = new Types(init.seed);
32
+ this.lorem = new Lorem(init);
33
+ this.time = new Time(init);
34
+ this.http = new Http(init);
35
+ this.har = new Har(init);
36
+ this.internet = new Internet(init);
37
+ }
38
+
39
+ response(init: IArcResponseInit = {}): IHttpResponse {
40
+ const ct = init.noBody ? undefined : this.http.headers.contentType();
41
+ const body = init.noBody ? undefined : this.http.payload.payload(ct);
42
+ const headers = this.http.headers.headers('response', { mime: ct });
43
+ const statusGroup = init.statusGroup ? init.statusGroup : this.types.number({ min: 2, max: 5 });
44
+ const sCode = this.types.number({ min: 0, max: 99 }).toString();
45
+ const code = Number(`${statusGroup}${sCode.padStart(2, '0')}`);
46
+ const status = this.lorem.word();
47
+ const result: IHttpResponse = {
48
+ kind: HttpResponseKind,
49
+ status: code,
50
+ statusText: status,
51
+ headers,
52
+ };
53
+ if (!init.noBody) {
54
+ result.payload = body;
55
+ }
56
+ return result;
57
+ }
58
+
59
+ arcResponse(init: IArcResponseInit={}): IArcResponse {
60
+ const base = this.response(init);
61
+ const length = this.types.number({ min: 10, max: 4000 });
62
+ const result: IArcResponse = {
63
+ ...base,
64
+ kind: ArcResponseKind,
65
+ loadingTime: length,
66
+ };
67
+ if (init.timings) {
68
+ result.timings = this.har.timing(init);
69
+ }
70
+ return result;
71
+ }
72
+
73
+ size(): IRequestsSize {
74
+ const result: IRequestsSize = {
75
+ request: this.types.number({ min: 10 }),
76
+ response: this.types.number({ min: 10 }),
77
+ };
78
+ return result;
79
+ }
80
+
81
+ redirect(init?: IArcResponseInit): IResponseRedirect {
82
+ const start = this.time.timestamp();
83
+ const end = this.time.timestamp({ min: start + 1 })
84
+ const info: IResponseRedirect = {
85
+ kind: ResponseRedirectKind,
86
+ startTime: start,
87
+ endTime: end,
88
+ url: this.internet.uri(),
89
+ response: this.response({ ...init, statusGroup: 3}),
90
+ };
91
+ return info;
92
+ }
93
+
94
+ redirects(size=1, init?: IArcResponseInit): IResponseRedirect[] {
95
+ const result: IResponseRedirect[] = [];
96
+ for (let i = 0; i < size; i++) {
97
+ result.push(this.redirect(init));
98
+ }
99
+ return result;
100
+ }
101
+ }
@@ -0,0 +1,29 @@
1
+ import { EventEmitter } from 'events';
2
+ import { IProjectExecutionIteration, IProjectExecutionLog } from '../reporters/Reporter.js';
3
+
4
+ export abstract class BaseRunner extends EventEmitter {
5
+ /**
6
+ * Iteration start time.
7
+ */
8
+ protected startTime?: number;
9
+ /**
10
+ * Iteration end time.
11
+ */
12
+ protected endTime?: number;
13
+ /**
14
+ * A list of already executed iterations.
15
+ */
16
+ protected executed: IProjectExecutionIteration[] = [];
17
+
18
+ /**
19
+ * Creates the report of the execution.
20
+ */
21
+ protected async createReport(): Promise<IProjectExecutionLog> {
22
+ const log: IProjectExecutionLog = {
23
+ started: this.startTime as number,
24
+ ended: this.endTime as number,
25
+ iterations: this.executed,
26
+ };
27
+ return log;
28
+ }
29
+ }
@@ -0,0 +1,234 @@
1
+ import cluster, { Worker } from 'cluster';
2
+ import { cpus } from 'os';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { HttpProject, IHttpProject } from '../../models/HttpProject.js';
6
+ import { IProjectRunnerOptions } from './ProjectRunner.js';
7
+ import { IProjectExecutionLog, IProjectExecutionIteration } from '../reporters/Reporter.js';
8
+ import { BaseRunner } from './BaseRunner.js';
9
+
10
+ const numCPUs = cpus().length;
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ export type WorkerStatus = 'initializing' | 'ready' | 'running' | 'finished' | 'error';
14
+
15
+ export interface IWorkerInfo {
16
+ /**
17
+ * Whether the worker is online.
18
+ */
19
+ online: boolean;
20
+ /**
21
+ * The number of iterations the worker is performing.
22
+ */
23
+ iterations: number;
24
+ /**
25
+ * The current status of the worker.
26
+ */
27
+ status: WorkerStatus;
28
+ /**
29
+ * Optional error message received from the worker.
30
+ */
31
+ message?: string;
32
+ }
33
+
34
+ interface WorkerInfoInternal extends IWorkerInfo {
35
+ worker: Worker;
36
+ }
37
+
38
+ export interface IWorkerMessage {
39
+ cmd: string;
40
+ data?: unknown;
41
+ }
42
+
43
+ export interface IProjectParallelRunnerOptions extends IProjectRunnerOptions {
44
+ }
45
+
46
+ export interface IProjectParallelWorkerOptions extends IProjectRunnerOptions {
47
+ project: IHttpProject;
48
+ }
49
+
50
+ export interface ProjectParallelRunner {
51
+ /**
52
+ * Dispatched when a status of a worker change.
53
+ * This can be used to render the current status.
54
+ */
55
+ on(event: 'status', listener: (info: IWorkerInfo[]) => void): this;
56
+ /**
57
+ * Dispatched when a status of a worker change.
58
+ * This can be used to render the current status.
59
+ */
60
+ once(event: 'status', listener: (info: IWorkerInfo[]) => void): this;
61
+ }
62
+
63
+ /**
64
+ * Runs a project in parallel.
65
+ * It creates a number of workers determined by the number of CPUs available on the current machine
66
+ * and the number of iterations defined in the configuration options.
67
+ *
68
+ * When the number of iterations is greater then the number of CPUs then
69
+ * the program distributes the remaining iterations among created workers.
70
+ *
71
+ * The program dispatched the `status` event. It is dispatched each time when the worker status
72
+ * change. This event can be user to refresh the UI to reflect the newest state.
73
+ */
74
+ export class ProjectParallelRunner extends BaseRunner {
75
+ project: HttpProject;
76
+ options: IProjectParallelRunnerOptions;
77
+ workers: WorkerInfoInternal[] = [];
78
+ private mainResolver?: (report: IProjectExecutionLog) => void;
79
+ private mainRejecter?: (err: Error) => void;
80
+
81
+ constructor(project: HttpProject, opts: IProjectParallelRunnerOptions = {}) {
82
+ super();
83
+ this.project = project;
84
+ this.options = opts || {};
85
+
86
+ this._exitHandler = this._exitHandler.bind(this);
87
+ }
88
+
89
+ execute(): Promise<IProjectExecutionLog> {
90
+ return new Promise((resolve, reject) => {
91
+ this.mainResolver = resolve;
92
+ this.mainRejecter = reject;
93
+ this._execute();
94
+ });
95
+ }
96
+
97
+ private _execute(): void {
98
+ try {
99
+ cluster.setupPrimary({
100
+ exec: join(__dirname, 'ProjectRunnerWorker.js'),
101
+ silent: true,
102
+ });
103
+ const { iterations = 1 } = this.options;
104
+ const poolSize = Math.min(iterations, numCPUs);
105
+ for (let i = 0; i < poolSize; i++) {
106
+ const worker = cluster.fork();
107
+ this.setupWorker(worker);
108
+ }
109
+ this.distributeIterations();
110
+ this.emit('status', this.getStatusWorkers());
111
+ cluster.on('exit', this._exitHandler);
112
+ } catch (e) {
113
+ const cause = e as Error;
114
+ this.mainRejecter!(cause);
115
+ }
116
+ }
117
+
118
+ private getStatusWorkers(): IWorkerInfo[] {
119
+ const { workers } = this;
120
+ const result: IWorkerInfo[] = [];
121
+ workers.forEach((info) => {
122
+ const cp = { ...info } as any;
123
+ delete cp.worker;
124
+ result.push(cp);
125
+ });
126
+ return result;
127
+ }
128
+
129
+ private distributeIterations(): void {
130
+ const workers = this.workers.length;
131
+ const { iterations = 1 } = this.options;
132
+ let iterationsRemaining = iterations - workers;
133
+ let currentIndex = 0;
134
+ while (iterationsRemaining > 0) {
135
+ this.workers[currentIndex].iterations += 1;
136
+ iterationsRemaining--;
137
+ currentIndex++;
138
+ if (currentIndex + 1 === workers) {
139
+ currentIndex = 0;
140
+ }
141
+ }
142
+ }
143
+
144
+ private setupWorker(worker: Worker): void {
145
+ this.workers.push({
146
+ worker,
147
+ online: false,
148
+ iterations: 1,
149
+ status: 'initializing',
150
+ });
151
+ worker.on('message', this._messageHandler.bind(this, worker));
152
+ }
153
+
154
+ private _messageHandler(worker: Worker, message: IWorkerMessage): void {
155
+ switch (message.cmd) {
156
+ case 'online': this.setOnline(worker); break;
157
+ case 'result': this.setRunResult(worker, message); break;
158
+ case 'error': this.setRunError(worker, message); break;
159
+ }
160
+ }
161
+
162
+ private _exitHandler(worker: Worker): void {
163
+ const info = this.workers.find(i => i.worker === worker);
164
+ if (!info) {
165
+ return;
166
+ }
167
+ this.finishWhenReady();
168
+ }
169
+
170
+ private setOnline(worker: Worker): void {
171
+ const info = this.workers.find(i => i.worker === worker);
172
+ if (!info) {
173
+ return;
174
+ }
175
+ info.online = true;
176
+ info.status = 'ready';
177
+ this.runWhenReady();
178
+ this.emit('status', this.getStatusWorkers());
179
+ }
180
+
181
+ private setRunResult(worker: Worker, message: IWorkerMessage): void {
182
+ const reports = message.data as IProjectExecutionIteration[];
183
+ this.executed = this.executed.concat(reports);
184
+ worker.destroy();
185
+ const info = this.workers.find(i => i.worker === worker);
186
+ if (!info) {
187
+ return;
188
+ }
189
+ info.status = 'finished';
190
+ this.emit('status', this.getStatusWorkers());
191
+ }
192
+
193
+ private runWhenReady(): void {
194
+ const waiting = this.workers.some(i => !i.online);
195
+ if (waiting) {
196
+ return;
197
+ }
198
+ this.startTime = Date.now();
199
+ this.workers.forEach((info) => {
200
+ const opts: IProjectParallelWorkerOptions = { ...this.options, project: this.project.toJSON() };
201
+ opts.iterations = info.iterations;
202
+ info.status = 'running';
203
+ info.worker.send({
204
+ cmd: 'run',
205
+ data: opts,
206
+ });
207
+ });
208
+ }
209
+
210
+ private async finishWhenReady(): Promise<void> {
211
+ if (this.endTime) {
212
+ return;
213
+ }
214
+ const working = this.workers.some(i => !['finished', 'error'].includes(i.status));
215
+ if (working || !this.mainResolver) {
216
+ return;
217
+ }
218
+ this.endTime = Date.now();
219
+ const report = await this.createReport();
220
+ this.mainResolver(report);
221
+ cluster.off('exit', this._exitHandler);
222
+ }
223
+
224
+ private setRunError(worker: Worker, message: IWorkerMessage): void {
225
+ worker.destroy();
226
+ const info = this.workers.find(i => i.worker === worker);
227
+ if (!info) {
228
+ return;
229
+ }
230
+ info.status = 'error';
231
+ info.message = message.data as string;
232
+ this.emit('status', this.getStatusWorkers());
233
+ }
234
+ }