@tskmgr/client 1.4.0 → 2.0.2

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 (41) hide show
  1. package/.babelrc +10 -0
  2. package/.eslintrc.json +21 -0
  3. package/jest.config.ts +17 -0
  4. package/package.json +6 -8
  5. package/project.json +31 -0
  6. package/src/{index.d.ts → index.ts} +1 -0
  7. package/src/lib/client-example.ts +134 -0
  8. package/src/lib/client-factory.ts +17 -0
  9. package/src/lib/client-options.ts +12 -0
  10. package/src/lib/client.spec.ts +5 -0
  11. package/src/lib/client.ts +280 -0
  12. package/src/lib/run-tasks-result.ts +17 -0
  13. package/src/lib/task-result.ts +10 -0
  14. package/src/lib/utils.ts +68 -0
  15. package/tsconfig.json +13 -0
  16. package/tsconfig.lib.json +12 -0
  17. package/tsconfig.spec.json +20 -0
  18. package/LICENSE +0 -21
  19. package/src/index.js +0 -22
  20. package/src/index.js.map +0 -1
  21. package/src/lib/client-example.d.ts +0 -11
  22. package/src/lib/client-example.js +0 -106
  23. package/src/lib/client-example.js.map +0 -1
  24. package/src/lib/client-factory.d.ts +0 -6
  25. package/src/lib/client-factory.js +0 -14
  26. package/src/lib/client-factory.js.map +0 -1
  27. package/src/lib/client-options.d.ts +0 -12
  28. package/src/lib/client-options.js +0 -3
  29. package/src/lib/client-options.js.map +0 -1
  30. package/src/lib/client.d.ts +0 -24
  31. package/src/lib/client.js +0 -249
  32. package/src/lib/client.js.map +0 -1
  33. package/src/lib/run-tasks-result.d.ts +0 -9
  34. package/src/lib/run-tasks-result.js +0 -14
  35. package/src/lib/run-tasks-result.js.map +0 -1
  36. package/src/lib/task-result.d.ts +0 -10
  37. package/src/lib/task-result.js +0 -3
  38. package/src/lib/task-result.js.map +0 -1
  39. package/src/lib/utils.d.ts +0 -14
  40. package/src/lib/utils.js +0 -72
  41. package/src/lib/utils.js.map +0 -1
package/.babelrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "presets": [
3
+ [
4
+ "@nx/js/babel",
5
+ {
6
+ "useBuiltIns": "usage"
7
+ }
8
+ ]
9
+ ]
10
+ }
package/.eslintrc.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": ["../../.eslintrc.json"],
3
+ "ignorePatterns": ["!**/*"],
4
+ "overrides": [
5
+ {
6
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7
+ "rules": {}
8
+ },
9
+ {
10
+ "files": ["*.ts", "*.tsx"],
11
+ "parserOptions": {
12
+ "project": ["libs/client/tsconfig.*?.json"]
13
+ },
14
+ "rules": {}
15
+ },
16
+ {
17
+ "files": ["*.js", "*.jsx"],
18
+ "rules": {}
19
+ }
20
+ ]
21
+ }
package/jest.config.ts ADDED
@@ -0,0 +1,17 @@
1
+ /* eslint-disable */
2
+ export default {
3
+ displayName: 'client',
4
+ preset: '../../jest.preset.js',
5
+ globals: {},
6
+ testEnvironment: 'node',
7
+ transform: {
8
+ '^.+\\.[tj]sx?$': [
9
+ 'ts-jest',
10
+ {
11
+ tsconfig: '<rootDir>/tsconfig.spec.json',
12
+ },
13
+ ],
14
+ },
15
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
16
+ coverageDirectory: '../../coverage/libs/client',
17
+ };
package/package.json CHANGED
@@ -1,15 +1,13 @@
1
1
  {
2
2
  "name": "@tskmgr/client",
3
- "version": "1.4.0",
3
+ "version": "2.0.2",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
6
  "debug": "^4.3.4",
7
7
  "node-fetch": "^2.6.7",
8
- "systeminformation": "^5.16.6",
9
- "@tskmgr/common": "1.4.0",
10
- "uuid": "^9.0.0"
11
- },
12
- "main": "./src/index.js",
13
- "types": "./src/index.d.ts",
14
- "peerDependencies": {}
8
+ "@tskmgr/common": "2.0.2",
9
+ "uuid": "^9.0.1",
10
+ "@nx/devkit": "18.0.7",
11
+ "form-data": "2.3.3"
12
+ }
15
13
  }
