@cleverbrush/scheduler 1.0.0-beta.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/README.md +1 -0
- package/dist/CloneReadableStream.d.ts +9 -0
- package/dist/CloneReadableStream.d.ts.map +1 -0
- package/dist/CloneReadableStream.js +25 -0
- package/dist/CloneReadableStream.js.map +1 -0
- package/dist/ScheduleCalculator.d.ts +10 -0
- package/dist/ScheduleCalculator.d.ts.map +1 -0
- package/dist/ScheduleCalculator.js +208 -0
- package/dist/ScheduleCalculator.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +307 -0
- package/dist/index.js.map +1 -0
- package/dist/jobRepository.d.ts +32 -0
- package/dist/jobRepository.d.ts.map +1 -0
- package/dist/jobRepository.js +84 -0
- package/dist/jobRepository.js.map +1 -0
- package/dist/reports.d.ts +0 -0
- package/dist/reports.js +231 -0
- package/dist/types.d.ts +577 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +122 -0
- package/dist/types.js.map +1 -0
- package/package.json +23 -0
- package/src/ScheduleCalculator.test.ts +482 -0
- package/src/ScheduleCalculator.ts +355 -0
- package/src/index.ts +419 -0
- package/src/jobRepository.ts +135 -0
- package/src/types.ts +219 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { Readable, PassThrough } from 'stream';
|
|
4
|
+
import { access as fsAccess } from 'fs/promises';
|
|
5
|
+
import { join as pathJoin } from 'path';
|
|
6
|
+
import { Worker } from 'worker_threads';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
schemaRegistry,
|
|
10
|
+
JobSchedulerProps,
|
|
11
|
+
CreateJobRequest,
|
|
12
|
+
SchedulerStatus,
|
|
13
|
+
Schedule,
|
|
14
|
+
Job,
|
|
15
|
+
JobInstance,
|
|
16
|
+
JobInstanceStatus,
|
|
17
|
+
Schemas
|
|
18
|
+
} from './types.js';
|
|
19
|
+
|
|
20
|
+
import { ScheduleCalculator } from './ScheduleCalculator.js';
|
|
21
|
+
|
|
22
|
+
import { IJobRepository, InMemoryJobRepository } from './jobRepository.js';
|
|
23
|
+
|
|
24
|
+
export { ScheduleCalculator, Schedule as TaskSchedule, Schemas };
|
|
25
|
+
|
|
26
|
+
type WorkerResult = {
|
|
27
|
+
status: JobInstanceStatus;
|
|
28
|
+
exitCode: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const CHECK_INTERVAL = 1000 * 10; // every 10 seconds
|
|
32
|
+
// const SCHEDULE_JOB_SPAN = 1000 * 60 * 60; // 1 hour
|
|
33
|
+
const SCHEDULE_JOB_SPAN = 1000 * 60; // 1 hour
|
|
34
|
+
const DEFAULT_JOB_TIMEOUT = 1000 * 20; // 20 seconds
|
|
35
|
+
const DEFAULT_MAX_CONSEQUENT_FAILS = 3;
|
|
36
|
+
|
|
37
|
+
type JobStartItem = {
|
|
38
|
+
jobId: string;
|
|
39
|
+
instanceId: number;
|
|
40
|
+
stdout: Readable;
|
|
41
|
+
stderr: Readable;
|
|
42
|
+
startDate: Date;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type JobEndItem = JobStartItem & {
|
|
46
|
+
endDate: Date;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type Events = {
|
|
50
|
+
'job:start': (job: JobStartItem) => any;
|
|
51
|
+
'job:end': (job: JobEndItem) => any;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
interface IJobScheduler {
|
|
55
|
+
on<T extends keyof Events>(name: T, callback: Events[T]): this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class JobScheduler extends EventEmitter implements IJobScheduler {
|
|
59
|
+
protected _rootFolder: string;
|
|
60
|
+
protected _status: SchedulerStatus = 'stopped';
|
|
61
|
+
protected _defaultTimezone: string;
|
|
62
|
+
|
|
63
|
+
protected _checkTimer;
|
|
64
|
+
|
|
65
|
+
protected _jobsRepository: IJobRepository = new InMemoryJobRepository();
|
|
66
|
+
|
|
67
|
+
protected _jobProps: Map<string, any> = new Map<string, any>();
|
|
68
|
+
|
|
69
|
+
public get status() {
|
|
70
|
+
return this._status;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected set status(val: SchedulerStatus) {
|
|
74
|
+
if (val === this._status) return;
|
|
75
|
+
this._status = val;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private scheduleCalculatorCache = new Map<string, ScheduleCalculator>();
|
|
79
|
+
|
|
80
|
+
protected async getJobSchedule(job: Job) {
|
|
81
|
+
if (this.scheduleCalculatorCache.has(job.id)) {
|
|
82
|
+
return this.scheduleCalculatorCache.get(job.id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const schedule = {
|
|
86
|
+
...job.schedule
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (typeof job.firstInstanceEndedAt !== 'undefined') {
|
|
90
|
+
schedule.startsOn = job.firstInstanceEndedAt;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof job.successfullTimesRunned === 'number') {
|
|
94
|
+
schedule.startingFromIndex = job.successfullTimesRunned + 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const res = new ScheduleCalculator(schedule);
|
|
98
|
+
this.scheduleCalculatorCache.set(job.id, res);
|
|
99
|
+
|
|
100
|
+
return res;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected runWorkerWithTimeout(file: string, props: any, timeout: number) {
|
|
104
|
+
const worker = new Worker(file, {
|
|
105
|
+
workerData: props,
|
|
106
|
+
execArgv: ['--unhandled-rejections=strict'],
|
|
107
|
+
stderr: true,
|
|
108
|
+
stdout: true
|
|
109
|
+
});
|
|
110
|
+
const promise = new Promise<WorkerResult>((resolve) => {
|
|
111
|
+
let timedOut = false;
|
|
112
|
+
let isFinished = false;
|
|
113
|
+
|
|
114
|
+
const timeoutTimer = setTimeout(() => {
|
|
115
|
+
if (isFinished) return;
|
|
116
|
+
timedOut = true;
|
|
117
|
+
worker.terminate();
|
|
118
|
+
resolve({
|
|
119
|
+
exitCode: 1,
|
|
120
|
+
status: 'timedout'
|
|
121
|
+
});
|
|
122
|
+
}, timeout);
|
|
123
|
+
|
|
124
|
+
worker.on('error', (e) => e);
|
|
125
|
+
|
|
126
|
+
worker.on('exit', (exitCode) => {
|
|
127
|
+
if (isFinished) return;
|
|
128
|
+
if (timedOut) return;
|
|
129
|
+
clearTimeout(timeoutTimer);
|
|
130
|
+
isFinished = true;
|
|
131
|
+
resolve({
|
|
132
|
+
status: exitCode === 0 ? 'succeeded' : 'errored',
|
|
133
|
+
exitCode
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
promise,
|
|
140
|
+
stderr: worker.stderr,
|
|
141
|
+
stdout: worker.stdout
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private readToEnd(source: Readable): Promise<Buffer> {
|
|
146
|
+
return new Promise<Buffer>((res, rej) => {
|
|
147
|
+
const chunks = [];
|
|
148
|
+
source.on('data', (chunk) => chunks.push(chunk));
|
|
149
|
+
source.on('end', () => res(Buffer.concat(chunks)));
|
|
150
|
+
source.on('error', (err) => rej(err));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected async startJobInstance(instance: JobInstance): Promise<void> {
|
|
155
|
+
const startDate = new Date();
|
|
156
|
+
instance = await this._jobsRepository.saveInstance({
|
|
157
|
+
...instance,
|
|
158
|
+
status: 'running',
|
|
159
|
+
startDate
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let status: JobInstanceStatus, exitCode: number;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const job = await this._jobsRepository.getJobById(instance.jobId);
|
|
166
|
+
|
|
167
|
+
const fileName = pathJoin(this._rootFolder, job.path);
|
|
168
|
+
|
|
169
|
+
const props = this._jobProps.get(job.id);
|
|
170
|
+
|
|
171
|
+
let finalProps = typeof props === 'function' ? props() : props;
|
|
172
|
+
if (finalProps instanceof Promise) {
|
|
173
|
+
finalProps = await finalProps;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
instance = await this._jobsRepository.saveInstance({
|
|
177
|
+
...instance,
|
|
178
|
+
status: 'running'
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { promise, stderr, stdout } = this.runWorkerWithTimeout(
|
|
182
|
+
fileName,
|
|
183
|
+
finalProps,
|
|
184
|
+
job.timeout
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const stdOutPass = new PassThrough();
|
|
188
|
+
stdout.pipe(stdOutPass);
|
|
189
|
+
const stdErrPass = new PassThrough();
|
|
190
|
+
stderr.pipe(stdErrPass);
|
|
191
|
+
|
|
192
|
+
const stdOutForJobStart = stdout.pipe(new PassThrough());
|
|
193
|
+
const stdErrForJobStart = stderr.pipe(new PassThrough());
|
|
194
|
+
|
|
195
|
+
const stdOutForJobEnd = stdout.pipe(new PassThrough());
|
|
196
|
+
const stdErrForJobEnd = stderr.pipe(new PassThrough());
|
|
197
|
+
|
|
198
|
+
this.emit('job:start', {
|
|
199
|
+
instanceId: instance.id,
|
|
200
|
+
jobId: job.id,
|
|
201
|
+
stderr: stdErrForJobStart,
|
|
202
|
+
stdout: stdOutForJobStart,
|
|
203
|
+
startDate
|
|
204
|
+
} as JobStartItem);
|
|
205
|
+
|
|
206
|
+
const stdOutStr = (await this.readToEnd(stdOutPass)).toString();
|
|
207
|
+
const stdErrStr = (await this.readToEnd(stdErrPass)).toString();
|
|
208
|
+
|
|
209
|
+
const result = await promise;
|
|
210
|
+
status = result.status;
|
|
211
|
+
exitCode = result.exitCode;
|
|
212
|
+
|
|
213
|
+
const endDate = new Date();
|
|
214
|
+
|
|
215
|
+
this.emit('job:end', {
|
|
216
|
+
instanceId: instance.id,
|
|
217
|
+
jobId: job.id,
|
|
218
|
+
stderr: stdErrForJobEnd,
|
|
219
|
+
stdout: stdOutForJobEnd,
|
|
220
|
+
startDate,
|
|
221
|
+
endDate
|
|
222
|
+
} as JobStartItem);
|
|
223
|
+
|
|
224
|
+
instance = await this._jobsRepository.saveInstance({
|
|
225
|
+
...instance,
|
|
226
|
+
status,
|
|
227
|
+
exitCode,
|
|
228
|
+
stdErr: stdErrStr,
|
|
229
|
+
stdOut: stdOutStr,
|
|
230
|
+
endDate
|
|
231
|
+
});
|
|
232
|
+
} finally {
|
|
233
|
+
const job = {
|
|
234
|
+
...(await this._jobsRepository.getJobById(instance.jobId))
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
job.timesRunned++;
|
|
238
|
+
|
|
239
|
+
let shouldRetry = false;
|
|
240
|
+
|
|
241
|
+
const schedule = await this.getJobSchedule(job);
|
|
242
|
+
|
|
243
|
+
if (status !== 'succeeded') {
|
|
244
|
+
if (job.consequentFailsCount + 1 >= job.maxConsequentFails) {
|
|
245
|
+
job.status = 'disabled';
|
|
246
|
+
} else {
|
|
247
|
+
job.consequentFailsCount += 1;
|
|
248
|
+
shouldRetry = true;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
job.successfullTimesRunned++;
|
|
252
|
+
job.consequentFailsCount = 0;
|
|
253
|
+
if (!schedule.hasNext()) {
|
|
254
|
+
job.status = 'finished';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await this._jobsRepository.saveJob(job);
|
|
259
|
+
|
|
260
|
+
if (shouldRetry) {
|
|
261
|
+
await this.startJobInstance(instance);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
protected async scheduleJobTo(
|
|
267
|
+
job: Job,
|
|
268
|
+
date: Date,
|
|
269
|
+
index: number
|
|
270
|
+
): Promise<JobInstance | null> {
|
|
271
|
+
let timer;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const now = new Date();
|
|
275
|
+
let interval = date.getTime() - now.getTime();
|
|
276
|
+
if (interval < 0) {
|
|
277
|
+
interval = 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const instance = await this._jobsRepository.addInstance(job.id, {
|
|
281
|
+
scheduledTo: date,
|
|
282
|
+
status: 'scheduled',
|
|
283
|
+
timeout: job.timeout,
|
|
284
|
+
index
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
timer = setTimeout(async () => {
|
|
288
|
+
const actualJob = await this._jobsRepository.getJobById(job.id);
|
|
289
|
+
if (actualJob.status !== 'active') {
|
|
290
|
+
instance.status = 'canceled';
|
|
291
|
+
await this._jobsRepository.saveInstance(instance);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await this.startJobInstance(instance);
|
|
295
|
+
}, interval);
|
|
296
|
+
|
|
297
|
+
return instance;
|
|
298
|
+
} catch (e) {
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
// console.log(e);
|
|
301
|
+
// console.log('task failed!');
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected async checkForUpcomingJobs(): Promise<void> {
|
|
307
|
+
const jobs = await this._jobsRepository.getJobs();
|
|
308
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
309
|
+
if (jobs[i].status !== 'active') continue;
|
|
310
|
+
|
|
311
|
+
const schedule = await this.getJobSchedule(jobs[i]);
|
|
312
|
+
|
|
313
|
+
if (schedule.hasNext()) {
|
|
314
|
+
const scheduledInstances =
|
|
315
|
+
await this._jobsRepository.getInstancesWithStatus(
|
|
316
|
+
jobs[i].id,
|
|
317
|
+
'scheduled'
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
while (schedule.hasNext(SCHEDULE_JOB_SPAN)) {
|
|
321
|
+
const { date: nextRun, index } = schedule.next();
|
|
322
|
+
const alreadyScheduled = scheduledInstances.find(
|
|
323
|
+
(i) => i.index === index
|
|
324
|
+
);
|
|
325
|
+
if (alreadyScheduled) continue;
|
|
326
|
+
|
|
327
|
+
await this.scheduleJobTo(jobs[i], nextRun, index);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
jobs[i].status = 'finished';
|
|
331
|
+
await this._jobsRepository.saveJob(jobs[i]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
public async start() {
|
|
337
|
+
if (this._status === 'started') {
|
|
338
|
+
throw new Error('Scheduler is already started');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.status = 'started';
|
|
342
|
+
|
|
343
|
+
this._checkTimer = setInterval(
|
|
344
|
+
this.checkForUpcomingJobs.bind(this),
|
|
345
|
+
CHECK_INTERVAL
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// TODO: add logic
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public stop() {
|
|
352
|
+
if (this._status === 'stopped') {
|
|
353
|
+
throw new Error('Scheduler is already stopped');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
clearInterval(this._checkTimer);
|
|
357
|
+
|
|
358
|
+
this.status = 'stopped';
|
|
359
|
+
// TODO: add logic
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public async addJob(job: CreateJobRequest) {
|
|
363
|
+
const validationResult =
|
|
364
|
+
await schemaRegistry.schemas.Models.CreateJobRequest.validate(job);
|
|
365
|
+
if (!validationResult.valid) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Invalid CreateJobRequest: ${validationResult.errors?.join(
|
|
368
|
+
'; '
|
|
369
|
+
)}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const path = pathJoin(this._rootFolder, job.path);
|
|
374
|
+
await fsAccess(path, fs.constants.R_OK);
|
|
375
|
+
|
|
376
|
+
this._jobProps.set(job.id, job.props);
|
|
377
|
+
|
|
378
|
+
await this._jobsRepository.createJob({
|
|
379
|
+
id: job.id,
|
|
380
|
+
createdAt: new Date(),
|
|
381
|
+
schedule: job.schedule,
|
|
382
|
+
timeout: job.timeout || DEFAULT_JOB_TIMEOUT,
|
|
383
|
+
path: job.path,
|
|
384
|
+
consequentFailsCount: 0,
|
|
385
|
+
timesRunned: 0,
|
|
386
|
+
successfullTimesRunned: 0,
|
|
387
|
+
maxConsequentFails:
|
|
388
|
+
typeof job.maxConsequentFails === 'number'
|
|
389
|
+
? job.maxConsequentFails
|
|
390
|
+
: DEFAULT_MAX_CONSEQUENT_FAILS
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
constructor(props: JobSchedulerProps) {
|
|
395
|
+
super();
|
|
396
|
+
if (typeof props.rootFolder !== 'string') {
|
|
397
|
+
throw new Error('rootFolder must be a string');
|
|
398
|
+
}
|
|
399
|
+
if (typeof props.defaultTimeZone === 'string') {
|
|
400
|
+
this._defaultTimezone = props.defaultTimeZone;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (typeof props.persistRepository === 'object') {
|
|
404
|
+
this._jobsRepository = props.persistRepository;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this._rootFolder = props.rootFolder;
|
|
408
|
+
|
|
409
|
+
setInterval(() => {
|
|
410
|
+
this._jobsRepository.dumpJobs();
|
|
411
|
+
this._jobsRepository.dumpInstances();
|
|
412
|
+
}, 10 * 1000);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
public on<T extends keyof Events>(name: T, callback: Events[T]): this {
|
|
416
|
+
super.on(name, callback);
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Job, JobInstance, JobInstanceStatus, JobStatus } from './types.js';
|
|
2
|
+
|
|
3
|
+
type AddJobRequest = Omit<Job, 'status'>;
|
|
4
|
+
type AddJobInstanceRequest = Omit<JobInstance, 'id' | 'jobId'>;
|
|
5
|
+
|
|
6
|
+
export interface IJobRepository {
|
|
7
|
+
getJobs(): Promise<Job[]>;
|
|
8
|
+
getJobById(jobId: string): Promise<Job>;
|
|
9
|
+
createJob(item: AddJobRequest): Promise<Job>;
|
|
10
|
+
|
|
11
|
+
saveJob(job: Job): Promise<Job>;
|
|
12
|
+
|
|
13
|
+
getInstancesWithStatus(
|
|
14
|
+
jobId: string,
|
|
15
|
+
status: JobInstanceStatus
|
|
16
|
+
): Promise<JobInstance[]>;
|
|
17
|
+
|
|
18
|
+
addInstance(
|
|
19
|
+
jobId: string,
|
|
20
|
+
instance: AddJobInstanceRequest
|
|
21
|
+
): Promise<JobInstance>;
|
|
22
|
+
|
|
23
|
+
saveInstance(instance: JobInstance): Promise<JobInstance>;
|
|
24
|
+
|
|
25
|
+
// TODO: remove
|
|
26
|
+
dumpJobs(): void;
|
|
27
|
+
dumpInstances(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class InMemoryJobRepository implements IJobRepository {
|
|
31
|
+
private _jobs: Array<Job> = [];
|
|
32
|
+
private _jobInstances: Array<JobInstance> = [];
|
|
33
|
+
private _instanceId = 1;
|
|
34
|
+
|
|
35
|
+
async getJobs(): Promise<Job[]> {
|
|
36
|
+
return this._jobs;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async createJob(item: AddJobRequest): Promise<Job> {
|
|
40
|
+
const job: Job = {
|
|
41
|
+
...item,
|
|
42
|
+
status: 'active'
|
|
43
|
+
};
|
|
44
|
+
this._jobs.push(job);
|
|
45
|
+
return job;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getInstances(jobId: string): Promise<JobInstance[]> {
|
|
49
|
+
return this._jobInstances.filter((ji) => ji.jobId === jobId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async addInstance(
|
|
53
|
+
jobId: string,
|
|
54
|
+
instance: AddJobInstanceRequest
|
|
55
|
+
): Promise<JobInstance> {
|
|
56
|
+
const newInstance = {
|
|
57
|
+
...instance,
|
|
58
|
+
id: this._instanceId++,
|
|
59
|
+
jobId
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this._jobInstances.push(newInstance);
|
|
63
|
+
|
|
64
|
+
return newInstance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getJobById(jobId: string): Promise<Job> {
|
|
68
|
+
return this._jobs.find((j) => j.id === jobId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async setJobStatus(jobId: string, status: JobStatus): Promise<Job> {
|
|
72
|
+
const job = await this.getJobById(jobId);
|
|
73
|
+
if (!job) return null;
|
|
74
|
+
job.status = status;
|
|
75
|
+
return job;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async saveJob(job: Job): Promise<Job> {
|
|
79
|
+
const index = this._jobs.findIndex((j) => j.id === job.id);
|
|
80
|
+
if (index !== -1) {
|
|
81
|
+
this._jobs[index] = {
|
|
82
|
+
...job
|
|
83
|
+
};
|
|
84
|
+
return this._jobs[index];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = {
|
|
88
|
+
...job
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this._jobs.push(result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getInstancesWithStatus(
|
|
96
|
+
jobId: string,
|
|
97
|
+
status: JobInstanceStatus
|
|
98
|
+
): Promise<JobInstance[]> {
|
|
99
|
+
return (await this.getInstances(jobId)).filter(
|
|
100
|
+
(i) => i.status === status
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async getInstanceById(id: number): Promise<JobInstance> {
|
|
105
|
+
return this._jobInstances.find((ji) => ji.id === id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async saveInstance(instance: JobInstance): Promise<JobInstance> {
|
|
109
|
+
const oldIndex = this._jobInstances.findIndex(
|
|
110
|
+
(ji) => ji.id === instance.id
|
|
111
|
+
);
|
|
112
|
+
if (oldIndex !== -1) {
|
|
113
|
+
this._jobInstances[oldIndex] = {
|
|
114
|
+
...instance
|
|
115
|
+
};
|
|
116
|
+
return this._jobInstances[oldIndex];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = {
|
|
120
|
+
...instance,
|
|
121
|
+
id: this._instanceId++
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this._jobInstances.push(result);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
dumpJobs(): void {
|
|
129
|
+
// console.table(this._jobs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
dumpInstances(): void {
|
|
133
|
+
// console.table(this._jobInstances);
|
|
134
|
+
}
|
|
135
|
+
}
|