@cleverbrush/scheduler 1.0.0-beta.3 → 1.0.0-beta.5

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.
@@ -0,0 +1,10 @@
1
+ import { Schedule } from './types.js';
2
+ export declare class ScheduleCalculator {
3
+ #private;
4
+ constructor(schedule: Schedule);
5
+ hasNext(span?: number): boolean;
6
+ next(): {
7
+ date: Date;
8
+ index: number;
9
+ };
10
+ }
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScheduleCalculator = void 0;
4
+ const MS_IN_DAY = 1000 * 60 * 60 * 24;
5
+ const MS_IN_WEEK = MS_IN_DAY * 7;
6
+ const getDayOfWeek = (date) => {
7
+ const res = date.getUTCDay();
8
+ if (res === 0)
9
+ return 7;
10
+ return res;
11
+ };
12
+ const getNumberOfDaysInMonth = (date) => {
13
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0, 0, 0, 0, 0)).getDate();
14
+ };
15
+ class ScheduleCalculator {
16
+ #schedule;
17
+ #currentDate = new Date();
18
+ #hour = 9;
19
+ #minute = 0;
20
+ #maxRepeat = -1;
21
+ #repeatCount = 0;
22
+ #hasNext = false;
23
+ #next;
24
+ constructor(schedule) {
25
+ if (!schedule)
26
+ throw new Error('schedule is required');
27
+ this.#schedule = { ...schedule };
28
+ if (typeof schedule.startsOn !== 'undefined') {
29
+ this.#currentDate = schedule.startsOn;
30
+ }
31
+ else {
32
+ this.#schedule.startsOn = new Date();
33
+ this.#currentDate = this.#schedule.startsOn;
34
+ }
35
+ if (schedule.every !== 'minute') {
36
+ if (typeof schedule.hour === 'number') {
37
+ this.#hour = schedule.hour;
38
+ }
39
+ if (typeof schedule.minute === 'number') {
40
+ this.#minute = schedule.minute;
41
+ }
42
+ if (schedule.every === 'day' &&
43
+ new Date(Date.UTC(this.#currentDate.getUTCFullYear(), this.#currentDate.getUTCMonth(), this.#currentDate.getUTCDate(), this.#hour, this.#minute, 0, 0)).getTime() < this.#currentDate.getTime()) {
44
+ const date = new Date(Date.UTC(this.#currentDate.getUTCFullYear(), this.#currentDate.getUTCMonth(), this.#currentDate.getUTCDate() + 1, this.#hour, this.#minute, 0, 0));
45
+ this.#currentDate = date;
46
+ }
47
+ }
48
+ if (typeof schedule.maxOccurences === 'number') {
49
+ this.#maxRepeat = schedule.maxOccurences;
50
+ }
51
+ const next = this.#getNext();
52
+ if (typeof next !== 'undefined') {
53
+ this.#next = next;
54
+ this.#hasNext = true;
55
+ }
56
+ let leftToSkip = typeof this.#schedule.skipFirst === 'number'
57
+ ? this.#schedule.skipFirst
58
+ : 0;
59
+ while (leftToSkip-- > 0 && this.#hasNext) {
60
+ this.next();
61
+ }
62
+ }
63
+ #getNext() {
64
+ let candidate = null;
65
+ let dayOfWeek;
66
+ switch (this.#schedule.every) {
67
+ case 'minute':
68
+ candidate =
69
+ this.#repeatCount === 0
70
+ ? this.#schedule.startsOn
71
+ : new Date(Date.UTC(this.#currentDate.getUTCFullYear(), this.#currentDate.getUTCMonth(), this.#currentDate.getUTCDate(), this.#currentDate.getUTCHours(), this.#currentDate.getUTCMinutes() +
72
+ this.#schedule.interval, this.#currentDate.getUTCSeconds(), this.#currentDate.getUTCMilliseconds()));
73
+ break;
74
+ case 'day':
75
+ {
76
+ const date = new Date(this.#currentDate.getTime() +
77
+ MS_IN_DAY *
78
+ (this.#repeatCount === 0
79
+ ? 0
80
+ : this.#schedule.interval - 1));
81
+ candidate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), this.#hour, this.#minute, 0, 0));
82
+ }
83
+ break;
84
+ case 'week':
85
+ {
86
+ let date = this.#currentDate;
87
+ do {
88
+ let found = false;
89
+ dayOfWeek = getDayOfWeek(date);
90
+ if (Number.isNaN(dayOfWeek))
91
+ return;
92
+ for (let i = 0; i < 7 - dayOfWeek + 1; i++) {
93
+ date = new Date(date.getTime() + (i == 0 ? 0 : MS_IN_DAY));
94
+ if (this.#schedule.endsOn &&
95
+ date > this.#schedule.endsOn) {
96
+ return;
97
+ }
98
+ if (this.#schedule.dayOfWeek.includes(getDayOfWeek(date))) {
99
+ const dateWithTime = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), this.#hour, this.#minute, 0, 0));
100
+ if (dateWithTime > this.#schedule.startsOn) {
101
+ candidate = dateWithTime;
102
+ found = true;
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ if (found)
108
+ break;
109
+ date = new Date(date.getTime() +
110
+ MS_IN_DAY +
111
+ (this.#repeatCount == 0
112
+ ? 0
113
+ : (this.#schedule.interval - 1) *
114
+ MS_IN_WEEK));
115
+ } while ((this.#schedule.endsOn &&
116
+ date <= this.#schedule.endsOn) ||
117
+ !this.#schedule.endsOn);
118
+ }
119
+ break;
120
+ case 'month':
121
+ {
122
+ let dateTime;
123
+ const cDate = this.#currentDate;
124
+ let iteration = 0;
125
+ do {
126
+ const date = this.#schedule.day === 'last'
127
+ ? getNumberOfDaysInMonth(new Date(Date.UTC(cDate.getUTCFullYear(), cDate.getUTCMonth() +
128
+ iteration *
129
+ (this.#repeatCount === 0
130
+ ? 1
131
+ : this.#schedule
132
+ .interval), 1, 0, 0, 0, 0)))
133
+ : this.#schedule.day;
134
+ dateTime = new Date(Date.UTC(cDate.getUTCFullYear(), cDate.getUTCMonth() +
135
+ iteration *
136
+ (this.#repeatCount === 0
137
+ ? 1
138
+ : this.#schedule.interval), date, this.#hour, this.#minute, 0, 0));
139
+ iteration++;
140
+ } while (dateTime < this.#schedule.startsOn ||
141
+ dateTime <= this.#currentDate);
142
+ candidate = dateTime;
143
+ }
144
+ break;
145
+ case 'year':
146
+ {
147
+ let dateTime;
148
+ const cDate = this.#currentDate;
149
+ let iteration = 0;
150
+ do {
151
+ const date = this.#schedule.day === 'last'
152
+ ? getNumberOfDaysInMonth(new Date(Date.UTC(cDate.getUTCFullYear() +
153
+ iteration *
154
+ (this.#repeatCount === 0
155
+ ? 1
156
+ : this.#schedule
157
+ .interval), this.#schedule.month - 1, 1, 0, 0, 0, 0)))
158
+ : this.#schedule.day;
159
+ dateTime = new Date(Date.UTC(cDate.getUTCFullYear() +
160
+ iteration *
161
+ (this.#repeatCount === 0
162
+ ? 1
163
+ : this.#schedule.interval), this.#schedule.month - 1, date, this.#hour, this.#minute, 0, 0));
164
+ iteration++;
165
+ } while (dateTime < this.#schedule.startsOn ||
166
+ dateTime <= this.#currentDate);
167
+ candidate = dateTime;
168
+ }
169
+ break;
170
+ default:
171
+ throw new Error('unknown schedule type');
172
+ }
173
+ if (!candidate)
174
+ return;
175
+ if (typeof this.#schedule.endsOn !== 'undefined' &&
176
+ candidate > this.#schedule.endsOn) {
177
+ return;
178
+ }
179
+ return candidate;
180
+ }
181
+ hasNext(span) {
182
+ if (!this.#hasNext) {
183
+ return false;
184
+ }
185
+ if (typeof span !== 'number')
186
+ return this.#hasNext;
187
+ return this.#next.getTime() - new Date().getTime() <= span;
188
+ }
189
+ next() {
190
+ if (!this.#hasNext)
191
+ throw new Error('schedule is over');
192
+ const result = this.#next;
193
+ this.#currentDate = new Date(result.getTime() +
194
+ (['day', 'week'].includes(this.#schedule.every) ? MS_IN_DAY : 0));
195
+ this.#repeatCount++;
196
+ const next = this.#getNext();
197
+ this.#next = next;
198
+ this.#hasNext = typeof next !== 'undefined';
199
+ if (this.#maxRepeat > 0 && this.#repeatCount >= this.#maxRepeat) {
200
+ this.#next = undefined;
201
+ this.#hasNext = false;
202
+ }
203
+ return {
204
+ date: result,
205
+ index: this.#repeatCount
206
+ };
207
+ }
208
+ }
209
+ exports.ScheduleCalculator = ScheduleCalculator;
210
+ //# sourceMappingURL=ScheduleCalculator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScheduleCalculator.js","sourceRoot":"","sources":["../src/ScheduleCalculator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /// <reference types="node" />
4
+ import { EventEmitter } from 'events';
5
+ import { Readable } from 'stream';
6
+ import { Worker } from 'worker_threads';
7
+ import { JobSchedulerProps, CreateJobRequest, SchedulerStatus, Schedule, Job, JobInstance, JobInstanceStatus, Schemas } from './types.js';
8
+ import { ScheduleCalculator } from './ScheduleCalculator.js';
9
+ import { IJobRepository } from './jobRepository.js';
10
+ export { ScheduleCalculator, Schedule as TaskSchedule, Schemas };
11
+ declare type WorkerResult = {
12
+ status: JobInstanceStatus;
13
+ exitCode: number;
14
+ error?: Error;
15
+ };
16
+ declare type JobStartItem = {
17
+ jobId: string;
18
+ instanceId: number;
19
+ stdout: Readable;
20
+ stderr: Readable;
21
+ startDate: Date;
22
+ };
23
+ declare type JobEndItem = JobStartItem & {
24
+ endDate: Date;
25
+ };
26
+ declare type JobErrorItem = JobStartItem & {
27
+ endDate: Date;
28
+ error?: Error;
29
+ };
30
+ declare type JobMessageItem = Omit<JobStartItem, 'stdout' | 'stderr'> & {
31
+ value: any;
32
+ };
33
+ declare type Events = {
34
+ 'job:start': (job: JobStartItem) => any;
35
+ 'job:end': (job: JobEndItem) => any;
36
+ 'job:error': (job: JobErrorItem) => any;
37
+ 'job:timeout': (job: JobErrorItem) => any;
38
+ 'job:message': (msg: JobMessageItem) => any;
39
+ };
40
+ interface IJobScheduler {
41
+ on<T extends keyof Events>(name: T, callback: Events[T]): this;
42
+ addJob(job: CreateJobRequest): Promise<void>;
43
+ removeJob(id: string): Promise<void>;
44
+ }
45
+ export declare class JobScheduler extends EventEmitter implements IJobScheduler {
46
+ protected _rootFolder: string;
47
+ protected _status: SchedulerStatus;
48
+ protected _defaultTimezone: string;
49
+ protected _checkTimer: any;
50
+ protected _jobsRepository: IJobRepository;
51
+ protected _jobProps: Map<string, any>;
52
+ get status(): SchedulerStatus;
53
+ protected set status(val: SchedulerStatus);
54
+ private scheduleCalculatorCache;
55
+ protected getJobSchedule(job: Job): Promise<ScheduleCalculator>;
56
+ protected runWorkerWithTimeout(file: string, props: any, timeout: number): {
57
+ promise: Promise<WorkerResult>;
58
+ worker: Worker;
59
+ };
60
+ private readToEnd;
61
+ protected startJobInstance(instance: JobInstance): Promise<void>;
62
+ protected scheduleJobTo(job: Job, date: Date, index: number): Promise<JobInstance | null>;
63
+ protected checkForUpcomingJobs(): Promise<void>;
64
+ start(): Promise<void>;
65
+ stop(): void;
66
+ jobExists(jobId: string): Promise<boolean>;
67
+ removeJob(jobId: string): Promise<void>;
68
+ addJob(job: CreateJobRequest): Promise<void>;
69
+ constructor(props: JobSchedulerProps);
70
+ on<T extends keyof Events>(name: T, callback: Events[T]): this;
71
+ }
package/dist/index.js ADDED
@@ -0,0 +1,376 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JobScheduler = exports.Schemas = exports.ScheduleCalculator = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const events_1 = require("events");
9
+ const stream_1 = require("stream");
10
+ const promises_1 = require("fs/promises");
11
+ const path_1 = require("path");
12
+ const worker_threads_1 = require("worker_threads");
13
+ const types_js_1 = require("./types.js");
14
+ Object.defineProperty(exports, "Schemas", { enumerable: true, get: function () { return types_js_1.Schemas; } });
15
+ const ScheduleCalculator_js_1 = require("./ScheduleCalculator.js");
16
+ Object.defineProperty(exports, "ScheduleCalculator", { enumerable: true, get: function () { return ScheduleCalculator_js_1.ScheduleCalculator; } });
17
+ const jobRepository_js_1 = require("./jobRepository.js");
18
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
19
+ const CHECK_INTERVAL = 1000 * 10; // every 10 seconds
20
+ // const SCHEDULE_JOB_SPAN = 1000 * 60 * 60; // 1 hour
21
+ const SCHEDULE_JOB_SPAN = 1000 * 60; // 1 hour
22
+ const DEFAULT_JOB_TIMEOUT = 1000 * 20; // 20 seconds
23
+ const DEFAULT_MAX_CONSEQUENT_FAILS = 3;
24
+ const DEFAULT_MAX_RETRIES = 2;
25
+ class JobScheduler extends events_1.EventEmitter {
26
+ _rootFolder;
27
+ _status = 'stopped';
28
+ _defaultTimezone;
29
+ _checkTimer;
30
+ _jobsRepository = new jobRepository_js_1.InMemoryJobRepository();
31
+ _jobProps = new Map();
32
+ get status() {
33
+ return this._status;
34
+ }
35
+ set status(val) {
36
+ if (val === this._status)
37
+ return;
38
+ this._status = val;
39
+ }
40
+ scheduleCalculatorCache = new Map();
41
+ async getJobSchedule(job) {
42
+ if (this.scheduleCalculatorCache.has(job.id)) {
43
+ return this.scheduleCalculatorCache.get(job.id);
44
+ }
45
+ const schedule = {
46
+ ...job.schedule
47
+ };
48
+ if (typeof job.firstInstanceEndedAt !== 'undefined') {
49
+ schedule.startsOn = job.firstInstanceEndedAt;
50
+ }
51
+ if (typeof job.successfullTimesRunned === 'number') {
52
+ schedule.skipFirst = job.successfullTimesRunned - 1;
53
+ }
54
+ const res = new ScheduleCalculator_js_1.ScheduleCalculator(schedule);
55
+ this.scheduleCalculatorCache.set(job.id, res);
56
+ return res;
57
+ }
58
+ runWorkerWithTimeout(file, props, timeout) {
59
+ const worker = new worker_threads_1.Worker(file, {
60
+ workerData: props,
61
+ execArgv: ['--unhandled-rejections=strict'],
62
+ stderr: true,
63
+ stdout: true
64
+ });
65
+ const promise = new Promise((resolve) => {
66
+ let timedOut = false;
67
+ let isFinished = false;
68
+ let error;
69
+ const timeoutTimer = setTimeout(() => {
70
+ if (isFinished)
71
+ return;
72
+ timedOut = true;
73
+ worker.terminate();
74
+ resolve({
75
+ exitCode: 1,
76
+ status: 'timedout'
77
+ });
78
+ }, timeout);
79
+ worker.on('error', (e) => {
80
+ error = e;
81
+ });
82
+ worker.on('exit', (exitCode) => {
83
+ if (isFinished)
84
+ return;
85
+ if (timedOut)
86
+ return;
87
+ clearTimeout(timeoutTimer);
88
+ isFinished = true;
89
+ resolve({
90
+ status: exitCode === 0 ? 'succeeded' : 'errored',
91
+ exitCode,
92
+ error
93
+ });
94
+ });
95
+ });
96
+ return {
97
+ promise,
98
+ worker
99
+ };
100
+ }
101
+ readToEnd(source) {
102
+ return new Promise((res, rej) => {
103
+ const chunks = [];
104
+ source.on('data', (chunk) => chunks.push(chunk));
105
+ source.on('end', () => res(Buffer.concat(chunks)));
106
+ source.on('error', (err) => rej(err));
107
+ });
108
+ }
109
+ async startJobInstance(instance) {
110
+ const startDate = new Date();
111
+ instance = await this._jobsRepository.saveInstance({
112
+ ...instance,
113
+ status: 'running',
114
+ startDate
115
+ });
116
+ let status, exitCode;
117
+ try {
118
+ const job = await this._jobsRepository.getJobById(instance.jobId);
119
+ const fileName = (0, path_1.join)(this._rootFolder, job.path);
120
+ const props = this._jobProps.get(job.id);
121
+ let finalProps = typeof props === 'function' ? props() : props;
122
+ if (finalProps instanceof Promise) {
123
+ finalProps = await finalProps;
124
+ }
125
+ instance = await this._jobsRepository.saveInstance({
126
+ ...instance,
127
+ status: 'running'
128
+ });
129
+ const { promise, worker } = this.runWorkerWithTimeout(fileName, finalProps, job.timeout);
130
+ const stdOutPass = worker.stdout.pipe(new stream_1.PassThrough({
131
+ highWaterMark: MAX_BUFFER_SIZE
132
+ }));
133
+ const stdErrPass = worker.stderr.pipe(new stream_1.PassThrough({
134
+ highWaterMark: MAX_BUFFER_SIZE
135
+ }));
136
+ const stdOutForJobStart = worker.stdout.pipe(new stream_1.PassThrough({
137
+ highWaterMark: MAX_BUFFER_SIZE
138
+ }));
139
+ const stdErrForJobStart = worker.stderr.pipe(new stream_1.PassThrough({
140
+ highWaterMark: MAX_BUFFER_SIZE
141
+ }));
142
+ const stdOutForJobEnd = worker.stdout.pipe(new stream_1.PassThrough({
143
+ highWaterMark: MAX_BUFFER_SIZE
144
+ }));
145
+ const stdErrForJobEnd = worker.stderr.pipe(new stream_1.PassThrough({
146
+ highWaterMark: MAX_BUFFER_SIZE
147
+ }));
148
+ worker.on('message', (value) => {
149
+ this.emit('job:message', {
150
+ instanceId: instance.id,
151
+ jobId: job.id,
152
+ startDate,
153
+ value
154
+ });
155
+ });
156
+ this.emit('job:start', {
157
+ instanceId: instance.id,
158
+ jobId: job.id,
159
+ stderr: stdErrForJobStart,
160
+ stdout: stdOutForJobStart,
161
+ startDate
162
+ });
163
+ const stdOutStr = (await this.readToEnd(stdOutPass)).toString();
164
+ const stdErrStr = (await this.readToEnd(stdErrPass)).toString();
165
+ const result = await promise;
166
+ status = result.status;
167
+ exitCode = result.exitCode;
168
+ const { error } = result;
169
+ const endDate = new Date();
170
+ switch (status) {
171
+ case 'errored':
172
+ this.emit('job:error', {
173
+ instanceId: instance.id,
174
+ jobId: job.id,
175
+ stderr: stdErrForJobEnd,
176
+ stdout: stdOutForJobEnd,
177
+ startDate,
178
+ endDate,
179
+ error
180
+ });
181
+ break;
182
+ case 'timedout':
183
+ this.emit('job:timeout', {
184
+ instanceId: instance.id,
185
+ jobId: job.id,
186
+ stderr: stdErrForJobEnd,
187
+ stdout: stdOutForJobEnd,
188
+ startDate,
189
+ endDate,
190
+ error
191
+ });
192
+ break;
193
+ default:
194
+ this.emit('job:end', {
195
+ instanceId: instance.id,
196
+ jobId: job.id,
197
+ stderr: stdErrForJobEnd,
198
+ stdout: stdOutForJobEnd,
199
+ startDate,
200
+ endDate
201
+ });
202
+ }
203
+ instance = await this._jobsRepository.saveInstance({
204
+ ...instance,
205
+ status,
206
+ exitCode,
207
+ stdErr: stdErrStr,
208
+ stdOut: stdOutStr,
209
+ endDate
210
+ });
211
+ }
212
+ finally {
213
+ const job = {
214
+ ...(await this._jobsRepository.getJobById(instance.jobId))
215
+ };
216
+ job.timesRunned++;
217
+ let shouldRetry = false;
218
+ const schedule = await this.getJobSchedule(job);
219
+ if (status !== 'succeeded') {
220
+ if (job.consequentFailsCount + 1 >= job.maxConsequentFails &&
221
+ job.maxConsequentFails > 0) {
222
+ job.status = 'disabled';
223
+ }
224
+ else {
225
+ job.consequentFailsCount += 1;
226
+ shouldRetry = true;
227
+ }
228
+ }
229
+ else {
230
+ job.successfullTimesRunned++;
231
+ job.consequentFailsCount = 0;
232
+ if (!schedule.hasNext()) {
233
+ job.status = 'finished';
234
+ }
235
+ }
236
+ await this._jobsRepository.saveJob(job);
237
+ if (shouldRetry && instance.retryIndex < instance.maxRetries) {
238
+ await this.startJobInstance({
239
+ ...instance,
240
+ retryIndex: instance.retryIndex + 1
241
+ });
242
+ }
243
+ }
244
+ }
245
+ async scheduleJobTo(job, date, index) {
246
+ let timer;
247
+ try {
248
+ const now = new Date();
249
+ let interval = date.getTime() - now.getTime();
250
+ if (interval < 0) {
251
+ interval = 0;
252
+ }
253
+ const instance = await this._jobsRepository.addInstance(job.id, {
254
+ scheduledTo: date,
255
+ status: 'scheduled',
256
+ timeout: job.timeout,
257
+ index,
258
+ retryIndex: 0,
259
+ maxRetries: job.maxRetries
260
+ });
261
+ timer = setTimeout(async () => {
262
+ const actualJob = await this._jobsRepository.getJobById(job.id);
263
+ if (!actualJob || actualJob.status !== 'active') {
264
+ instance.status = 'canceled';
265
+ await this._jobsRepository.saveInstance(instance);
266
+ return;
267
+ }
268
+ await this.startJobInstance(instance);
269
+ }, interval);
270
+ return instance;
271
+ }
272
+ catch (e) {
273
+ clearTimeout(timer);
274
+ // console.log(e);
275
+ // console.log('task failed!');
276
+ return null;
277
+ }
278
+ }
279
+ async checkForUpcomingJobs() {
280
+ const jobs = await this._jobsRepository.getJobs();
281
+ for (let i = 0; i < jobs.length; i++) {
282
+ if (jobs[i].status !== 'active')
283
+ continue;
284
+ const schedule = await this.getJobSchedule(jobs[i]);
285
+ if (schedule.hasNext()) {
286
+ const scheduledInstances = await this._jobsRepository.getInstancesWithStatus(jobs[i].id, 'scheduled');
287
+ while (schedule.hasNext(SCHEDULE_JOB_SPAN)) {
288
+ const { date: nextRun, index } = schedule.next();
289
+ if (nextRun < new Date())
290
+ continue;
291
+ const alreadyScheduled = scheduledInstances.find((i) => i.index === index);
292
+ if (alreadyScheduled)
293
+ continue;
294
+ await this.scheduleJobTo(jobs[i], nextRun, index);
295
+ }
296
+ }
297
+ else {
298
+ jobs[i].status = 'finished';
299
+ await this._jobsRepository.saveJob(jobs[i]);
300
+ }
301
+ }
302
+ }
303
+ async start() {
304
+ if (this._status === 'started') {
305
+ throw new Error('Scheduler is already started');
306
+ }
307
+ this.status = 'started';
308
+ this._checkTimer = setInterval(this.checkForUpcomingJobs.bind(this), CHECK_INTERVAL);
309
+ // TODO: add logic
310
+ }
311
+ stop() {
312
+ if (this._status === 'stopped') {
313
+ throw new Error('Scheduler is already stopped');
314
+ }
315
+ clearInterval(this._checkTimer);
316
+ this.status = 'stopped';
317
+ // TODO: add logic
318
+ }
319
+ async jobExists(jobId) {
320
+ return (await this._jobsRepository.getJobById(jobId)) !== null;
321
+ }
322
+ async removeJob(jobId) {
323
+ if (typeof jobId !== 'string' || !jobId) {
324
+ throw new Error('id is required');
325
+ }
326
+ await this._jobsRepository.removeJob(jobId);
327
+ }
328
+ async addJob(job) {
329
+ const validationResult = await types_js_1.schemaRegistry.schemas.Models.CreateJobRequest.validate(job);
330
+ if (!validationResult.valid) {
331
+ throw new Error(`Invalid CreateJobRequest: ${validationResult.errors?.join('; ')}`);
332
+ }
333
+ const path = (0, path_1.join)(this._rootFolder, job.path);
334
+ await (0, promises_1.access)(path, fs_1.default.constants.R_OK);
335
+ this._jobProps.set(job.id, job.props);
336
+ await this._jobsRepository.createJob({
337
+ id: job.id,
338
+ createdAt: new Date(),
339
+ schedule: job.schedule,
340
+ timeout: job.timeout || DEFAULT_JOB_TIMEOUT,
341
+ path: job.path,
342
+ consequentFailsCount: 0,
343
+ timesRunned: 0,
344
+ successfullTimesRunned: 0,
345
+ maxConsequentFails: typeof job.maxConsequentFails === 'number'
346
+ ? job.maxConsequentFails
347
+ : DEFAULT_MAX_CONSEQUENT_FAILS,
348
+ maxRetries: typeof job.maxRetries === 'number'
349
+ ? job.maxRetries
350
+ : DEFAULT_MAX_RETRIES
351
+ });
352
+ }
353
+ constructor(props) {
354
+ super();
355
+ if (typeof props.rootFolder !== 'string') {
356
+ throw new Error('rootFolder must be a string');
357
+ }
358
+ if (typeof props.defaultTimeZone === 'string') {
359
+ this._defaultTimezone = props.defaultTimeZone;
360
+ }
361
+ if (typeof props.persistRepository === 'object') {
362
+ this._jobsRepository = props.persistRepository;
363
+ }
364
+ this._rootFolder = props.rootFolder;
365
+ setInterval(() => {
366
+ this._jobsRepository.dumpJobs();
367
+ this._jobsRepository.dumpInstances();
368
+ }, 10 * 1000);
369
+ }
370
+ on(name, callback) {
371
+ super.on(name, callback);
372
+ return this;
373
+ }
374
+ }
375
+ exports.JobScheduler = JobScheduler;
376
+ //# sourceMappingURL=index.js.map