package/project.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "client",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/client/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "lint": {
8
+ "executor": "@nx/eslint:lint",
9
+ "outputs": ["{options.outputFile}"]
10
+ },
11
+ "test": {
12
+ "executor": "@nx/jest:jest",
13
+ "outputs": ["{workspaceRoot}/coverage/libs/client"],
14
+ "options": {
15
+ "jestConfig": "libs/client/jest.config.ts"
16
+ }
17
+ },
18
+ "build": {
19
+ "executor": "@nx/js:tsc",
20
+ "outputs": ["{options.outputPath}"],
21
+ "options": {
22
+ "outputPath": "dist/libs/client",
23
+ "tsConfig": "libs/client/tsconfig.lib.json",
24
+ "packageJson": "libs/client/package.json",
25
+ "main": "libs/client/src/index.ts",
26
+ "assets": ["libs/client/*.md", "LICENSE"]
27
+ }
28
+ }
29
+ },
30
+ "tags": []
31
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './lib/client';
2
2
  export * from './lib/client-options';
3
3
  export * from './lib/client-factory';
4
+
4
5
  export * from './lib/run-tasks-result';
5
6
  export * from './lib/task-result';
@@ -0,0 +1,134 @@
1
+ /**
2
+ * IntelliJ debug:
3
+ * Node parameters: --require ts-node/register --require tsconfig-paths/register
4
+ * Environment variables: TS_NODE_PROJECT=libs/client/tsconfig.lib.json
5
+ *
6
+ * Start API:
7
+ * nx serve api
8
+ * Command:
9
+ * DEBUG=tskmgr:* ts-node --project libs/client/tsconfig.lib.json -r tsconfig-paths/register "libs/client/src/lib/client-example.ts"
10
+ */
11
+
12
+ import { execSync } from 'child_process';
13
+ import { CreateTaskDto, Run, Task, TaskPriority } from '@tskmgr/common';
14
+ import { ClientOptions } from './client-options';
15
+ import { ClientFactory } from './client-factory';
16
+ import { v4 as uuid } from 'uuid';
17
+ import Debug from 'debug';
18
+ import { readJsonFile } from '@nx/devkit';
19
+ import { unlinkSync } from 'fs';
20
+ const debug = Debug('tskmgr:client-example');
21
+
22
+ delete process.env.TS_NODE_PROJECT;
23
+
24
+ const options: ClientOptions = {
25
+ parallel: 1,
26
+ dataCallback,
27
+ errorCallback,
28
+ spawnOptions: { env: { ...process.env } },
29
+ };
30
+ const client = ClientFactory.createNew('http://localhost:3333', 'RUNNER_1', options);
31
+
32
+ let completed = false;
33
+ let run: Run;
34
+
35
+ (async () => {
36
+ try {
37
+ // 1. Create the new run
38
+ run = await client.createRun({
39
+ name: uuid(),
40
+ type: '123',
41
+ prioritization: [TaskPriority.Longest],
42
+ });
43
+ debug(run);
44
+
45
+ // 2. Leader should create some tasks to run
46
+ const election = await client.setLeader(run.id);
47
+ if (election.leader) {
48
+ const tasks = getNxTasks().map<CreateTaskDto>((nxTask) => {
49
+ const command = `npx nx run ${nxTask.target.project}:${nxTask.target.target} --configuration=production`;
50
+ return {
51
+ name: nxTask.target.project,
52
+ type: nxTask.target.target,
53
+ command: command.trim(),
54
+ options: { shell: true },
55
+ priority: TaskPriority.Longest,
56
+ };
57
+ });
58
+
59
+ const createdTasks = await client.createTasks(run.id, { tasks });
60
+ debug(createdTasks);
61
+
62
+ const closeRun = await client.closeRun(run.id);
63
+ debug(closeRun);
64
+ }
65
+
66
+ // 3. Execute tasks
67
+ const result = await client.runTasks(run.id);
68
+ if (result.completed) {
69
+ // if failFast set to false, runTasks will continue without throwing errors.
70
+ completed = true;
71
+ }
72
+ } catch (e) {
73
+ console.error(e);
74
+ } finally {
75
+ // 4. See results
76
+ console.log('--------------------------------------------------');
77
+ console.log(` tskmgr run: http://localhost:4200/runs/${run.id}`);
78
+ console.log('--------------------------------------------------');
79
+ console.log(`${completed ? 'COMPLETED!' : 'FAILED!'}`);
80
+ process.exit(completed ? 0 : 1);
81
+ }
82
+ })();
83
+
84
+ function getNxTasks(): NxTask[] {
85
+ const graphFileName = uuid() + '.json';
86
+ // In CI environment use npx nx affected --graph=${graphFileName} --target=lint command. run-many is just for demo purpose.
87
+ execSync(`npx nx run-many --graph=lint-${graphFileName} --target=lint`);
88
+ const lintJson = readJsonFile(`lint-${graphFileName}`);
89
+ unlinkSync(`lint-${graphFileName}`);
90
+ const lintTasks: NxTask[] = Object.values(lintJson.tasks.tasks);
91
+
92
+ execSync(`npx nx run-many --graph=test-${graphFileName} --target=test`);
93
+ const testJson = readJsonFile(`test-${graphFileName}`);
94
+ unlinkSync(`test-${graphFileName}`);
95
+ const testTasks: NxTask[] = Object.values(testJson.tasks.tasks);
96
+
97
+ execSync(`npx nx run-many --graph=build-${graphFileName} --target=build`);
98
+ const buildJson = readJsonFile(`build-${graphFileName}`);
99
+ unlinkSync(`build-${graphFileName}`);
100
+ const buildTasks: NxTask[] = Object.values(buildJson.tasks.tasks);
101
+
102
+ execSync(`npx nx run-many --graph=e2e-${graphFileName} --target=e2e`);
103
+ const e2eJson = readJsonFile(`e2e-${graphFileName}`);
104
+ unlinkSync(`e2e-${graphFileName}`);
105
+ const e2eTasks: NxTask[] = Object.values(e2eJson.tasks.tasks);
106
+
107
+ return [...lintTasks, ...testTasks, ...buildTasks, ...e2eTasks];
108
+ }
109
+
110
+ function dataCallback(task: Task, data: string, cached: () => void): void {
111
+ // > nx run frontend:lint [existing outputs match the cache, left as is]
112
+ // > nx run client:lint [local cache]
113
+ if (
114
+ (data.startsWith(`> nx run ${task.name}:${task.type}`) &&
115
+ data.endsWith('[existing outputs match the cache, left as is]')) ||
116
+ data.endsWith('[local cache]')
117
+ ) {
118
+ cached();
119
+ }
120
+
121
+ console.log(`[stdout] ${data}`);
122
+ }
123
+
124
+ function errorCallback(task: Task, data: string): void {
125
+ console.log(`[stderr] ${data}`);
126
+ }
127
+
128
+ interface NxTask {
129
+ id: string;
130
+ overrides: any;
131
+ target: { project: string; target: string };
132
+ command: string;
133
+ outputs: string[];
134
+ }
@@ -0,0 +1,17 @@
1
+ import { Client } from './client';
2
+ import { ClientOptions } from './client-options';
3
+ import { ApiUrl } from '@tskmgr/common';
4
+
5
+ export class ClientFactory {
6
+ public static createNew(
7
+ baseUrl: string, //
8
+ runnerId: string,
9
+ options?: ClientOptions
10
+ ): Client {
11
+ return new Client(
12
+ ApiUrl.create(baseUrl), //
13
+ runnerId,
14
+ { ...Client.DefaultOptions, ...options }
15
+ );
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ import { Task } from '@tskmgr/common';
2
+ import { SpawnOptionsWithoutStdio } from 'child_process';
3
+
4
+ export interface ClientOptions {
5
+ parallel?: number;
6
+ dataCallback?: (task: Task, data: string, cached: () => void) => void;
7
+ errorCallback?: (task: Task, data: string) => void;
8
+ pollingDelayMs?: number;
9
+ retryDelayMs?: number;
10
+ retryCount?: number;
11
+ spawnOptions?: SpawnOptionsWithoutStdio;
12
+ }
@@ -0,0 +1,5 @@
1
+ describe('client', () => {
2
+ it('should work', () => {
3
+ //
4
+ });
5
+ });
@@ -0,0 +1,280 @@
1
+ import { ChildProcess } from 'child_process';
2
+ import {
3
+ ApiUrl,
4
+ Run,
5
+ CompleteTaskDto,
6
+ CreateRunRequestDto,
7
+ CreateTasksDto,
8
+ StartTaskDto,
9
+ StartTaskResponseDto,
10
+ Task,
11
+ File as File_,
12
+ SetLeaderRequestDto,
13
+ SetLeaderResponseDto,
14
+ CreateFileRequestDto,
15
+ } from '@tskmgr/common';
16
+ import fetch from 'node-fetch';
17
+ import * as FormData from 'form-data';
18
+ import { checkStatus, delay, getTaskLogFilename, spawnAsync } from './utils';
19
+ import { RunTasksResult } from './run-tasks-result';
20
+ import { TaskResult } from './task-result';
21
+ import { ClientOptions } from './client-options';
22
+ import { createReadStream, createWriteStream, unlinkSync } from 'fs';
23
+ import Debug from 'debug';
24
+
25
+ const debug = Debug('tskmgr:client');
26
+
27
+ export class Client {
28
+ public static readonly DefaultOptions: ClientOptions = {
29
+ parallel: 1,
30
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
31
+ dataCallback: (task, data, cached) => {},
32
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
33
+ errorCallback: (task, data) => {},
34
+ pollingDelayMs: 5000,
35
+ retryDelayMs: 5000,
36
+ retryCount: 2,
37
+ };
38
+
39
+ constructor(
40
+ private readonly apiUrl: ApiUrl,
41
+ private readonly runnerId: string,
42
+ private readonly options: ClientOptions
43
+ ) {}
44
+
45
+ public async createRun(params: CreateRunRequestDto): Promise<Run> {
46
+ const res = await fetch(this.apiUrl.createRunUrl(), {
47
+ headers: { 'Content-Type': 'application/json' },
48
+ method: 'POST',
49
+ body: JSON.stringify(params),
50
+ });
51
+
52
+ return await checkStatus(res).json();
53
+ }
54
+
55
+ public async closeRun(runId: number): Promise<Run> {
56
+ const res = await fetch(this.apiUrl.closeRunUrl(runId), {
57
+ headers: { 'Content-Type': 'application/json' },
58
+ method: 'PUT',
59
+ });
60
+
61
+ return await checkStatus(res).json();
62
+ }
63
+
64
+ public async abortRun(runId: number): Promise<Run> {
65
+ const res = await fetch(this.apiUrl.abortRunUrl(runId), {
66
+ headers: { 'Content-Type': 'application/json' },
67
+ method: 'PUT',
68
+ });
69
+
70
+ return await checkStatus(res).json();
71
+ }
72
+
73
+ public async failRun(runId: number): Promise<Run> {
74
+ const res = await fetch(this.apiUrl.failRunUrl(runId), {
75
+ headers: { 'Content-Type': 'application/json' },
76
+ method: 'PUT',
77
+ });
78
+
79
+ return await checkStatus(res).json();
80
+ }
81
+
82
+ public async setLeader(runId: number): Promise<SetLeaderResponseDto> {
83
+ const params: SetLeaderRequestDto = { runnerId: this.runnerId };
84
+ const res = await fetch(this.apiUrl.setLeaderUrl(runId), {
85
+ headers: { 'Content-Type': 'application/json' },
86
+ method: 'PUT',
87
+ body: JSON.stringify(params),
88
+ });
89
+
90
+ return await checkStatus(res).json();
91
+ }
92
+
93
+ public async createTasks(runId: number, params: CreateTasksDto): Promise<Task[]> {
94
+ const res = await fetch(this.apiUrl.createTasksUrl(runId), {
95
+ headers: { 'Content-Type': 'application/json' },
96
+ method: 'POST',
97
+ body: JSON.stringify(params),
98
+ });
99
+
100
+ return await checkStatus(res).json();
101
+ }
102
+
103
+ public async startTask(runId: number, params: StartTaskDto): Promise<StartTaskResponseDto> {
104
+ const res = await fetch(this.apiUrl.startTaskUrl(runId), {
105
+ headers: { 'Content-Type': 'application/json' },
106
+ method: 'PUT',
107
+ body: JSON.stringify(params),
108
+ });
109
+
110
+ return await checkStatus(res).json();
111
+ }
112
+
113
+ public async runTasks(runId: number): Promise<RunTasksResult> {
114
+ const taskRunners: Promise<TaskResult[]>[] = [];
115
+
116
+ for (let i = 0; i < this.options.parallel; i++) {
117
+ taskRunners.push(this.defaultParallelTaskRunner(runId, i));
118
+ }
119
+
120
+ const taskResults = await Promise.all(taskRunners);
121
+ return new RunTasksResult(taskResults.flatMap((x) => x));
122
+ }
123
+
124
+ public async completeTask(taskId: number, params: CompleteTaskDto): Promise<Task> {
125
+ const res = await fetch(this.apiUrl.completeTaskUrl(taskId), {
126
+ headers: { 'Content-Type': 'application/json' },
127
+ method: 'PUT',
128
+ body: JSON.stringify(params),
129
+ });
130
+
131
+ return await checkStatus(res).json();
132
+ }
133
+
134
+ public async failTask(taskId: number): Promise<Task> {
135
+ const res = await fetch(this.apiUrl.failTaskUrl(taskId), {
136
+ headers: { 'Content-Type': 'application/json' },
137
+ method: 'PUT',
138
+ });
139
+
140
+ return await checkStatus(res).json();
141
+ }
142
+
143
+ public async uploadRunFile(runId: number, path: string, params: CreateFileRequestDto): Promise<File_> {
144
+ return this.uploadFile(this.apiUrl.createFileRunUrl(runId), path, params);
145
+ }
146
+
147
+ public async uploadTaskFile(taskId: number, path: string, params: CreateFileRequestDto): Promise<File_> {
148
+ return this.uploadFile(this.apiUrl.createFileTaskUrl(taskId), path, params);
149
+ }
150
+
151
+ private async uploadFile(url: string, path: string, params: CreateFileRequestDto): Promise<File_> {
152
+ // https://github.com/node-fetch/node-fetch/tree/2.x#post-with-form-data-detect-multipart
153
+ // https://github.com/form-data/form-data#readme
154
+
155
+ const formData = new FormData();
156
+ formData.append('file', createReadStream(path));
157
+
158
+ if (params.type) {
159
+ formData.append('type', params.type);
160
+ }
161
+
162
+ if (params.description) {
163
+ formData.append('description', params.description);
164
+ }
165
+
166
+ const res = await fetch(url, {
167
+ method: 'POST',
168
+ body: formData,
169
+ });
170
+
171
+ return await checkStatus(res).json();
172
+ }
173
+
174
+ private async defaultParallelTaskRunner(runId: number, parallelId: number): Promise<TaskResult[]> {
175
+ const logInfo = `[${this.runnerId}:${parallelId}]`;
176
+ const taskResults: TaskResult[] = [];
177
+ let _continue = true;
178
+
179
+ debug(`${logInfo} parallel task runner started`);
180
+
181
+ while (_continue) {
182
+ const res = await this.startTask(runId, { runnerId: this.runnerId });
183
+
184
+ if (!res.continue) {
185
+ debug(`${logInfo} continue set to false`);
186
+ _continue = false;
187
+ }
188
+
189
+ if (!res.continue && !res.task) {
190
+ debug(`${logInfo} continue set to false and no task, exiting`);
191
+ continue;
192
+ }
193
+
194
+ if (!res.task) {
195
+ debug(`${logInfo} polling for new tasks in ${this.options.pollingDelayMs} ms`);
196
+ await delay(this.options.pollingDelayMs);
197
+ continue;
198
+ }
199
+
200
+ const { run, task } = res;
201
+ debug(`${logInfo} received task: ${task.id}`);
202
+
203
+ let cached = false;
204
+ let completed = false;
205
+ let writable = false;
206
+ let childProcess: ChildProcess;
207
+ let error: Error;
208
+
209
+ const taskLogFilename = getTaskLogFilename(task.id);
210
+ const writeStream = createWriteStream(taskLogFilename);
211
+ writeStream.on('open', () => (writable = true));
212
+
213
+ const dataHandler = (data: string): void => {
214
+ this.options.dataCallback(task, data, () => (cached = true));
215
+ if (writable) {
216
+ writeStream.write(`[${new Date().toISOString()}] ${data}\n`);
217
+ }
218
+ };
219
+
220
+ const errorHandler = (data: string): void => {
221
+ this.options.errorCallback(task, data);
222
+ if (writable) {
223
+ writeStream.write(`[${new Date().toISOString()}] ${data}\n`);
224
+ }
225
+ };
226
+
227
+ debug(`${logInfo} starting task: ${task.id}`);
228
+ try {
229
+ childProcess = await spawnAsync(
230
+ task.command,
231
+ task.arguments,
232
+ {
233
+ ...task.options,
234
+ ...this.options.spawnOptions,
235
+ },
236
+ {
237
+ dataCallback: dataHandler,
238
+ errorCallback: errorHandler,
239
+ }
240
+ );
241
+ completed = true;
242
+ debug(`${logInfo} completed task: ${task.id}`);
243
+ } catch (err) {
244
+ console.error(`${logInfo} failed task: ${task.id} with error: ${err}`);
245
+ error = err;
246
+
247
+ if (run.failFast) {
248
+ // TODO: should abort all running tasks by current client
249
+ debug(`${logInfo} fail fast enabled, aborting`);
250
+ throw err;
251
+ }
252
+ } finally {
253
+ taskResults.push({ run, task, childProcess, completed, error });
254
+
255
+ if (completed) {
256
+ await this.completeTask(task.id, { cached });
257
+ debug(`${logInfo} completed status for task: ${task.id} sent`);
258
+ } else {
259
+ await this.failTask(task.id);
260
+ debug(`${logInfo} failed status for task: ${task.id} sent`);
261
+ }
262
+
263
+ writeStream.end();
264
+ writeStream.close();
265
+
266
+ await this.uploadTaskFile(task.id, taskLogFilename, {
267
+ type: 'log',
268
+ description: `Log for task ${task.id}`,
269
+ });
270
+
271
+ debug(`${logInfo} uploaded file for task: ${task.id} sent`);
272
+ unlinkSync(taskLogFilename);
273
+ }
274
+ }
275
+
276
+ debug(`${logInfo} parallel task runner completed`);
277
+
278
+ return taskResults;
279
+ }
280
+ }
@@ -0,0 +1,17 @@
1
+ import { TaskResult } from './task-result';
2
+
3
+ export class RunTasksResult {
4
+ public readonly completedTasks: TaskResult[];
5
+ public readonly failedTasks: TaskResult[];
6
+
7
+ public readonly completed: boolean;
8
+ public readonly failed: boolean;
9
+
10
+ constructor(public readonly tasks: TaskResult[]) {
11
+ this.completedTasks = tasks.filter((x) => x.completed);
12
+ this.failedTasks = tasks.filter((x) => !x.completed);
13
+
14
+ this.completed = this.failedTasks.length === 0;
15
+ this.failed = this.failedTasks.length > 0;
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ import { Run, Task } from '@tskmgr/common';
2
+ import { ChildProcess } from 'child_process';
3
+
4
+ export interface TaskResult {
5
+ run: Run;
6
+ task: Task;
7
+ childProcess: ChildProcess;
8
+ completed: boolean;
9
+ error?: Error;
10
+ }
@@ -0,0 +1,68 @@
1
+ import { ChildProcess, spawn, SpawnOptionsWithoutStdio } from 'child_process';
2
+ import { createInterface } from 'readline';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ export interface SpawnAsyncOptions {
7
+ dataCallback?: (data: string) => void;
8
+ errorCallback?: (data: string) => void;
9
+ }
10
+
11
+ export async function spawnAsync(
12
+ command: string,
13
+ args?: ReadonlyArray<string>,
14
+ options?: SpawnOptionsWithoutStdio,
15
+ logging?: SpawnAsyncOptions
16
+ ): Promise<ChildProcess> {
17
+ return new Promise((resolve, reject) => {
18
+ const childProcess = spawn(command, args, options);
19
+
20
+ if (logging.dataCallback) {
21
+ const readlineStdout = createInterface({ input: childProcess.stdout });
22
+ readlineStdout.on('line', logging.dataCallback);
23
+ }
24
+
25
+ if (logging.errorCallback) {
26
+ const readlineStderr = createInterface({ input: childProcess.stderr });
27
+ readlineStderr.on('line', logging.errorCallback);
28
+ }
29
+
30
+ childProcess.on('close', (code) => {
31
+ if (code === 0) {
32
+ resolve(childProcess);
33
+ } else {
34
+ reject(childProcess);
35
+ }
36
+ });
37
+
38
+ childProcess.on('error', (err: Error) => {
39
+ reject(childProcess); // TODO: try to return error message
40
+ });
41
+ });
42
+ }
43
+
44
+ export function delay(ms: number): Promise<void> {
45
+ return new Promise((resolve) => {
46
+ setTimeout(resolve, ms);
47
+ });
48
+ }
49
+
50
+ export function getTaskLogFilename(taskId: number): string {
51
+ return join(tmpdir(), `task-${taskId}.log`);
52
+ }
53
+
54
+ export class HTTPResponseError extends Error {
55
+ constructor(public readonly response) {
56
+ super(`HTTP Error Response: ${response.status} ${response.statusText}`);
57
+ this.response = response;
58
+ }
59
+ }
60
+
61
+ export const checkStatus = (response) => {
62
+ if (response.ok) {
63
+ // response.status >= 200 && response.status < 300
64
+ return response;
65
+ } else {
66
+ throw new HTTPResponseError(response);
67
+ }
68
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.lib.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "outDir": "../../dist/out-tsc",
6
+ "declaration": true,
7
+ "types": ["node"],
8
+ "importHelpers": false
9
+ },
10
+ "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
11
+ "include": ["**/*.ts"]
12
+ }